// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Parsing and fingerprinting of registered public keys. //! //! `fabrica key add` validates key material before storing it and computes the //! fingerprint the schema keys on. This shares the same `ssh-key` / `pgp` crates as //! signature verification (§3.6), so a key that can be registered is a key we can //! actually verify against. use model::KeyKind; use pgp::composed::{Deserializable, SignedPublicKey}; use pgp::types::KeyDetails; use ssh_key::{HashAlg, PublicKey}; use crate::GitError; /// A parsed, validated public key ready to persist. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParsedKey { /// The fingerprint the store keys on: `SHA256:…` for SSH, lowercase hex for GPG. pub fingerprint: String, /// The canonical text to store: a single-line `authorized_keys` entry (SSH) or /// the ASCII-armored block as given (GPG). pub public_key: String, /// A default label derived from the key (the SSH comment, or the GPG user id), /// used when the operator does not supply one. pub label: Option, } /// Parse and fingerprint `material` as a key of the declared `kind`. /// /// # Errors /// /// Returns [`GitError::BadPublicKey`] if the material does not parse as the declared /// kind. pub fn parse_public_key(kind: KeyKind, material: &str) -> Result { match kind { KeyKind::Ssh => parse_ssh(material), KeyKind::Gpg => parse_gpg(material), } } /// Parse an OpenSSH `authorized_keys` line. fn parse_ssh(material: &str) -> Result { let key = PublicKey::from_openssh(material.trim()).map_err(|err| GitError::BadPublicKey { kind: KeyKind::Ssh, reason: err.to_string(), })?; let comment = key.comment().trim(); Ok(ParsedKey { fingerprint: key.fingerprint(HashAlg::Sha256).to_string(), public_key: key.to_openssh().map_err(|err| GitError::BadPublicKey { kind: KeyKind::Ssh, reason: err.to_string(), })?, label: (!comment.is_empty()).then(|| comment.to_string()), }) } /// Parse an ASCII-armored `OpenPGP` public key block. fn parse_gpg(material: &str) -> Result { let (key, _headers) = SignedPublicKey::from_string(material).map_err(|err| GitError::BadPublicKey { kind: KeyKind::Gpg, reason: err.to_string(), })?; Ok(ParsedKey { fingerprint: format!("{:x}", key.fingerprint()), public_key: material.trim().to_string(), // The operator supplies the label for GPG keys. label: None, }) } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use super::*; // Reuse the static GPG public key fixture from the signature tests. const GPG_PUB: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n\ mDMEamQrVhYJKwYBBAHaRw8BAQdAZmnSAPnE0i9ZFD376jPvfybRQnCVgdZznu/Z\n\ xu9NMc+0I0ZhYnJpY2EgVGVzdCA8dGVzdEBmYWJyaWNhLmV4YW1wbGU+iJAEExYK\n\ ADgWIQRCw0r0w7/IPhyGEPWZoNNqXdKXKAUCamQrVgIbAwULCQgHAgYVCgkICwIE\n\ FgIDAQIeAQIXgAAKCRCZoNNqXdKXKHT3APkBBY7MB1be0vsOf0Krr5+mLzStHwjQ\n\ ZhFF/DtG/1L+sgD+KcKERhLj5L5b/FHBxYGpw2ZY4aQoCOeGmk+zmflLbwS4OARq\n\ ZCtWEgorBgEEAZdVAQUBAQdAeLjNUQ2tEAUDtr+Iu7qa/EOArrAd3QSWKHNWXfX7\n\ 4lIDAQgHiHgEGBYKACAWIQRCw0r0w7/IPhyGEPWZoNNqXdKXKAUCamQrVgIbDAAK\n\ CRCZoNNqXdKXKJIEAP9Z2KdPQ7yVGAC6XjDoYKbbeAu58G4L4hqiq+JK6v18lwD/\n\ T4OoXtnXPpwpy5jv0KTtE+cjQCl1W2qhQTFSqlXeawM=\n\ =EOde\n\ -----END PGP PUBLIC KEY BLOCK-----\n"; #[test] fn parses_ssh_key_and_fingerprint() { use rand_core::OsRng; use ssh_key::{Algorithm, PrivateKey}; let private = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); let openssh = private.public_key().to_openssh().unwrap(); let parsed = parse_public_key(KeyKind::Ssh, &format!("{openssh} ada@host")).unwrap(); assert!(parsed.fingerprint.starts_with("SHA256:")); assert_eq!( parsed.fingerprint, private .public_key() .fingerprint(HashAlg::Sha256) .to_string() ); assert_eq!(parsed.label.as_deref(), Some("ada@host")); } #[test] fn parses_gpg_key_and_fingerprint() { let parsed = parse_public_key(KeyKind::Gpg, GPG_PUB).unwrap(); assert_eq!( parsed.fingerprint, "42c34af4c3bfc83e1c8610f599a0d36a5dd29728" ); assert!(parsed.label.is_none()); } #[test] fn rejects_garbage() { assert!(matches!( parse_public_key(KeyKind::Ssh, "not a key"), Err(GitError::BadPublicKey { .. }) )); assert!(matches!( parse_public_key(KeyKind::Gpg, "-----BEGIN PGP PUBLIC KEY BLOCK-----\nx\n"), Err(GitError::BadPublicKey { .. }) )); } }