Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
crates/git/src/lib.rs +11 −0
| @@ -28,6 +28,7 @@ | |||
| 28 | //! name→path authority; [`repo_path`] is the one place that mapping is computed. | 28 | //! name→path authority; [`repo_path`] is the one place that mapping is computed. |
| 29 | 29 | ||
| 30 | mod attributes; | 30 | mod attributes; |
| 31 | mod pubkey; | ||
| 31 | mod repo; | 32 | mod repo; |
| 32 | mod signature; | 33 | mod signature; |
| 33 | mod types; | 34 | mod types; |
| @@ -40,6 +41,7 @@ use std::path::{Path, PathBuf}; | |||
| 40 | use git2::{Repository, RepositoryInitOptions}; | 41 | use git2::{Repository, RepositoryInitOptions}; |
| 41 | 42 | ||
| 42 | pub use crate::attributes::{AttrValue, Attributes, AttributesCache, AttributesSet}; | 43 | pub use crate::attributes::{AttrValue, Attributes, AttributesCache, AttributesSet}; |
| 44 | pub use crate::pubkey::{ParsedKey, parse_public_key}; | ||
| 43 | pub use crate::repo::Repo; | 45 | pub use crate::repo::Repo; |
| 44 | pub use crate::signature::{SignatureCache, SignatureState, SigningKey}; | 46 | pub use crate::signature::{SignatureCache, SignatureState, SigningKey}; |
| 45 | pub use crate::types::{ | 47 | pub use crate::types::{ |
| @@ -79,6 +81,15 @@ pub enum GitError { | |||
| 79 | /// A path resolved to something other than a file where a file was required. | 81 | /// A path resolved to something other than a file where a file was required. |
| 80 | #[error("path {0:?} is not a file")] | 82 | #[error("path {0:?} is not a file")] |
| 81 | NotAFile(String), | 83 | NotAFile(String), |
| 84 | |||
| 85 | /// A public key could not be parsed as the declared kind. | ||
| 86 | #[error("invalid {kind} public key: {reason}")] | ||
| 87 | BadPublicKey { | ||
| 88 | /// The declared key kind. | ||
| 89 | kind: model::KeyKind, | ||
| 90 | /// A human-readable parse failure reason. | ||
| 91 | reason: String, | ||
| 92 | }, | ||
| 82 | } | 93 | } |
| 83 | 94 | ||
| 84 | /// Compute the on-disk path of a repository from its id: `{root}/{id[0..2]}/{id}.git`. | 95 | /// Compute the on-disk path of a repository from its id: `{root}/{id[0..2]}/{id}.git`. |
crates/git/src/pubkey.rs +138 −0
| @@ -0,0 +1,138 @@ | |||
| 1 | // This Source Code Form is subject to the terms of the Mozilla Public | ||
| 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
| 4 | |||
| 5 | //! Parsing and fingerprinting of registered public keys. | ||
| 6 | //! | ||
| 7 | //! `fabrica key add` validates key material before storing it and computes the | ||
| 8 | //! fingerprint the schema keys on. This shares the same `ssh-key` / `pgp` crates as | ||
| 9 | //! signature verification (§3.6), so a key that can be registered is a key we can | ||
| 10 | //! actually verify against. | ||
| 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 | /// A parsed, validated public key ready to persist. | ||
| 20 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 21 | pub struct ParsedKey { | ||
| 22 | /// The fingerprint the store keys on: `SHA256:…` for SSH, lowercase hex for GPG. | ||
| 23 | pub fingerprint: String, | ||
| 24 | /// The canonical text to store: a single-line `authorized_keys` entry (SSH) or | ||
| 25 | /// the ASCII-armored block as given (GPG). | ||
| 26 | pub public_key: String, | ||
| 27 | /// A default label derived from the key (the SSH comment, or the GPG user id), | ||
| 28 | /// used when the operator does not supply one. | ||
| 29 | pub label: Option<String>, | ||
| 30 | } | ||
| 31 | |||
| 32 | /// Parse and fingerprint `material` as a key of the declared `kind`. | ||
| 33 | /// | ||
| 34 | /// # Errors | ||
| 35 | /// | ||
| 36 | /// Returns [`GitError::BadPublicKey`] if the material does not parse as the declared | ||
| 37 | /// kind. | ||
| 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 | /// Parse an OpenSSH `authorized_keys` line. | ||
| 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 | /// Parse an ASCII-armored `OpenPGP` public key block. | ||
| 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 | // The operator supplies the label for GPG keys. | ||
| 73 | label: None, | ||
| 74 | }) | ||
| 75 | } | ||
| 76 | |||
| 77 | #[cfg(test)] | ||
| 78 | mod tests { | ||
| 79 | #![allow(clippy::unwrap_used)] | ||
| 80 | |||
| 81 | use super::*; | ||
| 82 | |||
| 83 | // Reuse the static GPG public key fixture from the signature tests. | ||
| 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 | } | ||