revenant-sign-core 3.0.5

Cross-platform client library for ARX CoSign / DocuSign Signature Appliance electronic signatures via the OASIS DSS SOAP API
Documentation
//! Chain-construction and verification steps.
//!
//! The mechanics behind the [`super`] validation pipeline: building the chain
//! by SKI/AKI matching, matching a trust anchor, and verifying each link
//! cryptographically with `x509-verify`.
//!
//! Nothing here touches the network. Issuers come from the pool the caller
//! assembled -- the CMS blob and the trust store -- never from the Authority
//! Information Access URLs printed inside a certificate, which the document
//! being verified would otherwise get to choose.

use x509_cert::Certificate;
use x509_verify::VerifyingKey;

use super::super::cert;
use super::super::tsl::TrustStore;

/// Hard cap on chain length, guarding against a cycle in issuer references.
const MAX_CHAIN_DEPTH: usize = 20;

/// Build a chain from `leaf` upward via SKI/AKI matching. Returns the ordered
/// chain from leaf to the highest issuer present in `pool`.
///
/// The leaf is a parameter rather than a position in `pool` on purpose: a CMS
/// certificate set is unordered, so any rule of the form "the signer is the
/// first one" is a bug waiting for a blob that lists them the other way round.
pub(super) fn build_chain(leaf: &Certificate, pool: &[Certificate]) -> Vec<Certificate> {
    let mut chain = vec![leaf.clone()];
    let mut current = leaf.clone();

    for _ in 0..MAX_CHAIN_DEPTH {
        if cert::is_self_signed(&current) {
            break;
        }
        let Some(aki) = cert::authority_key_id(&current) else {
            break;
        };
        let Some(issuer) = issuer_by_ski(pool, &aki, &current) else {
            break;
        };
        chain.push(issuer.clone());
        current = issuer;
    }

    chain
}

/// A certificate in the pool whose SKI equals `key_id` and which is not `current`
/// itself, so a certificate cannot be presented as its own issuer.
fn issuer_by_ski(
    pool: &[Certificate],
    key_id: &[u8],
    current: &Certificate,
) -> Option<Certificate> {
    pool.iter()
        .find(|candidate| {
            cert::subject_key_identifier(candidate).as_deref() == Some(key_id)
                && *candidate != current
        })
        .cloned()
}

/// Find which trust anchor the chain terminates at: first by SKI equality with
/// any chain cert, then by issuer-DN substring.
pub(super) fn find_matching_anchor(
    chain: &[Certificate],
    trust_store: &TrustStore,
) -> Option<String> {
    if chain.is_empty() {
        return None;
    }

    for cert in chain {
        let Some(cert_ski) = cert::subject_key_identifier(cert) else {
            continue;
        };
        for anchor in &trust_store.ca_anchors {
            if let Ok(anchor_cert) = cert::parse_der(&anchor.cert_der) {
                if cert::subject_key_identifier(&anchor_cert).as_deref() == Some(&cert_ski) {
                    return Some(anchor.service_name.clone());
                }
            }
        }
    }

    for cert in chain {
        let issuer_dn = cert::issuer_dn(cert);
        for anchor in &trust_store.ca_anchors {
            if !anchor.subject_name.is_empty() && issuer_dn.contains(&anchor.subject_name) {
                return Some(anchor.service_name.clone());
            }
        }
    }

    None
}

/// Cryptographically verify a built chain: every adjacent link's signature, the
/// validity window of each certificate, and that the top links to a trust
/// anchor. `Err` carries a human-readable reason and downgrades the result.
pub(super) fn verify_chain_crypto(
    chain: &[Certificate],
    anchors: &[Certificate],
) -> Result<(), String> {
    let Some(top) = chain.last() else {
        return Err("empty chain".to_owned());
    };

    for cert in chain {
        if !cert::is_currently_valid(cert) {
            return Err(format!(
                "certificate outside its validity period: {}",
                cert::subject_dn(cert)
            ));
        }
    }

    // Every certificate above the leaf issues the one below it, so each must be a
    // CA authorized to sign certificates (RFC 5280), and its pathLenConstraint
    // must permit the number of intermediates beneath it. This stops a
    // wrongly issued end-entity certificate from being accepted as an intermediate.
    for (index, issuer) in chain.iter().enumerate().skip(1) {
        if !cert::is_ca_cert(issuer) {
            return Err(format!(
                "chain certificate is not a valid CA: {}",
                cert::subject_dn(issuer)
            ));
        }
        if let Some(max_intermediates) = cert::ca_path_len(issuer) {
            // Certs strictly between this issuer and the leaf (indices 1..index).
            let intermediates_below = index - 1;
            if intermediates_below > usize::from(max_intermediates) {
                return Err(format!(
                    "pathLenConstraint violated at {}: {intermediates_below} intermediate(s) below a limit of {max_intermediates}",
                    cert::subject_dn(issuer)
                ));
            }
        }
    }

    for pair in chain.windows(2) {
        // pair = [subject, issuer]: the issuer's key must verify the subject.
        verify_signed_by(&pair[1], &pair[0])?;
    }

    for anchor in anchors {
        if certs_equal(anchor, top) || verify_signed_by(anchor, top).is_ok() {
            return Ok(());
        }
    }
    Err("chain does not terminate at a trusted anchor".to_owned())
}

/// Whether `issuer`'s public key verifies `subject`'s signature.
fn verify_signed_by(issuer: &Certificate, subject: &Certificate) -> Result<(), String> {
    let key = VerifyingKey::try_from(issuer).map_err(|e| format!("unusable issuer key: {e}"))?;
    key.verify(subject)
        .map_err(|e| format!("signature verification failed: {e}"))
}

fn certs_equal(a: &Certificate, b: &Certificate) -> bool {
    use der::Encode;
    matches!((a.to_der(), b.to_der()), (Ok(x), Ok(y)) if x == y)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pki::tsl::TrustAnchor;
    use std::time::Instant;

    const ROOT_DER: &[u8] = include_bytes!("../testdata/root.der");
    const INTER_DER: &[u8] = include_bytes!("../testdata/intermediate.der");
    const LEAF_DER: &[u8] = include_bytes!("../testdata/leaf.der");
    const LEAF_DIRECT_DER: &[u8] = include_bytes!("../testdata/leaf_direct.der");
    const LEAF_AIA_DER: &[u8] = include_bytes!("../testdata/leaf_aia.der");
    const ROOT2_DER: &[u8] = include_bytes!("../testdata/root2.der");
    const NO_AKI_DER: &[u8] = include_bytes!("../testdata/no_aki.der");

    fn cert(der: &[u8]) -> Certificate {
        cert::parse_der(der).unwrap()
    }

    fn trust_store_with(anchor_der: &[u8], subject_name: &str, service_name: &str) -> TrustStore {
        let anchor = TrustAnchor {
            subject_name: subject_name.to_owned(),
            service_name: service_name.to_owned(),
            service_type: "CA/QC".to_owned(),
            status: "granted".to_owned(),
            cert_der: anchor_der.to_vec(),
        };
        TrustStore {
            anchors: vec![anchor.clone()],
            ca_anchors: vec![anchor],
            scheme_operator: "Test Operator".to_owned(),
            tsl_url: "https://example.com".to_owned(),
            fetched_at: Instant::now(),
        }
    }

    #[test]
    fn builds_full_chain_from_pool() {
        let pool = [cert(LEAF_DER), cert(INTER_DER), cert(ROOT_DER)];
        assert_eq!(build_chain(&cert(LEAF_DER), &pool).len(), 3);
    }

    #[test]
    fn self_signed_only_is_depth_one() {
        assert_eq!(build_chain(&cert(ROOT_DER), &[cert(ROOT_DER)]).len(), 1);
    }

    #[test]
    fn no_aki_stops_at_depth_one() {
        assert_eq!(build_chain(&cert(NO_AKI_DER), &[cert(NO_AKI_DER)]).len(), 1);
    }

    #[test]
    fn a_missing_issuer_stops_the_chain_rather_than_being_fetched() {
        // This leaf names its issuer only through an AIA URL, and the
        // intermediate is absent from the pool. `build_chain` takes no fetcher,
        // so the URL cannot be dialed: the chain stops at the leaf.
        let pool = [cert(LEAF_AIA_DER), cert(ROOT_DER)];
        assert_eq!(build_chain(&cert(LEAF_AIA_DER), &pool).len(), 1);
    }

    #[test]
    fn matches_anchor_by_ski() {
        let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
        assert_eq!(
            find_matching_anchor(&[cert(ROOT_DER)], &store).as_deref(),
            Some("TestRootCA")
        );
    }

    #[test]
    fn matches_anchor_by_issuer_dn_substring() {
        // The anchor cert is the root (SKI won't match the leaf), but its
        // subject_name is a substring of the leaf's issuer DN -- exercising the
        // second matching stage (issuer-DN substring), which is intentional.
        let store = trust_store_with(ROOT_DER, "CN=Test Intermediate", "InterAnchor");
        assert_eq!(
            find_matching_anchor(&[cert(LEAF_DER)], &store).as_deref(),
            Some("InterAnchor")
        );
    }

    #[test]
    fn no_matching_anchor_returns_none() {
        let store = trust_store_with(ROOT_DER, "CN=Nonexistent CA", "X");
        assert_eq!(find_matching_anchor(&[cert(LEAF_DER)], &store), None);
    }

    #[test]
    fn empty_chain_matches_nothing() {
        let store = trust_store_with(ROOT_DER, "CN=Test Root CA", "TestRootCA");
        assert_eq!(find_matching_anchor(&[], &store), None);
    }

    #[test]
    fn crypto_verifies_direct_leaf() {
        assert!(verify_chain_crypto(&[cert(LEAF_DIRECT_DER)], &[cert(ROOT_DER)]).is_ok());
    }

    #[test]
    fn crypto_verifies_three_level_chain() {
        let chain = [cert(LEAF_DER), cert(INTER_DER)];
        assert!(verify_chain_crypto(&chain, &[cert(ROOT_DER)]).is_ok());
    }

    #[test]
    fn crypto_rejects_wrong_anchor() {
        assert!(verify_chain_crypto(&[cert(LEAF_DIRECT_DER)], &[cert(ROOT2_DER)]).is_err());
    }

    #[test]
    fn ca_role_is_enforced() {
        // Real CAs qualify; an end-entity leaf does not.
        assert!(cert::is_ca_cert(&cert(ROOT_DER)));
        assert!(cert::is_ca_cert(&cert(INTER_DER)));
        assert!(!cert::is_ca_cert(&cert(LEAF_DER)));
        // A chain whose "issuer" is actually a non-CA leaf is rejected even
        // before signature math (here the leaf cannot issue itself).
        let err = verify_chain_crypto(&[cert(LEAF_DER), cert(LEAF_DER)], &[cert(ROOT_DER)]);
        assert!(err.is_err());
    }
}