| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | use model::KeyKind; |
| 13 | + | use pgp::composed::{Deserializable, SignedPublicKey}; |
| 14 | + | use pgp::types::KeyDetails; |
| 15 | + | use ssh_key::{HashAlg, PublicKey}; |
| 16 | + | |
| 17 | + | use crate::GitError; |
| 18 | + | |
| 19 | + | |
| 20 | + | #[derive(Debug, Clone, PartialEq, Eq)] |
| 21 | + | pub struct ParsedKey { |
| 22 | + | |
| 23 | + | pub fingerprint: String, |
| 24 | + | |
| 25 | + | |
| 26 | + | pub public_key: String, |
| 27 | + | |
| 28 | + | |
| 29 | + | pub label: Option<String>, |
| 30 | + | } |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | pub fn parse_public_key(kind: KeyKind, material: &str) -> Result<ParsedKey, GitError> { |
| 39 | + | match kind { |
| 40 | + | KeyKind::Ssh => parse_ssh(material), |
| 41 | + | KeyKind::Gpg => parse_gpg(material), |
| 42 | + | } |
| 43 | + | } |
| 44 | + | |
| 45 | + | |
| 46 | + | fn parse_ssh(material: &str) -> Result<ParsedKey, GitError> { |
| 47 | + | let key = PublicKey::from_openssh(material.trim()).map_err(|err| GitError::BadPublicKey { |
| 48 | + | kind: KeyKind::Ssh, |
| 49 | + | reason: err.to_string(), |
| 50 | + | })?; |
| 51 | + | let comment = key.comment().trim(); |
| 52 | + | Ok(ParsedKey { |
| 53 | + | fingerprint: key.fingerprint(HashAlg::Sha256).to_string(), |
| 54 | + | public_key: key.to_openssh().map_err(|err| GitError::BadPublicKey { |
| 55 | + | kind: KeyKind::Ssh, |
| 56 | + | reason: err.to_string(), |
| 57 | + | })?, |
| 58 | + | label: (!comment.is_empty()).then(|| comment.to_string()), |
| 59 | + | }) |
| 60 | + | } |
| 61 | + | |
| 62 | + | |
| 63 | + | fn parse_gpg(material: &str) -> Result<ParsedKey, GitError> { |
| 64 | + | let (key, _headers) = |
| 65 | + | SignedPublicKey::from_string(material).map_err(|err| GitError::BadPublicKey { |
| 66 | + | kind: KeyKind::Gpg, |
| 67 | + | reason: err.to_string(), |
| 68 | + | })?; |
| 69 | + | Ok(ParsedKey { |
| 70 | + | fingerprint: format!("{:x}", key.fingerprint()), |
| 71 | + | public_key: material.trim().to_string(), |
| 72 | + | |
| 73 | + | label: None, |
| 74 | + | }) |
| 75 | + | } |
| 76 | + | |
| 77 | + | #[cfg(test)] |
| 78 | + | mod tests { |
| 79 | + | #![allow(clippy::unwrap_used)] |
| 80 | + | |
| 81 | + | use super::*; |
| 82 | + | |
| 83 | + | |
| 84 | + | const GPG_PUB: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n\ |
| 85 | + | mDMEamQrVhYJKwYBBAHaRw8BAQdAZmnSAPnE0i9ZFD376jPvfybRQnCVgdZznu/Z\n\ |
| 86 | + | xu9NMc+0I0ZhYnJpY2EgVGVzdCA8dGVzdEBmYWJyaWNhLmV4YW1wbGU+iJAEExYK\n\ |
| 87 | + | ADgWIQRCw0r0w7/IPhyGEPWZoNNqXdKXKAUCamQrVgIbAwULCQgHAgYVCgkICwIE\n\ |
| 88 | + | FgIDAQIeAQIXgAAKCRCZoNNqXdKXKHT3APkBBY7MB1be0vsOf0Krr5+mLzStHwjQ\n\ |
| 89 | + | ZhFF/DtG/1L+sgD+KcKERhLj5L5b/FHBxYGpw2ZY4aQoCOeGmk+zmflLbwS4OARq\n\ |
| 90 | + | ZCtWEgorBgEEAZdVAQUBAQdAeLjNUQ2tEAUDtr+Iu7qa/EOArrAd3QSWKHNWXfX7\n\ |
| 91 | + | 4lIDAQgHiHgEGBYKACAWIQRCw0r0w7/IPhyGEPWZoNNqXdKXKAUCamQrVgIbDAAK\n\ |
| 92 | + | CRCZoNNqXdKXKJIEAP9Z2KdPQ7yVGAC6XjDoYKbbeAu58G4L4hqiq+JK6v18lwD/\n\ |
| 93 | + | T4OoXtnXPpwpy5jv0KTtE+cjQCl1W2qhQTFSqlXeawM=\n\ |
| 94 | + | =EOde\n\ |
| 95 | + | -----END PGP PUBLIC KEY BLOCK-----\n"; |
| 96 | + | |
| 97 | + | #[test] |
| 98 | + | fn parses_ssh_key_and_fingerprint() { |
| 99 | + | use rand_core::OsRng; |
| 100 | + | use ssh_key::{Algorithm, PrivateKey}; |
| 101 | + | |
| 102 | + | let private = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); |
| 103 | + | let openssh = private.public_key().to_openssh().unwrap(); |
| 104 | + | |
| 105 | + | let parsed = parse_public_key(KeyKind::Ssh, &format!("{openssh} ada@host")).unwrap(); |
| 106 | + | assert!(parsed.fingerprint.starts_with("SHA256:")); |
| 107 | + | assert_eq!( |
| 108 | + | parsed.fingerprint, |
| 109 | + | private |
| 110 | + | .public_key() |
| 111 | + | .fingerprint(HashAlg::Sha256) |
| 112 | + | .to_string() |
| 113 | + | ); |
| 114 | + | assert_eq!(parsed.label.as_deref(), Some("ada@host")); |
| 115 | + | } |
| 116 | + | |
| 117 | + | #[test] |
| 118 | + | fn parses_gpg_key_and_fingerprint() { |
| 119 | + | let parsed = parse_public_key(KeyKind::Gpg, GPG_PUB).unwrap(); |
| 120 | + | assert_eq!( |
| 121 | + | parsed.fingerprint, |
| 122 | + | "42c34af4c3bfc83e1c8610f599a0d36a5dd29728" |
| 123 | + | ); |
| 124 | + | assert!(parsed.label.is_none()); |
| 125 | + | } |
| 126 | + | |
| 127 | + | #[test] |
| 128 | + | fn rejects_garbage() { |
| 129 | + | assert!(matches!( |
| 130 | + | parse_public_key(KeyKind::Ssh, "not a key"), |
| 131 | + | Err(GitError::BadPublicKey { .. }) |
| 132 | + | )); |
| 133 | + | assert!(matches!( |
| 134 | + | parse_public_key(KeyKind::Gpg, "-----BEGIN PGP PUBLIC KEY BLOCK-----\nx\n"), |
| 135 | + | Err(GitError::BadPublicKey { .. }) |
| 136 | + | )); |
| 137 | + | } |
| 138 | + | } |