use keyfork_prompt::{ default_handler, prompt_validated_passphrase, validators::{PinValidator, Validator}, }; use openpgp_card::{Error as CardError, StatusBytes}; use openpgp_card_sequoia::{state::Open, Card}; use sequoia_openpgp::{ self as openpgp, armor::{Kind, Writer}, crypto::hash::Digest, packet::{signature::SignatureBuilder, Packet}, parse::Parse, serialize::Serialize as _, types::SignatureType, Cert, Fingerprint, }; use chrono::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::{collections::BTreeMap, fs::File, io::Read, path::Path}; #[derive(thiserror::Error, Debug)] /// An error with a [`PayloadVerification`] policy. #[error("{error} (policy: {policy:?})")] pub struct Error { error: BaseError, policy: PayloadVerification, } #[derive(thiserror::Error, Debug)] pub enum BaseError { /// In the given certificate keyring, the provided fingerprint was not found. #[error("fingerprint not found: {0}")] FingerprintNotFound(Fingerprint), /// No smartcard was found. #[error("no smartcard found")] NoSmartcard, /// None of the certificates in the given certificate keyring matched any plugged-in smartcard. #[error("no certs found matching any available smartcard")] NoCertMatchedSmartcard, /// The certificate was not trusted by the root of trust. #[error("untrusted certificate: {0} has not signed {1:?}")] UntrustedCertificates(Fingerprint, Vec), /// No certificate in the given certificate keyring matched the signature. #[error("no public key matched signature")] NoPublicKeyMatchedSignature, /// Not enough signatures matched based on the given threshold #[error("not enough signatures: {0} < {1}")] NotEnoughSignatures(u8, u8), /// A Payload was provided when an inner [`serde_json::Value`] was expected. #[error("a payload was provided when a non-payload JSON value was expected")] UnexpectedPayloadProvided, /// The JSON object is not a valid value. #[error("the JSON object is not a valid value")] InvalidJSONValue, } impl BaseError { fn with_policy(self, policy: &PayloadVerification) -> Error { Error { error: self, policy: policy.clone(), } } } fn canonicalize(value: Value) -> Value { match value { Value::Array(vec) => { let values = vec.into_iter().map(canonicalize).collect(); Value::Array(values) } Value::Object(map) => { // this sorts the values let map: BTreeMap = map.into_iter().map(|(k, v)| (k, canonicalize(v))).collect(); let sorted: Vec = map .into_iter() .map(|(k, v)| Value::Array(vec![Value::String(k), v])) .collect(); Value::Array(sorted) } value => value, } } fn unhashed(value: Value) -> Result, Box> { let Value::Object(mut value) = value else { return Err(BaseError::InvalidJSONValue.into()); }; value.remove("signatures"); let value = canonicalize(Value::Object(value)); let bincoded = bincode::serialize(&value)?; Ok(bincoded) } fn hash(value: Value) -> Result, Box> { let bincoded = unhashed(value)?; let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?; digest.update(&bincoded); Ok(digest) } #[derive(Serialize, Deserialize, Debug)] pub struct Payload { workflow: [String; 2], values: Value, datetime: DateTime, #[serde(default)] signatures: Vec, } #[derive(Clone, Debug)] pub struct PayloadVerification { threshold: u8, error_on_invalid: bool, error_on_missing_key: bool, one_each: bool, } impl std::default::Default for PayloadVerification { fn default() -> Self { Self { threshold: 0, error_on_invalid: true, error_on_missing_key: true, one_each: true, } } } #[allow(dead_code)] impl PayloadVerification { pub fn new() -> Self { Default::default() } pub fn with_one_per_key(self, one_each: bool) -> Self { Self { one_each, ..self } } pub fn with_threshold(self, threshold: u8) -> Self { Self { threshold, ..self } } pub fn with_any_valid(self) -> Self { Self { threshold: 1, error_on_invalid: false, ..self } } pub fn with_all_valid(self, threshold: u8) -> Self { Self { threshold, error_on_invalid: true, ..self } } pub fn ignoring_invalid_signatures(self) -> Self { Self { error_on_invalid: false, ..self } } pub fn ignoring_missing_keys(self) -> Self { Self { error_on_missing_key: true, ..self } } } /// Format a name from an OpenPGP card. fn format_name(input: impl AsRef) -> String { let mut n = input .as_ref() .split("<<") .take(2) .map(|s| s.replace('<', " ")) .collect::>(); n.reverse(); n.join(" ") } impl Payload { /// Create a new Payload, using the current system's time, in UTC. pub fn new(values: serde_json::Value, module_name: impl AsRef, workflow_name: impl AsRef) -> Self { Self { workflow: [module_name.as_ref().to_string(), workflow_name.as_ref().to_string()], values, datetime: Utc::now(), signatures: vec![], } } /// Load a Payload and the relevant certificates. /// /// # Errors /// /// The constructor may error if either file can't be read or if either file has invalid data. pub fn load( payload_path: impl AsRef, keyring_path: impl AsRef, ) -> Result<(Self, Vec), Box> { let payload_file = File::open(payload_path)?; let cert_file = File::open(keyring_path)?; Self::from_readers(payload_file, cert_file) } pub fn from_readers( payload: impl Read, keyring: impl Read + Send + Sync, ) -> Result<(Self, Vec), Box> { let payload: Payload = serde_json::from_reader(payload)?; let certs = openpgp::cert::CertParser::from_reader(keyring)?.collect::, _>>()?; Ok((payload, certs)) } /// Attach a signature from an OpenPGP card. /// /// # Errors /// /// The method may error if a signature could not be created. pub fn add_signature(&mut self) -> Result<(), Box> { let unhashed = unhashed(serde_json::to_value(&self)?)?; let builder = SignatureBuilder::new(SignatureType::Binary); let mut prompt_handler = default_handler()?; let pin_validator = PinValidator { min_length: Some(6), ..Default::default() }; for backend in card_backend_pcsc::PcscBackend::cards(None)? { let mut card = Card::::new(backend?)?; let mut transaction = card.transaction()?; let cardholder_name = format_name(transaction.cardholder_name()?); let card_id = transaction.application_identifier()?.ident(); let mut pin = None; while transaction.pw_status_bytes()?.err_count_pw1() > 0 && pin.is_none() { transaction.reload_ard()?; let attempts = transaction.pw_status_bytes()?.err_count_pw1(); let rpea = "Remaining PIN entry attempts"; let message = if cardholder_name.is_empty() { format!("Unlock card {card_id}\n{rpea}: {attempts}\n\nPIN: ") } else { format!( "Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: " ) }; let temp_pin = prompt_validated_passphrase( &mut *prompt_handler, &message, 3, pin_validator.to_fn(), )?; let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim()); match verification_status { #[allow(clippy::ignored_unit_patterns)] Ok(_) => { pin.replace(temp_pin); } // NOTE: This should not be hit, because of the above validator. Err(CardError::CardStatus( StatusBytes::IncorrectParametersCommandDataField, )) => { prompt_handler.prompt_message(keyfork_prompt::Message::Text( "Invalid PIN length entered.".to_string(), ))?; } Err(_) => {} } } let mut signer_card = transaction.to_signing_card(pin.expect("valid PIN").as_str())?; // NOTE: Can't use a PromptHandler to prompt a message as it doesn't provide a way to // cancel a prompt when in terminal mode. Just eprintln to stderr. // // We shouldn't be writing with a PromptHandler, so the terminal should be reset. let mut signer = signer_card.signer(&|| eprintln!("Touch confirmation needed for signing"))?; let signature = builder.clone().sign_message(&mut signer, &unhashed)?; let signature = Packet::from(signature); let mut armored_signature = vec![]; let mut writer = Writer::new(&mut armored_signature, Kind::Signature)?; signature.serialize(&mut writer)?; writer.finalize()?; self.signatures.push(String::from_utf8(armored_signature)?); } Ok(()) } /// Verify the keychain and certificates using either a Key ID or an OpenPGP card. /// /// # Errors /// /// The method may error if no certificate could be verified or if any singatures are invalid. pub fn verify_signatures( &self, certs: &[Cert], verification_policy: &PayloadVerification, fingerprint: Option, ) -> Result<&serde_json::Value, Box> { let policy = openpgp::policy::StandardPolicy::new(); let validated_cert = find_matching_certificate(fingerprint, certs, &policy)?; let (certs, invalid_certs) = validate_cross_signed_certs(&validated_cert, certs, &policy)?; if !invalid_certs.is_empty() { return Err(BaseError::UntrustedCertificates( validated_cert.fingerprint(), invalid_certs.iter().map(Cert::fingerprint).collect(), ))?; } let hashed = hash(serde_json::to_value(self)?)?; let PayloadVerification { mut threshold, error_on_invalid, error_on_missing_key, one_each, } = *verification_policy; let mut matches = 0; if one_each { threshold = certs.len() as u8; } for signature in &self.signatures { let packet = Packet::from_bytes(signature.as_bytes())?; let Packet::Signature(signature) = packet else { panic!("bad packet found: {}", packet.tag()); }; let mut signature_matched = false; for issuer in signature.get_issuers() { for cert in &certs { match cert .with_policy(&policy, None)? .keys() .alive() .for_signing() .key_handle(issuer.clone()) .next() .map(|signing_key| signature.verify_hash(&signing_key, hashed.clone())) { Some(Ok(())) => { // key found, signature matched signature_matched = true; } Some(Err(e)) => { if error_on_invalid { return Err(e)?; } } None => { // key not found, but we have more certs to go through } } } } if signature_matched { matches += 1; } else if error_on_missing_key { return Err( BaseError::NoPublicKeyMatchedSignature.with_policy(verification_policy) )?; } } if matches < threshold { return Err( BaseError::NotEnoughSignatures(matches, threshold).with_policy(verification_policy) )?; } Ok(&self.values) } } fn find_matching_certificate( fingerprint: Option, certs: &[Cert], policy: &sequoia_openpgp::policy::StandardPolicy<'_>, ) -> Result> { if let Some(fingerprint) = fingerprint { Ok(certs .iter() .find(|cert| cert.fingerprint() == fingerprint) .ok_or(BaseError::FingerprintNotFound(fingerprint))? .clone()) } else { let mut any_smartcard = false; for backend in card_backend_pcsc::PcscBackend::cards(None)? { any_smartcard = true; let mut card = Card::::new(backend?)?; let mut transaction = card.transaction()?; let signing_fingerprint = transaction .fingerprint(openpgp_card::KeyType::Signing)? .expect("smartcard signing key is unavailable"); for cert in certs { let valid_cert = cert.with_policy(policy, None)?; for key in valid_cert.keys().alive().for_signing() { let fpr = key.fingerprint(); if fpr.as_bytes() == signing_fingerprint.as_bytes() { return Ok(cert.clone()); } } } } if any_smartcard { Err(BaseError::NoCertMatchedSmartcard.into()) } else { Err(BaseError::NoSmartcard.into()) } } } /// Validate that `certs` are signed by `validated_cert`, either by a signature directly upon the /// primary key of that certificate, or a signature on a user ID of the certificate. /// /// Returns a list of trusted certs and a list of untrusted certs. fn validate_cross_signed_certs( validated_cert: &Cert, certs: &[Cert], policy: &sequoia_openpgp::policy::StandardPolicy, ) -> Result<(Vec, Vec), Box> { let our_pkey = validated_cert.primary_key(); let mut verified_certs = vec![validated_cert.clone()]; let mut unverified_certs = vec![]; for cert in certs .iter() .filter(|cert| cert.fingerprint() != validated_cert.fingerprint()) { let mut has_valid_userid_signature = false; let cert_pkey = cert.primary_key(); // check signatures on User IDs let userids = cert .userids() .map(|ua| (ua.certifications(), ua.userid().clone())); for (signatures, userid) in userids { for signature in signatures { if signature .verify_userid_binding(&our_pkey, &*cert_pkey, &userid) .is_ok() { has_valid_userid_signature = true; } } } // check signatures on the primary key itself let has_valid_direct_signature = cert_pkey .active_certifications_by_key(policy, None, &***our_pkey.role_as_unspecified()) .next() .is_some(); if has_valid_userid_signature || has_valid_direct_signature { verified_certs.push(cert.clone()); } else { unverified_certs.push(cert.clone()); } } Ok((verified_certs, unverified_certs)) }