icepick/crates/miniquorum/src/lib.rs

489 lines
16 KiB
Rust
Raw Normal View History

2025-01-30 16:07:32 +00:00
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,
};
2025-01-30 19:51:53 +00:00
use chrono::prelude::*;
2025-01-30 16:07:32 +00:00
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<Fingerprint>),
/// 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,
2025-01-30 19:51:53 +00:00
/// The JSON object is not a valid value.
#[error("the JSON object is not a valid value")]
InvalidJSONValue,
2025-01-30 16:07:32 +00:00
}
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<String, Value> =
map.into_iter().map(|(k, v)| (k, canonicalize(v))).collect();
let sorted: Vec<Value> = map
.into_iter()
.map(|(k, v)| Value::Array(vec![Value::String(k), v]))
.collect();
Value::Array(sorted)
}
value => value,
}
}
fn unhashed(value: Value) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
2025-01-30 19:51:53 +00:00
let Value::Object(mut value) = value else {
return Err(BaseError::InvalidJSONValue.into());
};
value.remove("signatures");
let value = canonicalize(Value::Object(value));
2025-01-30 16:07:32 +00:00
let bincoded = bincode::serialize(&value)?;
Ok(bincoded)
}
fn hash(value: Value) -> Result<Box<dyn Digest>, Box<dyn std::error::Error>> {
2025-01-30 19:51:53 +00:00
let bincoded = unhashed(value)?;
2025-01-30 16:07:32 +00:00
let mut digest = openpgp::types::HashAlgorithm::SHA512.context()?;
digest.update(&bincoded);
Ok(digest)
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Payload {
2025-01-30 19:51:53 +00:00
workflow: [String; 2],
2025-01-30 16:07:32 +00:00
values: Value,
2025-01-30 19:51:53 +00:00
datetime: DateTime<Utc>,
#[serde(default)]
2025-01-30 16:07:32 +00:00
signatures: Vec<String>,
}
#[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<str>) -> String {
let mut n = input
.as_ref()
.split("<<")
.take(2)
.map(|s| s.replace('<', " "))
.collect::<Vec<_>>();
n.reverse();
n.join(" ")
}
impl Payload {
2025-01-30 19:51:53 +00:00
/// Create a new Payload, using the current system's time, in UTC.
pub fn new(values: serde_json::Value, module_name: impl AsRef<str>, workflow_name: impl AsRef<str>) -> Self {
Self {
workflow: [module_name.as_ref().to_string(), workflow_name.as_ref().to_string()],
values,
datetime: Utc::now(),
signatures: vec![],
}
}
2025-01-30 16:07:32 +00:00
/// 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<Path>,
keyring_path: impl AsRef<Path>,
) -> Result<(Self, Vec<Cert>), Box<dyn std::error::Error>> {
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<Cert>), Box<dyn std::error::Error>> {
let payload: Payload = serde_json::from_reader(payload)?;
let certs =
openpgp::cert::CertParser::from_reader(keyring)?.collect::<Result<Vec<_>, _>>()?;
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<dyn std::error::Error>> {
2025-01-30 19:51:53 +00:00
let unhashed = unhashed(serde_json::to_value(&self)?)?;
2025-01-30 16:07:32 +00:00
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::<Open>::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<Fingerprint>,
) -> Result<&serde_json::Value, Box<dyn std::error::Error>> {
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(),
))?;
}
2025-01-30 19:51:53 +00:00
let hashed = hash(serde_json::to_value(self)?)?;
2025-01-30 16:07:32 +00:00
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<Fingerprint>,
certs: &[Cert],
policy: &sequoia_openpgp::policy::StandardPolicy<'_>,
) -> Result<Cert, Box<dyn std::error::Error>> {
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::<Open>::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<Cert>, Vec<Cert>), Box<dyn std::error::Error>> {
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))
}