gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **An account's own IdP CA** (owner 2026-10-06): an account admin pastes
//! the CA (PEM; or a chain up to one) their company IdP's https certificate is
//! issued under, and gunnar trusts it for THAT account's IdP calls alone
//! ([`super::oidc::idp`]: discovery, keys, token), beside the public roots.
//! Judged here when it is set; kept as DER in the identity table
//! (`gunnar_core::sso::OidcSettings::idp_ca`).

/// The most an IdP CA may be, as pasted (PEM).
pub const IDP_CA_MAX_PEM_BYTES: usize = 64 * 1024;
/// The most certificates an IdP CA (a chain) may hold.
pub const IDP_CA_MAX_CERTIFICATES: usize = 10;

/// **An account admin's IdP CA, judged** (owner 2026-10-06: a company IdP
/// behind its own CA): PEM, at most [`IDP_CA_MAX_PEM_BYTES`] and
/// [`IDP_CA_MAX_CERTIFICATES`]; every block a `CERTIFICATE` (a private key
/// pasted by mistake is refused by name, never kept); every certificate
/// parses, is a CA (`basicConstraints cA`) and is valid at `now`. Answers the
/// certificates, DER, in the order pasted.
pub fn parse_idp_ca(pem: &str, now: i64) -> Result<Vec<Vec<u8>>, String> {
    if pem.len() > IDP_CA_MAX_PEM_BYTES {
        return Err(format!(
            "the IdP CA is {} bytes; at most 64 KiB is taken (paste the CA, or its chain, as PEM)",
            pem.len()
        ));
    }
    if !pem.contains("-----BEGIN ") {
        return Err("the IdP CA holds no certificate: paste it as PEM (-----BEGIN CERTIFICATE-----)".to_owned());
    }
    let mut ders = Vec::new();
    for block in x509_parser::pem::Pem::iter_from_buffer(pem.as_bytes()) {
        let block = block.map_err(|err| format!("the IdP CA is not PEM: {err}"))?;
        if block.label.contains("PRIVATE KEY") {
            return Err(
                "a private key was pasted with the IdP CA; paste only the CA's certificate — the key was not kept"
                    .to_owned(),
            );
        }
        if block.label != "CERTIFICATE" {
            return Err(format!(
                "the IdP CA holds a {:?} block; only CERTIFICATE blocks are taken",
                block.label
            ));
        }
        ders.push(block.contents);
        if ders.len() > IDP_CA_MAX_CERTIFICATES {
            return Err(format!(
                "the IdP CA holds more than {IDP_CA_MAX_CERTIFICATES} certificates; at most 10 are taken"
            ));
        }
    }
    if ders.is_empty() {
        return Err("the IdP CA holds no certificate: paste it as PEM (-----BEGIN CERTIFICATE-----)".to_owned());
    }
    for der in &ders {
        let (_rest, cert) = x509_parser::parse_x509_certificate(der)
            .map_err(|err| format!("a certificate in the IdP CA is not an X.509 certificate: {err}"))?;
        let subject = cert.subject().to_string();
        let is_ca = cert.basic_constraints().ok().flatten().is_some_and(|bc| bc.value.ca);
        if !is_ca {
            return Err(format!(
                "the certificate {subject:?} is not a CA (its basicConstraints say cA false or nothing): paste the CA that \
                 issued the IdP's certificate, not the IdP's own"
            ));
        }
        let validity = cert.validity();
        let (from, until) = (validity.not_before.timestamp() * 1000, validity.not_after.timestamp() * 1000);
        if until < now {
            return Err(format!("the CA {subject:?} expired ({})", validity.not_after));
        }
        if from > now + 120_000 {
            return Err(format!("the CA {subject:?} is not valid yet ({})", validity.not_before));
        }
    }
    Ok(ders)
}

/// The certificates of an IdP CA as PEM text (what [`parse_idp_ca`] reads).
pub fn idp_ca_pem(ders: &[Vec<u8>]) -> String {
    use base64ct::{Base64, Encoding as _};
    let mut out = String::new();
    for der in ders {
        out.push_str("-----BEGIN CERTIFICATE-----\n");
        let text = Base64::encode_string(der);
        for line in text.as_bytes().chunks(64) {
            out.push_str(&String::from_utf8_lossy(line));
            out.push('\n');
        }
        out.push_str("-----END CERTIFICATE-----\n");
    }
    out
}

/// The CERTIFICATE blocks of a PEM text, DER, judged by nothing — a stored CA
/// read back (it was judged by [`parse_idp_ca`] when it was set).
pub fn idp_ca_blocks(pem: &str) -> Vec<Vec<u8>> {
    x509_parser::pem::Pem::iter_from_buffer(pem.as_bytes())
        .filter_map(Result::ok)
        .filter(|b| b.label == "CERTIFICATE")
        .map(|b| b.contents)
        .collect()
}


#[cfg(test)]
mod tests {
    use super::*;

    /// A CA, or a leaf under it, minted here (rcgen): `(pem, der)`.
    fn minted(ca: bool, not_after_year: i32) -> (String, Vec<u8>) {
        let key = rcgen::KeyPair::generate().expect("a key");
        let mut params = rcgen::CertificateParams::new(vec!["idp.acme.example".to_owned()]).expect("params");
        params.not_before = rcgen::date_time_ymd(2020, 1, 1);
        params.not_after = rcgen::date_time_ymd(not_after_year, 1, 1);
        if ca {
            params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
            params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign, rcgen::KeyUsagePurpose::CrlSign];
        }
        let cert = params.self_signed(&key).expect("self-signed");
        (cert.pem(), cert.der().to_vec())
    }

    const NOW: i64 = 1_791_250_000_000; // 2026-10-06

    /// ★ **An IdP CA is a CA, as PEM, within bounds, valid now** — and comes
    /// back as the DER it was pasted as; a chain of two is two.
    #[test]
    fn an_idp_ca_is_read_as_the_ca_certificates_it_holds() {
        let (pem, der) = minted(true, 2036);
        assert_eq!(parse_idp_ca(&pem, NOW).expect("a CA"), vec![der.clone()]);
        let (pem2, der2) = minted(true, 2036);
        let chain = format!("{pem}\n{pem2}");
        assert_eq!(parse_idp_ca(&chain, NOW).expect("a chain"), vec![der.clone(), der2]);
        // What is stored reads back as itself.
        assert_eq!(
            parse_idp_ca(&idp_ca_pem(std::slice::from_ref(&der)), NOW).expect("round trip"),
            vec![der]
        );
    }

    /// ★ **Everything else is refused, by name**: no PEM, a leaf (not a CA),
    /// an expired CA, a private key, too large, too many.
    #[test]
    fn what_is_not_a_ca_is_refused_by_name() {
        let refused = |pem: &str| parse_idp_ca(pem, NOW).expect_err("refused");
        assert!(refused("").contains("no certificate"), "{}", refused(""));
        assert!(refused("hello").contains("no certificate"));
        let (leaf, _) = minted(false, 2036);
        assert!(refused(&leaf).contains("not a CA"), "{}", refused(&leaf));
        let (old, _) = minted(true, 2025);
        assert!(refused(&old).contains("expired"), "{}", refused(&old));
        let key = rcgen::KeyPair::generate().expect("a key").serialize_pem();
        let (ca, _) = minted(true, 2036);
        let with_key = format!("{ca}\n{key}");
        assert!(refused(&with_key).contains("private key"), "{}", refused(&with_key));
        assert!(!refused(&with_key).contains("PRIVATE KEY-----\n"), "the key is never repeated back");
        let big = format!("{ca}\n{}", " ".repeat(IDP_CA_MAX_PEM_BYTES));
        assert!(refused(&big).contains("64 KiB"), "{}", refused(&big));
        let many = std::iter::repeat_n(ca.as_str(), IDP_CA_MAX_CERTIFICATES + 1).collect::<Vec<_>>().join("\n");
        assert!(refused(&many).contains("at most 10"), "{}", refused(&many));
    }

}