| 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 | } |