Add craft-pkcs7: DER, X.509, CMS SignedData, PKCS #12, RFC 3161 and revocation from PdfCraft

* Add craft-pkcs7: DER, X.509, CMS SignedData, PKCS #12, RFC 3161 and revocation from PdfCraft

Moved from PdfCraft's crates/sign at 1d7a9f9f: der, x509, cms, pkcs12, timestamp,
revocation, keys, rsa_pad and ec512, with their unit tests, the integration tests
that need no PDF, and the test keys.

x509 no longer returns PdfCraft's worded sentences: signing_problem gives a
SigningProblem and build_chain_noted a Refused (reason and the two certificates).
The checks behind them are unchanged. Error is SignError without its PDF variants.

* craft-pkcs7: write the certificate serial verbatim in SignerInfo and ESSCertIDv2

A serial with its high bit set and no leading zero was written with one added, so
the signer identifier and the signing-certificate attribute named a different
number than the certificate has.

Ported from storytold/pdfcraft#6868. The test uses a certificate that really has
such a serial and also checks that the signer's certificate is found.

* craft-pkcs7: note why RIPEMD-160 is supported

---------

Co-authored-by: jonathanwxh-cell <jonathanwxh@gmail.com>
This commit is contained in:
Dylan M. Taylorandjonathanwxh-cell authored and GitHub committed 2026-11-12 09:45:31 -05:00
1 parent 493476eb8d
commit 1c5c8a4e14
33 files changed
+6554 -88

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
[package]
name = "craft-pkcs7"
description = "The formats of digital signatures: ASN.1 DER, X.509 certificates and chains, CMS SignedData (PKCS #7), PKCS #12 identity files, RFC 3161 timestamps, and CRL / OCSP revocation evidence, read, verified and written."
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
[lints]
workspace = true
# Every dependency here is MIT OR Apache-2.0 (RustCrypto, base64, chrono, thiserror), except
# ed25519-dalek (BSD-3-Clause) and aws-lc-rs (ISC AND (Apache-2.0 OR ISC)), all on the allowlist.
[dependencies]
thiserror = { workspace = true }
zeroize = "1"
# ASN.1 UTCTime/GeneralizedTime conversion goes through chrono (no hand-rolled calendar code).
chrono = { workspace = true }
base64 = "0.23"
sha1 = "0.11"
sha2 = "0.11"
sha3 = "0.12.0"
ripemd = "0.2.0"
p256 = { version = "0.14", default-features = false, features = ["ecdsa", "std"] }
p384 = { version = "0.14", default-features = false, features = ["ecdsa", "std"] }
# Verification only: P-521 and the Brainpool curves German qualified-signature cards use.
p521 = { version = "0.14", default-features = false, features = ["ecdsa", "std"] }
bp256 = { version = "0.14", default-features = false, features = ["arithmetic", "ecdsa", "std"] }
bp384 = { version = "0.14", default-features = false, features = ["arithmetic", "ecdsa", "std"] }
ecdsa = { version = "0.17", default-features = false, features = ["algorithm", "der"] }
ed25519-dalek = "3.0.0"
# Verification only on native targets (RSA private-key operations use aws-lc-rs).
crypto-bigint = { version = "0.7.11", default-features = false, features = ["alloc"] }
rsa = { version = "=0.10.0-rc.18", default-features = false, features = ["std", "hazmat"] }
pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"] }
hmac = "0.13"
aes = "0.9"
cbc = { version = "0.2", features = ["alloc", "block-padding"] }
des = "0.9"
rc2 = "0.9"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
aws-lc-rs = { version = "1.18", default-features = false, features = ["aws-lc-sys", "prebuilt-nasm"] }
getrandom = "0.4"
[target.'cfg(target_arch = "wasm32")'.dependencies]
# crypto-bigint asks for randomness; in the browser it comes from Web Crypto.
getrandom = { version = "0.4", features = ["wasm_js"] }
[[test]]
name = "it"
path = "tests/it/main.rs"
+44
View File
@@ -0,0 +1,44 @@
# craft-pkcs7
The formats of digital signatures, read, verified and written: what a signed PDF, a signed mail
message or a digital ID file is made of.
- **`der`**: a small BER/DER reader and writer (ITU-T X.690). A `Tlv` borrows from the input and
keeps its exact encoding, so a signed structure is verified over the bytes that were signed.
Reading is strict DER; `parse_ber` also takes indefinite lengths and segmented strings, which
PKCS #12 files from Windows use. Nesting is bounded.
- **`x509`**: certificates (RFC 5280): names, validity, key, the extensions that matter for
signing (basic constraints, key usage, extended key usage, key identifiers, OCSP and CRL
locations), `build_chain` / `build_chain_noted` (issuers that may issue, path length, validity,
unprocessed critical extensions), `load_certificates` (DER, PEM, PKCS #7 bundles), `to_pem`,
and `Certificate::self_signed`. An extension that is present but unreadable restricts: an
unreadable key usage allows no usage, an unreadable extended key usage no purpose.
- **`cms`**: CMS SignedData (RFC 5652): `SignedData::parse` and `verify_signature`,
`sign_detached` / `sign_encapsulated`, `certificate_bundle`, and `attach_timestamp_token`.
- **`pkcs12`**: `.p12` / `.pfx` (RFC 7292): `open` reads PBES2 (PBKDF2 with HMAC-SHA-1/2;
AES-CBC or 3DES-CBC) and the legacy PBE schemes (SHA-1 with 3DES or RC2) and checks the MAC,
stopping at a fixed amount of key-derivation work; `write` uses PBES2 AES-256-CBC with
PBKDF2-HMAC-SHA-256.
- **`timestamp`**: RFC 3161 requests, responses and tokens, and a responder for a local
stamping identity. No transport: `TimestampAuthority` is the caller's.
- **`revocation`**: CRLs (RFC 5280) and OCSP responses (RFC 6960): parsed, their signatures
verified, and a certificate's status looked up. Nothing is fetched.
- **`keys`**: digests (SHA-1, SHA-2, SHA-3, RIPEMD-160), public keys for verification (RSA
PKCS #1 v1.5 and PSS, ECDSA on P-256, P-384, P-521 and the Brainpool curves, Ed25519) and
private keys for signing (RSA, P-256, P-384, or a key held elsewhere behind `ExternalKey`).
What a signature means for a document or a message stays with the caller: which bytes it covers,
what changed after it, which certificates are trusted, and how a result is worded.
## Cryptography
The primitives are RustCrypto's; RSA private-key operations use aws-lc-rs on native targets.
Two things are done in this crate, both verification only and on public data, because no audited
crate offers them: the RSA signature paddings for any digest, MGF1 hash and salt length
(`rsa_pad`, over the `rsa` crate's raw public-key operation), and ECDSA on brainpoolP512r1
(`ec512`, over `crypto-bigint`). Both are checked against signatures made by OpenSSL.
Weak algorithms are read, not refused, because old signed documents use them:
`DigestAlg::weakness` and `PublicKey::weakness` say when one is weak, and the caller decides.
Moved from PdfCraft's `pdfcraft-sign`.
+595
View File
@@ -0,0 +1,595 @@
//! CMS SignedData (RFC 5652) as PDF signatures use it: detached (`adbe.pkcs7.detached`,
//! `ETSI.CAdES.detached`) or with the digest encapsulated (`adbe.pkcs7.sha1`).
use crate::Error;
use crate::der::{self, Time, Tlv, tag};
use crate::keys::{DigestAlg, PrivateKey, Scheme, signature_algorithm};
use crate::x509::Certificate;
pub const SIGNED_DATA: &str = "1.2.840.113549.1.7.2";
pub const DATA: &str = "1.2.840.113549.1.7.1";
const CONTENT_TYPE: &str = "1.2.840.113549.1.9.3";
const MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4";
const SIGNING_TIME: &str = "1.2.840.113549.1.9.5";
const SIGNING_CERT_V2: &str = "1.2.840.113549.1.9.16.2.47";
const SIGNING_CERT: &str = "1.2.840.113549.1.9.16.2.12";
const TIMESTAMP_TOKEN: &str = "1.2.840.113549.1.9.16.2.14";
/// How a signer names its certificate.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SignerId {
IssuerSerial { issuer: Vec<u8>, serial: Vec<u8> },
KeyId(Vec<u8>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignerInfo {
pub sid: SignerId,
pub digest: DigestAlg,
/// The signed attributes as the SET that was signed (`None`: the signature covers the
/// content itself).
pub signed_attrs: Option<Vec<u8>>,
pub message_digest: Option<Vec<u8>>,
pub content_type: Option<String>,
pub signing_time: Option<Time>,
/// ESS signing-certificate(-v2) present (CAdES).
pub signing_certificate: bool,
pub scheme: Scheme,
/// The digest named by the signature algorithm, when it names one.
pub scheme_digest: Option<DigestAlg>,
pub signature: Vec<u8>,
/// An RFC 3161 timestamp token is attached (unsigned attribute).
pub timestamp: bool,
/// The attached token's raw CMS bytes, when present.
pub timestamp_token: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedData {
pub certificates: Vec<Certificate>,
pub signer: SignerInfo,
/// Encapsulated content (`adbe.pkcs7.sha1` carries the document digest here).
pub content: Option<Vec<u8>>,
/// `eContentType`, the type of the encapsulated content: data for a PDF signature, TSTInfo
/// for an RFC 3161 token. The signature does not cover it.
pub econtent_type: String,
/// Encoding irregularities that were tolerated (BER instead of DER, …), for the details.
pub quirks: Vec<&'static str>,
}
/// The X.509 certificates in a SignedData `certificates` field (`[0] IMPLICIT CertificateSet`).
/// Other certificate formats (attribute certificates) and certificates that don't parse are
/// skipped.
fn carried_certificates(set: &Tlv) -> Result<Vec<Certificate>, Error> {
let mut out = Vec::new();
for c in set.children()? {
if c.tag == tag::SEQUENCE
&& let Ok(cert) = Certificate::parse(c.raw)
{
out.push(cert);
}
}
Ok(out)
}
/// The certificates of a PKCS #7 certificate bundle (`.p7b`, `.p7c`): a SignedData with no
/// signers ("degenerate", RFC 5652 §5.2), which is how Windows, Acrobat and certificate
/// authorities hand out a chain or a set of CA certificates. Signers and CRLs, if present, are
/// ignored; only the certificates are read. DER or BER.
pub fn certificate_bundle(bytes: &[u8]) -> Result<Vec<Certificate>, Error> {
let (ci, rest) = Tlv::parse_ber(bytes)?;
// A trust file is the bundle alone: bytes after it mean a damaged or spliced file.
if !rest.is_empty() {
return Err(bad("data after the bundle"));
}
let ci = ci.expect(tag::SEQUENCE, "ContentInfo")?.children()?;
let [ct, content] = ci.as_slice() else { return Err(bad("ContentInfo")) };
if ct.oid()? != SIGNED_DATA {
return Err(bad("not SignedData"));
}
let sd = content.expect(tag::ctx(0), "content")?.inner()?.children()?;
// version, digestAlgorithms, encapContentInfo, the optional [0] certificates and [1] CRLs,
// then signerInfos (RFC 5652 §5.1).
let [version, digests, encap, middle @ .., signers] = sd.as_slice() else { return Err(bad("SignedData")) };
let shape = [(version, tag::INTEGER), (digests, tag::SET), (encap, tag::SEQUENCE), (signers, tag::SET)];
// At most one [0] certificates and one [1] CRLs, in that order.
const CERTS: u8 = tag::ctx(0);
const CRLS: u8 = tag::ctx(1);
let middle_ok = matches!(middle.iter().map(|t| t.tag).collect::<Vec<_>>().as_slice(), [] | [CERTS] | [CRLS] | [CERTS, CRLS]);
// encapContentInfo starts with its eContentType OID.
let encap_ok = encap.children()?.first().is_some_and(|t| t.tag == tag::OID);
if shape.iter().any(|(t, want)| t.tag != *want) || !middle_ok || !encap_ok {
return Err(bad("SignedData"));
}
let mut certificates = Vec::new();
for t in middle.iter().filter(|t| t.tag == tag::ctx(0)) {
certificates.extend(carried_certificates(t)?);
}
if certificates.is_empty() {
return Err(bad("the bundle holds no certificate"));
}
Ok(certificates)
}
fn bad(what: &str) -> Error {
Error::Malformed(format!("CMS: {what}"))
}
impl SignedData {
/// Parse a `ContentInfo` holding SignedData. Bytes after it (a PDF placeholder's zero
/// padding) are ignored.
pub fn parse(bytes: &[u8]) -> Result<SignedData, Error> {
// CMS from DocuSign, Documenso, Windows and `openssl cms -stream` is BER (indefinite lengths,
// segmented strings). Only its structure is read this way: certificates and signed
// attributes are still verified over their own exact bytes.
let (ci, _) = Tlv::parse_ber(bytes)?;
let mut quirks = Vec::new();
if ci.is_indefinite() {
quirks.push("The signature is BER-encoded (indefinite lengths) rather than DER; it was read leniently.");
}
let ci = ci.expect(tag::SEQUENCE, "ContentInfo")?.children()?;
let [ct, content] = ci.as_slice() else { return Err(bad("ContentInfo")) };
if ct.oid()? != SIGNED_DATA {
return Err(bad("not SignedData"));
}
let sd = content.expect(tag::ctx(0), "content")?.inner()?.children()?;
let mut it = sd.into_iter();
let _version = it.next();
let _digest_algs = it.next();
let encap = it.next().ok_or_else(|| bad("encapContentInfo"))?.children()?;
let econtent_type = encap.first().ok_or_else(|| bad("eContentType"))?.oid()?;
let content = match encap.get(1) {
Some(c) if c.tag == tag::ctx(0) => {
// BER signers may split the content into a constructed OCTET STRING.
let inner = c.inner()?;
if inner.tag != tag::OCTET_STRING {
quirks.push("The signed content is a constructed (segmented) OCTET STRING, as BER allows; it was read anyway.");
}
Some(inner.octets("eContent")?.into_owned())
}
_ => None,
};
let mut certificates = Vec::new();
let mut signer_infos = None;
for t in it {
match t.tag {
t2 if t2 == tag::ctx(0) => certificates.extend(carried_certificates(&t)?),
tag::SET => signer_infos = Some(t),
_ => {}
}
}
let infos = signer_infos.ok_or_else(|| bad("no signerInfos"))?.children()?;
let si = infos.first().ok_or_else(|| bad("no signer"))?;
let signer = parse_signer(si)?;
// RFC 5652 §11.1: the content-type attribute names the type of the encapsulated content.
// The signature covers the attribute but not `eContentType`, so when they differ, one of
// them was written wrong or changed after signing.
if let Some(signed) = &signer.content_type
&& *signed != econtent_type
{
return Err(bad("the signed content type is not the encapsulated content type"));
}
Ok(SignedData { certificates, signer, content, econtent_type, quirks })
}
/// The signer's certificate among those carried.
pub fn signer_certificate(&self) -> Option<&Certificate> {
self.certificates.iter().find(|c| match &self.signer.sid {
SignerId::IssuerSerial { issuer, serial } => &c.issuer.raw == issuer && strip(&c.serial) == strip(serial),
SignerId::KeyId(k) => c.subject_key_id.as_ref() == Some(k),
})
}
/// Check the signature value with `cert`'s key. `content_digest` is the digest of the
/// signed content (the document byte ranges) when there are no signed attributes.
/// `Err(Unsupported)`: the signer's key algorithm can't be checked here.
pub fn verify_signature(&self, cert: &Certificate, content_digest: &[u8]) -> Result<bool, Error> {
self.verify_signature_noting(cert, content_digest, &mut Vec::new())
}
/// [`SignedData::verify_signature`], noting in `notes` each encoding irregularity or
/// disagreement it tolerated. Only meaningful when it returns `Ok(true)`.
pub fn verify_signature_noting(&self, cert: &Certificate, content_digest: &[u8], notes: &mut Vec<&'static str>) -> Result<bool, Error> {
let s = &self.signer;
// EdDSA signs the message itself: with signed attributes, their DER encoding.
if s.scheme == Scheme::Ed25519 {
let Some(attrs) = &s.signed_attrs else {
return Err(Error::Unsupported("Ed25519 signature without signed attributes".into()));
};
return cert.public_key.verify_message(s.scheme, attrs, &s.signature);
}
// The signature algorithm names the digest it used; a bare `rsaEncryption` / `ecPublicKey`
// uses the SignerInfo's digestAlgorithm. Some signers disagree, so try both.
let named = s.scheme_digest.unwrap_or(s.digest);
let mut algs = vec![named];
if !algs.contains(&s.digest) {
algs.push(s.digest);
}
for alg in algs {
let digest = match &s.signed_attrs {
Some(attrs) => alg.digest(&[attrs]),
None => content_digest.to_vec(),
};
// Notes from an attempt that doesn't verify say nothing about the signature.
let mut attempt = Vec::new();
if cert.public_key.verify_noting(s.scheme, alg, &digest, &s.signature, &mut attempt)? {
notes.extend(attempt);
// The hash of the content and the hash the signature is made with.
for weak in [s.digest, alg].into_iter().filter_map(DigestAlg::weakness) {
if !notes.contains(&weak) {
notes.push(weak);
}
}
if alg != named {
notes.push("The signature algorithm and the SignerInfo's digest algorithm name different hashes; the signature was made with the SignerInfo's.");
}
return Ok(true);
}
}
Ok(false)
}
}
fn strip(mut b: &[u8]) -> &[u8] {
while let [0, rest @ ..] = b
&& !rest.is_empty()
{
b = rest;
}
b
}
fn parse_signer_id(sid: &Tlv<'_>) -> Result<SignerId, Error> {
match sid.tag {
tag::SEQUENCE => {
let p = sid.children()?;
let [issuer, serial] = p.as_slice() else { return Err(bad("issuerAndSerialNumber")) };
if issuer.tag != tag::SEQUENCE || serial.tag != tag::INTEGER || serial.value.is_empty() {
return Err(bad("issuerAndSerialNumber"));
}
Ok(SignerId::IssuerSerial { issuer: issuer.raw.to_vec(), serial: serial.value.to_vec() })
}
t if t == tag::ctx_prim(0) => Ok(SignerId::KeyId(sid.value.to_vec())),
t if t == tag::ctx(0) => {
let key_id = match sid.inner() {
Ok(inner) if matches!(inner.tag, tag::OCTET_STRING | tag::OCTET_STRING_SEGMENTS) => inner.octets("sid")?.into_owned(),
_ => {
let mut seg = *sid;
seg.tag = tag::OCTET_STRING_SEGMENTS;
seg.octets("sid")?.into_owned()
}
};
Ok(SignerId::KeyId(key_id))
}
_ => Err(bad("sid")),
}
}
type SignedAttrs = (Vec<u8>, Option<Vec<u8>>, Option<String>, Option<Time>, bool);
fn parse_signed_attrs(attrs: &Tlv<'_>) -> Result<SignedAttrs, Error> {
// Signed over the DER SET OF, i.e. the same contents with the universal SET tag.
// (An indefinite-length encoding is re-encoded with a definite length.)
let mut set = if attrs.is_indefinite() { der::tlv(tag::SET, attrs.value) } else { attrs.raw.to_vec() };
if let Some(t) = set.first_mut() {
*t = tag::SET;
}
let (mut message_digest, mut content_type, mut signing_time, mut signing_certificate) = (None, None, None, false);
let (mut seen_type, mut seen_digest) = (false, false);
for a in attrs.children()? {
let p = a.children()?;
let Some(o) = p.first().and_then(|o| o.oid().ok()) else { continue };
let values = p.get(1).and_then(|v| v.children().ok()).unwrap_or_default();
// RFC 5652 §11.1 and §11.2: one content-type and one message-digest attribute, each
// with one value. With a second one, two readers could each take a different value.
let (seen, what) = match o.as_str() {
CONTENT_TYPE => (Some(&mut seen_type), "content type"),
MESSAGE_DIGEST => (Some(&mut seen_digest), "message digest"),
_ => (None, ""),
};
if let Some(seen) = seen
&& (std::mem::replace(seen, true) || values.len() != 1)
{
return Err(bad(&format!("the signed attributes must hold one {what} with one value")));
}
let value = values.into_iter().next();
match (o.as_str(), value) {
(MESSAGE_DIGEST, Some(v)) => message_digest = Some(v.value.to_vec()),
// A value that is not an OID would skip every content-type check after this.
(CONTENT_TYPE, Some(v)) => content_type = Some(v.oid()?),
(SIGNING_TIME, Some(v)) => signing_time = v.time().ok(),
(SIGNING_CERT_V2 | SIGNING_CERT, _) => signing_certificate = true,
_ => {}
}
}
Ok((set, message_digest, content_type, signing_time, signing_certificate))
}
fn parse_signer(si: &Tlv<'_>) -> Result<SignerInfo, Error> {
let f = si.children()?;
let mut it = f.into_iter();
let _version = it.next();
let sid = parse_signer_id(&it.next().ok_or_else(|| bad("sid"))?)?;
let digest_alg = it.next().ok_or_else(|| bad("digestAlgorithm"))?.children()?;
let d_oid = digest_alg.first().ok_or_else(|| bad("digestAlgorithm"))?.oid()?;
let digest = DigestAlg::from_oid(&d_oid).ok_or_else(|| Error::Unsupported(format!("digest {d_oid}")))?;
let mut next = it.next().ok_or_else(|| bad("signatureAlgorithm"))?;
let (mut signed_attrs, mut message_digest, mut content_type, mut signing_time, mut signing_certificate) = (None, None, None, None, false);
if next.tag == tag::ctx(0) {
let (set, md, ct, st, sc) = parse_signed_attrs(&next)?;
(signed_attrs, message_digest, content_type, signing_time, signing_certificate) = (Some(set), md, ct, st, sc);
next = it.next().ok_or_else(|| bad("signatureAlgorithm"))?;
}
let (scheme, scheme_digest) = signature_algorithm(&next)?;
let signature = it.next().ok_or_else(|| bad("signature"))?.octets("signature")?.into_owned();
let (timestamp, timestamp_token) = parse_timestamp_token(it)?;
Ok(SignerInfo {
sid,
digest,
signed_attrs,
message_digest,
content_type,
signing_time,
signing_certificate,
scheme,
scheme_digest,
signature,
timestamp,
timestamp_token,
})
}
fn parse_timestamp_token<'a>(it: impl Iterator<Item = Tlv<'a>>) -> Result<(bool, Option<Vec<u8>>), Error> {
let mut timestamp = false;
let mut timestamp_token = None;
for t in it {
if t.tag == tag::ctx(1) {
for a in t.children()? {
let kids = a.children()?;
if kids.first().and_then(|o| o.oid().ok()).as_deref() == Some(TIMESTAMP_TOKEN) {
timestamp = true;
// The value is a SET OF ContentInfo: the token's own encoding.
let set = kids.get(1).and_then(|s| s.children().ok());
timestamp_token = set.and_then(|c| c.first().copied()).map(|ci| ci.raw.to_vec());
}
}
}
}
Ok((timestamp, timestamp_token))
}
fn merge_unsigned_attrs(signer_kids: &[Tlv<'_>], token_attr: Vec<u8>) -> Result<Vec<Vec<u8>>, Error> {
let mut parts: Vec<Vec<u8>> = signer_kids.iter().map(|t| t.raw.to_vec()).collect();
let mut attrs = Vec::new();
if let Some(last) = signer_kids.last()
&& last.tag == tag::ctx(1)
{
parts.pop();
for a in last.children()? {
let is_ts = a.children()?.first().and_then(|o| o.oid().ok()).as_deref() == Some(TIMESTAMP_TOKEN);
if !is_ts {
attrs.push(a.raw.to_vec());
}
}
}
attrs.push(token_attr);
let attr_refs: Vec<&[u8]> = attrs.iter().map(Vec::as_slice).collect();
let mut unsigned = der::set_of(&attr_refs);
if let Some(first) = unsigned.first_mut() {
*first = tag::ctx(1);
}
parts.push(unsigned);
Ok(parts)
}
/// Attach an RFC 3161 `signatureTimeStampToken` unsigned attribute to a detached CMS object.
/// The signed attributes and signature value are preserved byte-for-byte.
pub fn attach_timestamp_token(cms: &[u8], token: &[u8]) -> Result<Vec<u8>, Error> {
let (content_info, _) = Tlv::parse(cms)?;
let ci = content_info.expect(tag::SEQUENCE, "ContentInfo")?.children()?;
let [content_type, wrapped] = ci.as_slice() else { return Err(bad("ContentInfo")) };
if content_type.oid()? != SIGNED_DATA {
return Err(bad("not SignedData"));
}
let signed_data = wrapped.expect(tag::ctx(0), "content")?.inner()?;
let mut sd = signed_data.children()?;
// signerInfos is the last field of SignedData (RFC 5652 §5.1); searching from the end
// skips the SET OF digest algorithms.
let signer_index = sd.iter().rposition(|t| t.tag == tag::SET).ok_or_else(|| bad("no signerInfos"))?;
let signer_set = sd[signer_index];
let signer = signer_set.children()?.first().copied().ok_or_else(|| bad("no signer"))?;
let signer_kids = signer.children()?;
if signer_kids.len() < 6 {
return Err(bad("malformed signerInfo"));
}
let attr = der::seq(&[&der::oid(TIMESTAMP_TOKEN), &der::set_of(&[token])]);
let signer_parts = merge_unsigned_attrs(&signer_kids, attr)?;
let signer_refs: Vec<&[u8]> = signer_parts.iter().map(Vec::as_slice).collect();
let new_signer = der::seq(&signer_refs);
let new_signer_set = der::set_of(&[&new_signer]);
sd[signer_index] = Tlv::parse_all(&new_signer_set)?;
let sd_refs: Vec<&[u8]> = sd.iter().map(|t| t.raw).collect();
let new_sd = der::seq(&sd_refs);
let wrapped_new = der::explicit(0, &new_sd);
Ok(der::seq(&[&der::oid(SIGNED_DATA), &wrapped_new]))
}
fn attribute(o: &str, value: &[u8]) -> Vec<u8> {
der::seq(&[&der::oid(o), &der::set_of(&[value])])
}
/// A detached CAdES signature (PAdES B-B): signed attributes content-type, message-digest and
/// signing-certificate-v2 (no signing-time: PAdES takes the time from the signature
/// dictionary's `/M`). `chain` holds further certificates to embed (issuers).
pub fn sign_detached(key: &PrivateKey, cert: &Certificate, chain: &[Certificate], alg: DigestAlg, content_digest: &[u8]) -> Result<Vec<u8>, Error> {
build(SignedDataInput { key, cert, chain, alg, content_type: DATA, content: None, content_digest })
}
/// A CMS SignedData that encapsulates `content` under `content_type` (an RFC 3161 token wraps
/// its TSTInfo this way).
pub fn sign_encapsulated(
key: &PrivateKey,
cert: &Certificate,
chain: &[Certificate],
alg: DigestAlg,
content_type: &str,
content: &[u8],
) -> Result<Vec<u8>, Error> {
let content_digest = alg.digest(&[content]);
build(SignedDataInput { key, cert, chain, alg, content_type, content: Some(content), content_digest: &content_digest })
}
/// The values needed to assemble a CMS SignedData object.
struct SignedDataInput<'a> {
key: &'a PrivateKey,
cert: &'a Certificate,
chain: &'a [Certificate],
alg: DigestAlg,
content_type: &'a str,
content: Option<&'a [u8]>,
content_digest: &'a [u8],
}
fn build(input: SignedDataInput<'_>) -> Result<Vec<u8>, Error> {
let SignedDataInput { key, cert, chain, alg, content_type, content, content_digest } = input;
let cert_hash = DigestAlg::Sha256.digest(&[&cert.raw]);
let general_names = der::seq(&[&der::explicit(4, &cert.issuer.raw)]);
// The serial as the certificate has it: `cert.serial` is the INTEGER's own bytes, so one with
// its high bit set gets no leading zero here.
let serial = der::tlv(tag::INTEGER, if cert.serial.is_empty() { &[0] } else { &cert.serial });
let ess = der::seq(&[&der::seq(&[&der::seq(&[&der::octets(&cert_hash), &der::seq(&[&general_names, &serial])])])]);
let attrs =
[attribute(CONTENT_TYPE, &der::oid(content_type)), attribute(MESSAGE_DIGEST, &der::octets(content_digest)), attribute(SIGNING_CERT_V2, &ess)];
let refs: Vec<&[u8]> = attrs.iter().map(Vec::as_slice).collect();
let set = der::set_of(&refs);
let signature = key.sign(alg, &set)?;
let mut signed_attrs = set.clone();
signed_attrs[0] = tag::ctx(0);
let sid = der::seq(&[&cert.issuer.raw, &serial]);
let signer = der::seq(&[&der::int(1), &sid, &alg.algorithm(), &signed_attrs, &key.signature_algorithm(alg), &der::octets(&signature)]);
let mut certs: Vec<&[u8]> = vec![&cert.raw];
certs.extend(chain.iter().filter(|c| c.raw != cert.raw).map(|c| c.raw.as_slice()));
let encap = match content {
Some(c) => der::seq(&[&der::oid(content_type), &der::explicit(0, &der::octets(c))]),
None => der::seq(&[&der::oid(content_type)]),
};
let signed_data =
der::seq(&[&der::int(1), &der::set_of(&[&alg.algorithm()]), &encap, &der::tlv(tag::ctx(0), &certs.concat()), &der::set_of(&[&signer])]);
Ok(der::seq(&[&der::oid(SIGNED_DATA), &der::explicit(0, &signed_data)]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_removes_multiple_leading_zero_bytes() {
assert_eq!(strip(&[0, 0, 0, 0x42]), &[0x42]);
assert_eq!(strip(&[0, 0, 0]), &[0]);
}
#[test]
fn parse_signer_handles_constructed_key_id_and_rejects_invalid_sid_tags() {
let alg = DigestAlg::Sha256.algorithm();
let sig_alg = der::seq(&[&der::oid("1.2.840.113549.1.1.11")]);
let sig = der::octets(&[1, 2, 3, 4]);
// Constructed [0] wrapping an OCTET STRING key identifier.
let sid_explicit = der::explicit(0, &der::octets(&[0xAA, 0xBB, 0xCC]));
let si_explicit = der::seq(&[&der::int(3), &sid_explicit, &alg, &sig_alg, &sig]);
let parsed = parse_signer(&Tlv::parse_all(&si_explicit).expect("valid DER")).expect("explicit [0] KeyId should parse");
assert_eq!(parsed.sid, SignerId::KeyId(vec![0xAA, 0xBB, 0xCC]));
// Invalid sid tag (OCTET STRING instead of SEQUENCE or [0]).
let bad_sid = der::octets(&[0xAA]);
let si_bad = der::seq(&[&der::int(1), &bad_sid, &alg, &sig_alg, &sig]);
assert!(parse_signer(&Tlv::parse_all(&si_bad).expect("valid DER")).is_err());
// IssuerAndSerialNumber with non-INTEGER serial.
let bad_ias = der::seq(&[&der::seq(&[]), &der::octets(&[1])]);
let si_bad_ias = der::seq(&[&der::int(1), &bad_ias, &alg, &sig_alg, &sig]);
assert!(parse_signer(&Tlv::parse_all(&si_bad_ias).expect("valid DER")).is_err());
}
#[test]
fn attach_timestamp_token_merges_into_single_unsigned_attrs_set() {
let issuer = der::seq(&[]);
let sid = der::seq(&[&issuer, &der::int(1)]);
let alg = DigestAlg::Sha256.algorithm();
let sig_alg = der::seq(&[&der::oid("1.2.840.113549.1.1.11")]);
let signed_attrs = der::tlv(tag::ctx(0), &attribute(CONTENT_TYPE, &der::oid(DATA)));
let sig = der::octets(&[0x11, 0x22]);
let other_attr = attribute("1.2.3.4", &der::octets(&[0x99]));
let existing_unsigned = der::tlv(tag::ctx(1), &other_attr);
let signer = der::seq(&[&der::int(1), &sid, &alg, &signed_attrs, &sig_alg, &sig, &existing_unsigned]);
let encap = der::seq(&[&der::oid(DATA)]);
let sd = der::seq(&[&der::int(1), &der::set_of(&[&alg]), &encap, &der::set_of(&[&signer])]);
let cms = der::seq(&[&der::oid(SIGNED_DATA), &der::explicit(0, &sd)]);
let tok1 = der::seq(&[&der::oid(SIGNED_DATA), &der::explicit(0, &der::seq(&[]))]);
let tok2 = der::seq(&[&der::oid(SIGNED_DATA), &der::explicit(0, &der::seq(&[&der::int(1)]))]);
let with_tok1 = attach_timestamp_token(&cms, &tok1).expect("first attach");
let with_tok2 = attach_timestamp_token(&with_tok1, &tok2).expect("second attach replaces token in single [1]");
// Inspect raw SignerInfo children: must have exactly one [1] unsignedAttrs element.
let (ci, _) = Tlv::parse(&with_tok2).expect("valid cms");
let sd_children = ci.children().expect("ci")[1].inner().expect("sd").children().expect("sd kids");
let si_tlv = sd_children.last().expect("signerInfos").children().expect("set")[0];
let si_kids = si_tlv.children().expect("si kids");
let unsigned_fields: Vec<_> = si_kids.iter().filter(|t| t.tag == tag::ctx(1)).collect();
assert_eq!(unsigned_fields.len(), 1, "SignerInfo must contain at most one [1] unsignedAttrs field");
assert_eq!(unsigned_fields[0].children().expect("attrs").len(), 2, "other unsigned attr preserved alongside replaced timestamp token");
let parsed = SignedData::parse(&with_tok2).expect("parsed SignedData");
assert!(parsed.signer.timestamp);
assert_eq!(parsed.signer.timestamp_token.as_deref(), Some(tok2.as_slice()));
}
/// A self-signed certificate whose serial number is exactly `serial` (the INTEGER's bytes).
fn certificate_with_serial(key: &PrivateKey, serial: &[u8]) -> Certificate {
use crate::x509::Name;
let name = Name::build("High-Bit Serial Signer", "", "", "", "");
let alg = key.preferred_digest();
let sig_alg = key.signature_algorithm(alg);
let from = Time { year: 2026, month: 1, day: 1, hour: 0, minute: 0, second: 0 };
let until = Time { year: 2027, ..from };
let tbs = der::seq(&[
&der::explicit(0, &der::int(2)),
&der::tlv(tag::INTEGER, serial),
&sig_alg,
&name.raw,
&der::seq(&[&from.encode(), &until.encode()]),
&name.raw,
&key.public_key().spki(),
]);
let signature = key.sign(alg, &tbs).expect("signed");
Certificate::parse(&der::seq(&[&tbs, &sig_alg, &der::bit_string(&signature)])).expect("certificate")
}
#[test]
fn build_preserves_high_bit_certificate_serial_verbatim_in_sid_and_ess() {
let key = PrivateKey::generate_p256().expect("key");
let neg_serial = vec![0xEA, 0xE4, 0x80, 0x7F, 0xC7, 0x49, 0x4C, 0x35, 0x8D, 0x58];
let cert = certificate_with_serial(&key, &neg_serial);
assert_eq!(cert.serial, neg_serial);
let digest = DigestAlg::Sha256.digest(&[b"hello"]);
let cms_bytes = sign_detached(&key, &cert, &[], DigestAlg::Sha256, &digest).expect("signed");
let parsed = SignedData::parse(&cms_bytes).expect("parsed SignedData");
assert_eq!(parsed.signer.sid, SignerId::IssuerSerial { issuer: cert.issuer.raw.clone(), serial: neg_serial.clone() });
assert_eq!(parsed.signer_certificate(), Some(&cert));
assert!(parsed.verify_signature(&cert, &digest).expect("verified"));
let expected_tlv = der::tlv(tag::INTEGER, &neg_serial);
let padded_tlv = der::uint(&neg_serial);
let signed_attrs = parsed.signer.signed_attrs.as_deref().expect("signed attributes");
assert!(
signed_attrs.windows(expected_tlv.len()).any(|w| w == expected_tlv.as_slice()),
"ESSCertIDv2 must encode the certificate serial verbatim"
);
assert!(
!signed_attrs.windows(padded_tlv.len()).any(|w| w == padded_tlv.as_slice()),
"ESSCertIDv2 must not prepend a leading 0x00 to a high-bit certificate serial"
);
}
}
+663
View File
@@ -0,0 +1,663 @@
//! A small BER/DER reader and writer (ITU-T X.690): just what X.509, CMS and PKCS#12 need.
//!
//! [`Tlv`] borrows from the input and keeps its full encoding (`raw`), so signed structures can
//! be verified over the exact bytes that were signed.
//!
//! Reading is strict DER by default, which is what everything signed must use. [`Tlv::parse_ber`]
//! also accepts the two BER constructions a reader meets in practice — the indefinite length form
//! and strings split into segments — which PKCS #12 needs: Windows writes `.p12` files that way.
//! Writing is always DER.
use std::borrow::Cow;
use crate::Error;
/// Universal tags (single-byte identifiers).
pub mod tag {
pub const BOOLEAN: u8 = 0x01;
pub const INTEGER: u8 = 0x02;
pub const ENUMERATED: u8 = 0x0A;
pub const BIT_STRING: u8 = 0x03;
pub const OCTET_STRING: u8 = 0x04;
/// An OCTET STRING split into segments (BER only, X.690 §8.21); see [`super::Tlv::octets`].
pub const OCTET_STRING_SEGMENTS: u8 = 0x24;
pub const NULL: u8 = 0x05;
pub const OID: u8 = 0x06;
pub const UTF8_STRING: u8 = 0x0C;
pub const PRINTABLE_STRING: u8 = 0x13;
pub const T61_STRING: u8 = 0x14;
pub const IA5_STRING: u8 = 0x16;
pub const UTC_TIME: u8 = 0x17;
pub const GENERALIZED_TIME: u8 = 0x18;
pub const BMP_STRING: u8 = 0x1E;
pub const SEQUENCE: u8 = 0x30;
pub const SET: u8 = 0x31;
/// `[n]` context-specific, constructed (EXPLICIT, or IMPLICIT over a constructed type).
pub const fn ctx(n: u8) -> u8 {
0xA0 | n
}
/// `[n]` context-specific, primitive (IMPLICIT over a primitive type).
pub const fn ctx_prim(n: u8) -> u8 {
0x80 | n
}
}
fn bad(what: &str) -> Error {
Error::Malformed(what.to_string())
}
/// Which encoding rules a reader accepts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Form {
/// DER: every length definite, every string primitive. Anything whose bytes are signed is read
/// this way, so that what is verified is exactly what was written.
Der,
/// BER as well: the indefinite length form (X.690 §8.1.3.6) and strings split into segments
/// (§8.21).
Ber,
}
/// One BER/DER element.
#[derive(Clone, Copy, Debug, Eq)]
pub struct Tlv<'a> {
pub tag: u8,
/// The contents octets (without the end-of-contents octets of an indefinite length).
pub value: &'a [u8],
/// The whole encoding (identifier, length, contents and any end-of-contents octets).
pub raw: &'a [u8],
/// The rules this element was read with. Its children are read the same way.
form: Form,
}
/// Two elements are equal when they encode the same thing; the rules they were read with are not
/// part of that.
impl PartialEq for Tlv<'_> {
fn eq(&self, other: &Self) -> bool {
self.tag == other.tag && self.value == other.value && self.raw == other.raw
}
}
impl<'a> Tlv<'a> {
/// Parse one DER element at the start of `input`; returns it and the rest.
pub fn parse(input: &'a [u8]) -> Result<(Tlv<'a>, &'a [u8]), Error> {
Tlv::read(input, Form::Der, 0)
}
/// Parse one BER element at the start of `input`; returns it and the rest.
pub fn parse_ber(input: &'a [u8]) -> Result<(Tlv<'a>, &'a [u8]), Error> {
Tlv::read(input, Form::Ber, 0)
}
fn read(input: &'a [u8], form: Form, depth: usize) -> Result<(Tlv<'a>, &'a [u8]), Error> {
let (&tag, rest) = input.split_first().ok_or_else(|| bad("truncated DER"))?;
if tag & 0x1F == 0x1F {
return Err(bad("multi-byte DER tags are not supported"));
}
let (&first, rest) = rest.split_first().ok_or_else(|| bad("truncated DER length"))?;
// `trailer` is the two end-of-contents octets, which belong to the element but not to its
// contents.
let (len, rest, trailer) = if first < 0x80 {
(first as usize, rest, 0)
} else if first == 0x80 {
if form == Form::Der {
return Err(bad("indefinite lengths are not allowed in DER"));
}
if tag & 0x20 == 0 {
return Err(bad("an indefinite length needs a constructed element"));
}
(contents_len(rest, form, depth)?, rest, 2)
} else {
let n = (first & 0x7F) as usize;
if n > 4 || rest.len() < n {
return Err(bad("unsupported DER length"));
}
let len = rest[..n].iter().fold(0usize, |acc, b| (acc << 8) | *b as usize);
(len, &rest[n..], 0)
};
let end = len.checked_add(trailer).ok_or_else(|| bad("DER length overflows"))?;
if rest.len() < end {
return Err(bad("DER element runs past the end"));
}
// `end <= rest.len()`, so neither sum below can overflow.
let header = input.len() - rest.len();
Ok((Tlv { tag, value: &rest[..len], raw: &input[..header + end], form }, &rest[end..]))
}
/// Parse `input` as exactly one DER element.
pub fn parse_all(input: &'a [u8]) -> Result<Tlv<'a>, Error> {
Tlv::only(input, Form::Der)
}
/// Parse `input` as exactly one BER element.
pub fn parse_all_ber(input: &'a [u8]) -> Result<Tlv<'a>, Error> {
Tlv::only(input, Form::Ber)
}
fn only(input: &'a [u8], form: Form) -> Result<Tlv<'a>, Error> {
let (t, rest) = Tlv::read(input, form, 0)?;
if !rest.is_empty() {
return Err(bad("trailing bytes after DER element"));
}
Ok(t)
}
/// Whether this element is written with the indefinite length form (BER, never DER). Tags are
/// single-octet here, so the length octet is the second.
pub fn is_indefinite(&self) -> bool {
self.raw.get(1) == Some(&0x80)
}
/// The children of a constructed element, read with the same rules as their parent.
pub fn children(&self) -> Result<Vec<Tlv<'a>>, Error> {
let mut out = Vec::new();
let mut rest = self.value;
while !rest.is_empty() {
let (t, r) = Tlv::read(rest, self.form, 0)?;
out.push(t);
rest = r;
}
Ok(out)
}
/// Check the tag, for a clear error.
pub fn expect(self, tag: u8, what: &str) -> Result<Tlv<'a>, Error> {
if self.tag == tag { Ok(self) } else { Err(bad(&format!("{what}: expected tag {tag:#04x}, found {:#04x}", self.tag))) }
}
/// The element inside an EXPLICIT tag.
pub fn inner(&self) -> Result<Tlv<'a>, Error> {
Tlv::only(self.value, self.form)
}
/// An OCTET STRING's contents, joining the segments of BER's constructed form (X.690 §8.21).
/// Borrowed in the ordinary case, so the common path copies nothing.
pub fn octets(&self, what: &str) -> Result<Cow<'a, [u8]>, Error> {
self.join(what, 0)
}
fn join(&self, what: &str, depth: usize) -> Result<Cow<'a, [u8]>, Error> {
/// Deep enough for anything real: a writer that segments at all segments once.
const MAX_DEPTH: usize = 16;
match self.tag {
tag::OCTET_STRING => Ok(Cow::Borrowed(self.value)),
tag::OCTET_STRING_SEGMENTS if self.form == Form::Ber => {
if depth >= MAX_DEPTH {
return Err(bad(&format!("{what}: OCTET STRING segments nested too deeply")));
}
// The join is never larger than the segments it is made of, so this is bounded by
// the input.
let mut out: Vec<u8> = Vec::with_capacity(self.value.len());
for s in self.children()? {
out.extend_from_slice(&s.join(what, depth + 1)?);
}
Ok(Cow::Owned(out))
}
found => Err(bad(&format!("{what}: expected an OCTET STRING, found tag {found:#04x}"))),
}
}
/// An OID in dotted form.
pub fn oid(&self) -> Result<String, Error> {
if self.tag != tag::OID {
return Err(bad("expected an OID"));
}
oid_to_string(self.value)
}
/// A non-negative INTEGER that fits in a u64.
pub fn u64(&self) -> Result<u64, Error> {
// (nine bytes only with a 0x00 pad: a nonzero first byte is past 64 bits)
if self.tag != tag::INTEGER
|| self.value.is_empty()
|| self.value.len() > 9
|| self.value[0] & 0x80 != 0
|| (self.value.len() == 9 && self.value[0] != 0)
{
return Err(bad("expected a small non-negative INTEGER"));
}
Ok(self.value.iter().fold(0u64, |acc, b| (acc << 8) | *b as u64))
}
/// A non-negative ENUMERATED that fits in a u64 (same encoding as an INTEGER).
pub fn enumerated(&self) -> Result<u64, Error> {
if self.tag != tag::ENUMERATED {
return Err(bad("expected an ENUMERATED"));
}
Tlv { tag: tag::INTEGER, ..*self }.u64()
}
/// The magnitude of an INTEGER (leading zero dropped).
pub fn uint_bytes(&self) -> &'a [u8] {
match self.value {
[0, rest @ ..] if !rest.is_empty() => rest,
v => v,
}
}
/// A BIT STRING's bytes (no unused bits allowed).
pub fn bits(&self) -> Result<&'a [u8], Error> {
match self.value {
[0, rest @ ..] if self.tag == tag::BIT_STRING => Ok(rest),
_ => Err(bad("expected a whole-byte BIT STRING")),
}
}
/// A string type as text (UTF-8, Printable, IA5, T61 as Latin-1, BMP).
pub fn text(&self) -> Option<String> {
match self.tag {
tag::UTF8_STRING | tag::PRINTABLE_STRING | tag::IA5_STRING => Some(String::from_utf8_lossy(self.value).into_owned()),
tag::T61_STRING => Some(self.value.iter().map(|b| *b as char).collect()),
tag::BMP_STRING => {
let units: Vec<u16> = self.value.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
Some(String::from_utf16_lossy(&units))
}
_ => None,
}
}
/// UTCTime / GeneralizedTime as (year, month, day, hour, minute, second) in UTC.
pub fn time(&self) -> Result<Time, Error> {
let s = std::str::from_utf8(self.value).map_err(|_| bad("time"))?;
let s = s.strip_suffix('Z').ok_or_else(|| bad("times must be in UTC (Z)"))?;
let num = |r: std::ops::Range<usize>| s.get(r).and_then(|x| x.parse::<u32>().ok()).ok_or_else(|| bad("time"));
let (year, rest) = match self.tag {
tag::UTC_TIME => {
let y = num(0..2)?;
(if y >= 50 { 1900 + y } else { 2000 + y }, 2)
}
tag::GENERALIZED_TIME => (num(0..4)?, 4),
_ => return Err(bad("expected a time")),
};
let t = Time {
year,
month: num(rest..rest + 2)?,
day: num(rest + 2..rest + 4)?,
hour: num(rest + 4..rest + 6)?,
minute: num(rest + 6..rest + 8)?,
second: if s.len() >= rest + 10 { num(rest + 8..rest + 10)? } else { 0 },
};
Ok(t)
}
}
/// How many contents octets an indefinite-length element has: everything up to the matching
/// end-of-contents octets, which have to be there (X.690 §8.1.5). The contents are elements, so
/// finding the end means reading them, and one of those may be indefinite too — hence the depth
/// limit. Every step consumes at least two octets, so the walk always ends.
fn contents_len(input: &[u8], form: Form, depth: usize) -> Result<usize, Error> {
/// Far deeper than any real file nests, and shallow enough never to exhaust the stack.
const MAX_DEPTH: usize = 48;
if depth >= MAX_DEPTH {
return Err(bad("BER elements nested too deeply"));
}
let mut rest = input;
loop {
match rest {
[] => return Err(bad("an indefinite length with no end-of-contents")),
[0, 0, ..] => return Ok(input.len() - rest.len()),
_ => rest = Tlv::read(rest, form, depth + 1)?.1,
}
}
}
/// `NaiveDate::num_days_from_ce` of 1970-01-01, so the count can be rebased onto the Unix epoch.
const UNIX_EPOCH_DAYS_FROM_CE: i64 = 719_163;
/// A UTC time.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
pub year: u32,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
pub second: u32,
}
impl Time {
/// From a PDF date (`D:YYYYMMDDHHmmSS±HH'mm'`), converted to UTC.
pub fn from_pdf(s: &str) -> Option<Time> {
let s = s.strip_prefix("D:").unwrap_or(s);
let d = |r: std::ops::Range<usize>, default: u32| s.get(r).map_or(Some(default), |x| x.parse::<u32>().ok());
let mut t =
Time { year: d(0..4, 0)?, month: d(4..6, 1)?, day: d(6..8, 1)?, hour: d(8..10, 0)?, minute: d(10..12, 0)?, second: d(12..14, 0)? };
if t.year == 0 {
return None;
}
let tz = s.get(14..).unwrap_or("");
let sign = match tz.chars().next() {
Some('+') => -1i64,
Some('-') => 1,
_ => 0,
};
if sign != 0 {
let h = tz.get(1..3).and_then(|x| x.parse::<i64>().ok()).unwrap_or(0);
let m = tz.get(4..6).and_then(|x| x.parse::<i64>().ok()).unwrap_or(0);
t = t.add_seconds(sign * (h * 3600 + m * 60));
}
Some(t)
}
/// As a PDF date in UTC (`D:YYYYMMDDHHmmSSZ`).
pub fn to_pdf(&self) -> String {
format!("D:{:04}{:02}{:02}{:02}{:02}{:02}Z", self.year, self.month, self.day, self.hour, self.minute, self.second)
}
/// Seconds since 1970-01-01 (proleptic Gregorian).
pub fn unix(&self) -> i64 {
use chrono::Datelike;
// The calendar date goes through chrono; the time of day is a plain second count, as
// before. A date chrono cannot represent (an out-of-range field) reads as the epoch day,
// which no well-formed UTCTime/GeneralizedTime produces.
let days = i32::try_from(self.year)
.ok()
.and_then(|y| chrono::NaiveDate::from_ymd_opt(y, self.month, self.day))
.map_or(0, |d| i64::from(d.num_days_from_ce()) - UNIX_EPOCH_DAYS_FROM_CE);
days * 86_400 + self.hour as i64 * 3600 + self.minute as i64 * 60 + self.second as i64
}
/// The UTC time at `t` seconds since 1970-01-01.
///
/// The calendar date goes through chrono's 400-year Gregorian cycle (146_097 days) so a `t`
/// past `NaiveDate`'s ±262143-year range keeps the proleptic Gregorian date the old hand-rolled
/// converter produced instead of falling back to the epoch. The `year` field is a `u32`, so the
/// year is cast exactly as the old code did (`as u32`, wrapping) — no well-formed signing time
/// is anywhere near that range, and this preserves byte-for-byte the old output everywhere.
pub fn from_unix(t: i64) -> Time {
let (days, secs) = (t.div_euclid(86_400), t.rem_euclid(86_400));
let (year, month, day) = civil_from_days(days);
Time { year: year as u32, month, day, hour: (secs / 3600) as u32, minute: (secs % 3600 / 60) as u32, second: (secs % 60) as u32 }
}
pub fn add_seconds(&self, s: i64) -> Time {
Time::from_unix(self.unix() + s)
}
/// UTCTime before 2050, GeneralizedTime from then on (RFC 5280 §4.1.2.5).
pub fn encode(&self) -> Vec<u8> {
if (1950..2050).contains(&self.year) {
let s = format!("{:02}{:02}{:02}{:02}{:02}{:02}Z", self.year % 100, self.month, self.day, self.hour, self.minute, self.second);
tlv(tag::UTC_TIME, s.as_bytes())
} else {
let s = format!("{:04}{:02}{:02}{:02}{:02}{:02}Z", self.year, self.month, self.day, self.hour, self.minute, self.second);
tlv(tag::GENERALIZED_TIME, s.as_bytes())
}
}
}
impl std::fmt::Display for Time {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:04}.{:02}.{:02} {:02}:{:02}:{:02} UTC", self.year, self.month, self.day, self.hour, self.minute, self.second)
}
}
/// The proleptic Gregorian date `days` after 1970-01-01, through chrono's 400-year cycle so that
/// any `i64` day count has a date.
fn civil_from_days(days: i64) -> (i64, u32, u32) {
use chrono::Datelike;
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` is 719_163..865_259, always a valid CE day.
match i32::try_from(rest + UNIX_EPOCH_DAYS_FROM_CE).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) {
Some(d) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
(i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.month(), d.day())
}
None => (1970, 1, 1),
}
}
fn oid_to_string(v: &[u8]) -> Result<String, Error> {
let mut arcs: Vec<u64> = Vec::new();
let mut acc = 0u64;
for (i, b) in v.iter().enumerate() {
acc = acc.checked_mul(128).ok_or_else(|| bad("OID arc too large"))? | (b & 0x7F) as u64;
if b & 0x80 == 0 {
if arcs.is_empty() {
let first = (acc / 40).min(2);
arcs.push(first);
arcs.push(acc - first * 40);
} else {
arcs.push(acc);
}
acc = 0;
} else if i + 1 == v.len() {
return Err(bad("truncated OID"));
}
}
Ok(arcs.iter().map(u64::to_string).collect::<Vec<_>>().join("."))
}
// ── writing ─────────────────────────────────────────────────────────────────────────────────
/// One element: identifier, definite length, contents.
pub fn tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
let n = value.len();
if n < 0x80 {
out.push(n as u8);
} else {
let bytes: Vec<u8> = n.to_be_bytes().iter().copied().skip_while(|b| *b == 0).collect();
out.push(0x80 | bytes.len() as u8);
out.extend(bytes);
}
out.extend_from_slice(value);
out
}
pub fn seq(parts: &[&[u8]]) -> Vec<u8> {
tlv(tag::SEQUENCE, &parts.concat())
}
/// A SET OF, in DER order (sorted encodings, X.690 §11.6).
pub fn set_of(parts: &[&[u8]]) -> Vec<u8> {
let mut v: Vec<&[u8]> = parts.to_vec();
v.sort();
tlv(tag::SET, &v.concat())
}
pub fn explicit(n: u8, inner: &[u8]) -> Vec<u8> {
tlv(tag::ctx(n), inner)
}
/// The encoding of an OID constant (callers pass only literals from this crate).
// The documented never-crash exception: every caller passes a literal that always parses.
#[allow(clippy::expect_used)]
pub fn oid(dotted: &str) -> Vec<u8> {
let arcs: Vec<u64> = dotted.split('.').map(|a| a.parse().expect("valid OID constant")).collect();
let mut body = Vec::new();
let mut push = |mut v: u64| {
let mut tmp = vec![(v & 0x7F) as u8];
v >>= 7;
while v > 0 {
tmp.push(0x80 | (v & 0x7F) as u8);
v >>= 7;
}
tmp.reverse();
body.extend(tmp);
};
push(arcs[0] * 40 + arcs[1]);
for a in &arcs[2..] {
push(*a);
}
tlv(tag::OID, &body)
}
/// Encode a dotted OID supplied by a caller. Unlike [`oid`], this validates every arc and
/// never panics on malformed input.
pub fn try_oid(dotted: &str) -> Result<Vec<u8>, Error> {
let mut arcs = dotted.split('.').map(|a| {
if a.is_empty() || !a.bytes().all(|c| c.is_ascii_digit()) {
return Err(bad("invalid OID arc"));
}
a.parse::<u64>().map_err(|_| bad("invalid OID arc"))
});
let first = arcs.next().ok_or_else(|| bad("empty OID"))??;
let second = arcs.next().ok_or_else(|| bad("OID needs two arcs"))??;
if first > 2 || (first < 2 && second >= 40) {
return Err(bad("invalid OID prefix"));
}
let mut body = Vec::new();
let mut push = |mut v: u64| {
let mut tmp = vec![(v & 0x7F) as u8];
v >>= 7;
while v > 0 {
tmp.push(0x80 | (v & 0x7F) as u8);
v >>= 7;
}
tmp.reverse();
body.extend(tmp);
};
let base = first.checked_mul(40).and_then(|b| b.checked_add(second)).ok_or_else(|| bad("invalid OID prefix"))?;
push(base);
for arc in arcs {
push(arc?);
}
Ok(tlv(tag::OID, &body))
}
pub fn uint(bytes: &[u8]) -> Vec<u8> {
let b: &[u8] = match bytes.iter().position(|x| *x != 0) {
Some(i) => &bytes[i..],
None => &[0],
};
if b[0] & 0x80 != 0 {
let mut v = vec![0];
v.extend_from_slice(b);
tlv(tag::INTEGER, &v)
} else {
tlv(tag::INTEGER, b)
}
}
pub fn int(n: u64) -> Vec<u8> {
uint(&n.to_be_bytes())
}
pub fn octets(b: &[u8]) -> Vec<u8> {
tlv(tag::OCTET_STRING, b)
}
pub fn bit_string(b: &[u8]) -> Vec<u8> {
let mut v = vec![0];
v.extend_from_slice(b);
tlv(tag::BIT_STRING, &v)
}
pub fn null() -> Vec<u8> {
vec![tag::NULL, 0]
}
pub fn utf8(s: &str) -> Vec<u8> {
tlv(tag::UTF8_STRING, s.as_bytes())
}
pub fn printable(s: &str) -> Vec<u8> {
tlv(tag::PRINTABLE_STRING, s.as_bytes())
}
pub fn ia5(s: &str) -> Vec<u8> {
tlv(tag::IA5_STRING, s.as_bytes())
}
pub fn boolean(b: bool) -> Vec<u8> {
tlv(tag::BOOLEAN, &[if b { 0xFF } else { 0 }])
}
/// `AlgorithmIdentifier` with optional parameters.
pub fn algorithm(oid_s: &str, params: Option<&[u8]>) -> Vec<u8> {
match params {
Some(p) => seq(&[&oid(oid_s), p]),
None => seq(&[&oid(oid_s)]),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_nine_byte_integer_needs_a_zero_pad_to_fit_64_bits() {
let tlv = |value: &[u8]| {
let mut b = vec![tag::INTEGER, value.len() as u8];
b.extend_from_slice(value);
b
};
// 2^64 would fold to 0: refused, not read as 0
assert!(Tlv::parse_all(&tlv(&[1, 0, 0, 0, 0, 0, 0, 0, 0])).unwrap().u64().is_err());
// u64::MAX with its pad is fine
let max = [0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
assert_eq!(Tlv::parse_all(&tlv(&max)).unwrap().u64().unwrap(), u64::MAX);
}
#[test]
fn try_oid_rejects_signs_and_empty_arcs() {
for bad in ["+1.2.840", "1.+2", "1.2.-3", "1..2"] {
assert!(try_oid(bad).is_err(), "accepted {bad}");
}
assert_eq!(try_oid("1.2.840.113549").unwrap(), oid("1.2.840.113549"));
}
#[test]
fn round_trips() {
let o = oid("1.2.840.113549.1.7.2");
let t = Tlv::parse_all(&o).unwrap();
assert_eq!(t.oid().unwrap(), "1.2.840.113549.1.7.2");
let big = vec![7u8; 300];
let e = octets(&big);
assert_eq!(&e[..4], &[0x04, 0x82, 0x01, 0x2C]);
assert_eq!(Tlv::parse_all(&e).unwrap().value, &big[..]);
assert_eq!(uint(&[0x80, 1]), vec![0x02, 3, 0, 0x80, 1]);
assert_eq!(Tlv::parse_all(&int(65537)).unwrap().u64().unwrap(), 65537);
let s = set_of(&[&int(2), &int(1)]);
assert_eq!(s, vec![0x31, 6, 2, 1, 1, 2, 1, 2]);
assert!(Tlv::parse(&[0x30, 5, 1]).is_err());
}
#[test]
fn reads_ber_indefinite_lengths_and_segmented_strings() {
// SEQUENCE { INTEGER 3, OCTET STRING { "hi", "!" } }, every wrapper in the indefinite form:
// the shape Windows gives a .p12 file.
let ber: &[u8] = &[0x30, 0x80, 0x02, 0x01, 0x03, 0x24, 0x80, 0x04, 0x02, b'h', b'i', 0x04, 0x01, b'!', 0, 0, 0, 0];
let e = Tlv::parse_all(ber).unwrap_err().to_string();
assert!(e.contains("indefinite lengths are not allowed in DER"), "{e}");
let t = Tlv::parse_all_ber(ber).unwrap();
assert_eq!(t.tag, tag::SEQUENCE);
assert_eq!(t.raw, ber, "the end-of-contents octets belong to the element");
assert_eq!(t.value, &ber[2..ber.len() - 2], "but not to its contents");
let c = t.children().unwrap();
assert_eq!(c[0].u64().unwrap(), 3);
assert_eq!(c[1].octets("segments").unwrap().as_ref(), b"hi!", "the segments are joined");
assert!(c[0].octets("not a string").is_err());
// Definite lengths read the same either way, and a segmented string stays a DER error.
assert_eq!(Tlv::parse_all_ber(&int(65537)).unwrap().u64().unwrap(), 65537);
assert_eq!(Tlv::parse_all_ber(&octets(b"hi!")).unwrap().octets("plain").unwrap().as_ref(), b"hi!");
assert!(Tlv::parse_all(&[0x24, 2, 0x04, 0]).unwrap().octets("segments").is_err());
// Malformed: no end-of-contents, and an indefinite length on a primitive element.
assert!(Tlv::parse_all_ber(&ber[..ber.len() - 2]).is_err());
assert!(Tlv::parse_all_ber(&[0x04, 0x80, 0, 0]).is_err());
// Nesting is bounded, so a file made of nothing but wrappers fails instead of crashing.
let deep = [[0x30u8, 0x80].repeat(400), [0u8, 0].repeat(400)].concat();
assert!(Tlv::parse_all_ber(&deep).is_err());
}
#[test]
fn times() {
let t = Time { year: 2026, month: 10, day: 2, hour: 9, minute: 30, second: 5 };
assert_eq!(Time::from_unix(t.unix()), t);
assert_eq!(Tlv::parse_all(&t.encode()).unwrap().time().unwrap(), t);
let far = Time { year: 2051, ..t };
assert_eq!(Tlv::parse_all(&far.encode()).unwrap().tag, tag::GENERALIZED_TIME);
assert_eq!(Time::from_pdf("D:20261002113005+02'00'"), Some(Time { year: 2026, month: 10, day: 2, hour: 9, minute: 30, second: 5 }));
assert_eq!(Time::from_unix(0), Time { year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0 });
// Past chrono's ±262143-year range `from_unix` keeps the proleptic Gregorian date the old
// hand-rolled converter gave, not the epoch. No real signing time is near this; the `year`
// field is a `u32`, so the year wraps exactly as the old `as u32` cast did (values pinned
// from the old formula). This also covers both i64 extremes so neither div_euclid panics.
assert_eq!(Time::from_unix(10_000_000_000_000), Time { year: 318857, month: 5, day: 20, hour: 17, minute: 46, second: 40 });
assert_eq!(Time::from_unix(-10_000_000_000_000), Time { year: 4_294_652_378, month: 8, day: 13, hour: 6, minute: 13, second: 20 });
assert_eq!(Time::from_unix(i64::MAX), Time { year: 219_250_468, month: 12, day: 4, hour: 15, minute: 30, second: 7 });
assert_eq!(Time::from_unix(i64::MIN), Time { year: 4_075_720_767, month: 1, day: 27, hour: 8, minute: 29, second: 52 });
}
}
+180
View File
@@ -0,0 +1,180 @@
//! ECDSA verification on brainpoolP512r1 (RFC 5639), for which no RustCrypto curve crate exists.
//!
//! Verification only, on public data, so plain affine arithmetic over `crypto-bigint` is enough:
//! no secret is involved, and a verification takes a few milliseconds. Checked against
//! signatures made by OpenSSL (`tests/it/crypto.rs`).
use rsa::BoxedUint;
const P: &str = "aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3";
const A: &str = "7830a3318b603b89e2327145ac234cc594cbdd8d3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94ca";
const B: &str = "3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94cadc083e67984050b75ebae5dd2809bd638016f723";
const GX: &str = "81aee4bdd82ed9645a21322e9c4c6a9385ed9f70b5d916c1b43b62eef4d0098eff3b1f78e2d0d48d50d1687b93b97d5f7c6d5047406a5e688b352209bcb9f822";
const GY: &str = "7dde385d566332ecc0eabfa9cf7822fdf209f70024a57b1aa000c55b881f8111b2dcde494a5f485e5bca4bd88a2763aed1ca2b2fa8f0540678cd1e0f3ad80892";
const N: &str = "aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca70330870553e5c414ca92619418661197fac10471db1d381085ddaddb58796829ca90069";
const BITS: u32 = 512;
const BYTES: usize = 64;
type Point = Option<(BoxedUint, BoxedUint)>;
fn big(bytes: &[u8]) -> Option<BoxedUint> {
let pad = BYTES.checked_sub(bytes.len())?;
let mut v = vec![0u8; pad];
v.extend_from_slice(bytes);
BoxedUint::from_be_slice(&v, BITS).ok()
}
fn hex(s: &str) -> Option<BoxedUint> {
let bytes: Option<Vec<u8>> = (0..s.len()).step_by(2).map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok()).collect();
big(&bytes?)
}
/// Arithmetic modulo a prime.
struct Field {
nz: crypto_bigint::NonZero<BoxedUint>,
}
impl Field {
fn new(p: BoxedUint) -> Option<Field> {
let nz = Option::from(crypto_bigint::NonZero::new(p))?;
Some(Field { nz })
}
fn add(&self, a: &BoxedUint, b: &BoxedUint) -> BoxedUint {
a.add_mod(b, &self.nz)
}
fn sub(&self, a: &BoxedUint, b: &BoxedUint) -> BoxedUint {
a.sub_mod(b, &self.nz)
}
fn mul(&self, a: &BoxedUint, b: &BoxedUint) -> BoxedUint {
a.mul_mod(b, &self.nz)
}
fn inv(&self, a: &BoxedUint) -> Option<BoxedUint> {
Option::from(a.invert_mod(&self.nz))
}
/// Whether `v` is reduced (`v < p`). `add_mod`, `sub_mod` and friends assert it, and panic on
/// anything else, so nothing from outside may reach them without this check.
fn is_reduced(&self, v: &BoxedUint) -> bool {
v < self.nz.as_ref()
}
}
struct Curve {
f: Field,
a: BoxedUint,
b: BoxedUint,
g: (BoxedUint, BoxedUint),
n: BoxedUint,
}
impl Curve {
fn brainpool_p512r1() -> Option<Curve> {
Some(Curve { f: Field::new(hex(P)?)?, a: hex(A)?, b: hex(B)?, g: (hex(GX)?, hex(GY)?), n: hex(N)? })
}
fn on_curve(&self, x: &BoxedUint, y: &BoxedUint) -> bool {
let f = &self.f;
let lhs = f.mul(y, y);
let rhs = f.add(&f.add(&f.mul(&f.mul(x, x), x), &f.mul(&self.a, x)), &self.b);
lhs == rhs
}
fn double(&self, p: &Point) -> Point {
let (x, y) = p.as_ref()?;
let f = &self.f;
if bool::from(y.is_zero()) {
return None;
}
let three = big(&[3])?;
let two = big(&[2])?;
let num = f.add(&f.mul(&three, &f.mul(x, x)), &self.a);
let lambda = f.mul(&num, &f.inv(&f.mul(&two, y))?);
let x3 = f.sub(&f.sub(&f.mul(&lambda, &lambda), x), x);
let y3 = f.sub(&f.mul(&lambda, &f.sub(x, &x3)), y);
Some((x3, y3))
}
fn add(&self, p: &Point, q: &Point) -> Point {
let (Some((x1, y1)), Some((x2, y2))) = (p, q) else { return p.clone().or_else(|| q.clone()) };
let f = &self.f;
if x1 == x2 {
return if y1 == y2 { self.double(p) } else { None };
}
let lambda = f.mul(&f.sub(y2, y1), &f.inv(&f.sub(x2, x1))?);
let x3 = f.sub(&f.sub(&f.mul(&lambda, &lambda), x1), x2);
let y3 = f.sub(&f.mul(&lambda, &f.sub(x1, &x3)), y1);
Some((x3, y3))
}
/// `k · p`, double-and-add from the top bit.
fn mul(&self, k: &BoxedUint, p: &Point) -> Point {
let mut acc: Point = None;
for i in (0..k.bit_width_vartime()).rev() {
acc = self.double(&acc);
if bool::from(k.bit(i)) {
acc = self.add(&acc, p);
}
}
acc
}
}
/// Check the ECDSA signature `r ‖ s` (64 bytes each) of `digest` with the SEC1 uncompressed
/// point `public` (`04 ‖ x ‖ y`). `None`: the key is not a valid point or the input is malformed.
pub fn verify(public: &[u8], digest: &[u8], rs: &[u8]) -> Option<bool> {
let c = Curve::brainpool_p512r1()?;
let [0x04, xy @ ..] = public else { return None };
if xy.len() != 2 * BYTES || rs.len() != 2 * BYTES {
return None;
}
let (qx, qy) = (big(xy.get(..BYTES)?)?, big(xy.get(BYTES..)?)?);
// SEC 1 §2.3.4: coordinates are field elements. The field arithmetic panics on unreduced ones.
if !c.f.is_reduced(&qx) || !c.f.is_reduced(&qy) || !c.on_curve(&qx, &qy) {
return None;
}
let (r, s) = (big(rs.get(..BYTES)?)?, big(rs.get(BYTES..)?)?);
let n = crypto_bigint::NonZero::new(c.n.clone());
let n: crypto_bigint::NonZero<BoxedUint> = Option::from(n)?;
let zero = |v: &BoxedUint| bool::from(v.is_zero());
if zero(&r) || zero(&s) || r >= c.n || s >= c.n {
return Some(false);
}
// The leftmost 512 bits of the digest (all of it, for digests up to SHA-512).
let e = big(digest.get(..digest.len().min(BYTES))?)?.rem_vartime(&n);
let w: BoxedUint = Option::from(s.invert_mod(&n))?;
let u1 = e.mul_mod(&w, &n);
let u2 = r.mul_mod(&w, &n);
let point = c.add(&c.mul(&u1, &Some(c.g.clone())), &c.mul(&u2, &Some((qx, qy))));
// The point at infinity is no valid result (X9.62 §7.4.1 step 5): the signature is wrong.
let Some((x, _)) = point else { return Some(false) };
Some(x.rem_vartime(&n) == r)
}
#[cfg(test)]
mod tests {
use super::*;
fn unhex(s: &str) -> [u8; 64] {
let v: Vec<u8> = (0..s.len()).step_by(2).map(|i| u8::from_str_radix(s.get(i..i + 2).unwrap_or("00"), 16).unwrap_or(0)).collect();
let mut out = [0u8; 64];
out[64 - v.len()..].copy_from_slice(&v);
out
}
/// Never-crash: public keys and signature values of any size are refused, not panicked on.
#[test]
fn hostile_keys_and_signatures_are_refused() {
let digest = [7u8; 64];
let key = |x: &[u8; 64], y: &[u8; 64]| [&[4u8][..], x, y].concat();
let (p, n, gx, gy) = (unhex(P), unhex(N), unhex(GX), unhex(GY));
// Coordinates that are not field elements, or not on the curve.
for (x, y) in [([0xFF; 64], [0xFF; 64]), (p, [0; 64]), ([0; 64], p), ([0; 64], [0; 64]), (p, p)] {
assert_eq!(verify(&key(&x, &y), &digest, &[1u8; 128]), None);
}
// The generator, a real point, with an r or s that is zero, n, or all ones.
for bad in [[0u8; 64], n, [0xFF; 64]] {
assert_eq!(verify(&key(&gx, &gy), &digest, &[bad.to_vec(), vec![1; 64]].concat()), Some(false));
assert_eq!(verify(&key(&gx, &gy), &digest, &[vec![1; 64], bad.to_vec()].concat()), Some(false));
}
}
}
+767
View File
@@ -0,0 +1,767 @@
//! Digests, public keys (verification everywhere) and private keys (signing).
//!
//! Verification uses RustCrypto. RSA private-key operations use `aws-lc-rs` on native targets
//! (constant-time: the `rsa` crate is never used for private keys there). ECDSA
//! signing uses RustCrypto with deterministic nonces (RFC 6979).
use zeroize::Zeroizing;
use crate::Error;
use crate::der::{self, Tlv, tag};
pub mod oid {
pub const SHA1: &str = "1.3.14.3.2.26";
pub const SHA224: &str = "2.16.840.1.101.3.4.2.4";
pub const SHA256: &str = "2.16.840.1.101.3.4.2.1";
pub const SHA384: &str = "2.16.840.1.101.3.4.2.2";
pub const SHA512: &str = "2.16.840.1.101.3.4.2.3";
pub const SHA512_224: &str = "2.16.840.1.101.3.4.2.5";
pub const SHA512_256: &str = "2.16.840.1.101.3.4.2.6";
pub const SHA3_224: &str = "2.16.840.1.101.3.4.2.7";
pub const SHA3_256: &str = "2.16.840.1.101.3.4.2.8";
pub const SHA3_384: &str = "2.16.840.1.101.3.4.2.9";
pub const SHA3_512: &str = "2.16.840.1.101.3.4.2.10";
pub const RIPEMD160: &str = "1.3.36.3.2.1";
pub const RSA: &str = "1.2.840.113549.1.1.1";
pub const RSA_PSS: &str = "1.2.840.113549.1.1.10";
pub const MGF1: &str = "1.2.840.113549.1.1.8";
pub const EC: &str = "1.2.840.10045.2.1";
pub const P256: &str = "1.2.840.10045.3.1.7";
pub const P384: &str = "1.3.132.0.34";
pub const P521: &str = "1.3.132.0.35";
pub const BRAINPOOL_P256: &str = "1.3.36.3.3.2.8.1.1.7";
pub const BRAINPOOL_P384: &str = "1.3.36.3.3.2.8.1.1.11";
pub const BRAINPOOL_P512: &str = "1.3.36.3.3.2.8.1.1.13";
pub const ED25519: &str = "1.3.101.112";
}
/// Message digests.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DigestAlg {
Sha1,
Sha224,
Sha256,
Sha384,
Sha512,
Sha512_224,
Sha512_256,
Sha3_224,
Sha3_256,
Sha3_384,
Sha3_512,
Ripemd160,
}
impl DigestAlg {
/// Every digest, for tables.
pub const ALL: [DigestAlg; 12] = [
DigestAlg::Sha1,
DigestAlg::Sha224,
DigestAlg::Sha256,
DigestAlg::Sha384,
DigestAlg::Sha512,
DigestAlg::Sha512_224,
DigestAlg::Sha512_256,
DigestAlg::Sha3_224,
DigestAlg::Sha3_256,
DigestAlg::Sha3_384,
DigestAlg::Sha3_512,
DigestAlg::Ripemd160,
];
pub fn from_oid(o: &str) -> Option<DigestAlg> {
DigestAlg::ALL.into_iter().find(|d| d.oid() == o)
}
pub fn oid(self) -> &'static str {
match self {
DigestAlg::Sha1 => oid::SHA1,
DigestAlg::Sha224 => oid::SHA224,
DigestAlg::Sha256 => oid::SHA256,
DigestAlg::Sha384 => oid::SHA384,
DigestAlg::Sha512 => oid::SHA512,
DigestAlg::Sha512_224 => oid::SHA512_224,
DigestAlg::Sha512_256 => oid::SHA512_256,
DigestAlg::Sha3_224 => oid::SHA3_224,
DigestAlg::Sha3_256 => oid::SHA3_256,
DigestAlg::Sha3_384 => oid::SHA3_384,
DigestAlg::Sha3_512 => oid::SHA3_512,
DigestAlg::Ripemd160 => oid::RIPEMD160,
}
}
pub fn name(self) -> &'static str {
match self {
DigestAlg::Sha1 => "SHA-1",
DigestAlg::Sha224 => "SHA-224",
DigestAlg::Sha256 => "SHA-256",
DigestAlg::Sha384 => "SHA-384",
DigestAlg::Sha512 => "SHA-512",
DigestAlg::Sha512_224 => "SHA-512/224",
DigestAlg::Sha512_256 => "SHA-512/256",
DigestAlg::Sha3_224 => "SHA3-224",
DigestAlg::Sha3_256 => "SHA3-256",
DigestAlg::Sha3_384 => "SHA3-384",
DigestAlg::Sha3_512 => "SHA3-512",
DigestAlg::Ripemd160 => "RIPEMD-160",
}
}
/// The digest of the concatenation of `parts`.
pub fn digest(self, parts: &[&[u8]]) -> Vec<u8> {
fn sha2_run<D: sha2::Digest>(parts: &[&[u8]]) -> Vec<u8> {
let mut h = D::new();
for p in parts {
h.update(p);
}
h.finalize().to_vec()
}
fn sha3_run<D: sha3::Digest>(parts: &[&[u8]]) -> Vec<u8> {
let mut h = D::new();
for p in parts {
h.update(p);
}
h.finalize().to_vec()
}
match self {
DigestAlg::Sha1 => sha2_run::<sha1::Sha1>(parts),
DigestAlg::Sha224 => sha2_run::<sha2::Sha224>(parts),
DigestAlg::Sha256 => sha2_run::<sha2::Sha256>(parts),
DigestAlg::Sha384 => sha2_run::<sha2::Sha384>(parts),
DigestAlg::Sha512 => sha2_run::<sha2::Sha512>(parts),
DigestAlg::Sha512_224 => sha2_run::<sha2::Sha512_224>(parts),
DigestAlg::Sha512_256 => sha2_run::<sha2::Sha512_256>(parts),
DigestAlg::Sha3_224 => sha3_run::<sha3::Sha3_224>(parts),
DigestAlg::Sha3_256 => sha3_run::<sha3::Sha3_256>(parts),
DigestAlg::Sha3_384 => sha3_run::<sha3::Sha3_384>(parts),
DigestAlg::Sha3_512 => sha3_run::<sha3::Sha3_512>(parts),
DigestAlg::Ripemd160 => {
use ripemd::Digest;
let mut h = ripemd::Ripemd160::new(); // NOSONAR: a digest CMS signatures name; validated, and reported as weak by `weakness`
for p in parts {
h.update(p);
}
h.finalize().to_vec()
}
}
}
/// What to tell the user when a signature that still validates uses this digest, or `None`
/// for one that is fine. Old documents carry SHA-1 and RIPEMD-160, so they validate, but a
/// signature panel that showed them like a SHA-256 one would overstate what they prove.
pub fn weakness(self) -> Option<&'static str> {
match self {
DigestAlg::Sha1 => Some(
"This signature uses SHA-1, a weak hash algorithm: collisions in it have been demonstrated. It still validates because many documents signed years ago use it, but it should not be relied on.",
),
DigestAlg::Ripemd160 => Some(
"This signature uses RIPEMD-160, a legacy 160-bit hash algorithm that is no longer recommended for signatures. It still validates, but it should not be relied on.",
),
_ => None,
}
}
/// The digest length in bytes.
pub fn output_len(self) -> usize {
match self {
DigestAlg::Sha1 | DigestAlg::Ripemd160 => 20,
DigestAlg::Sha224 | DigestAlg::Sha512_224 | DigestAlg::Sha3_224 => 28,
DigestAlg::Sha256 | DigestAlg::Sha512_256 | DigestAlg::Sha3_256 => 32,
DigestAlg::Sha384 | DigestAlg::Sha3_384 => 48,
DigestAlg::Sha512 | DigestAlg::Sha3_512 => 64,
}
}
/// The `AlgorithmIdentifier` (no parameters, as RFC 5754 recommends).
pub fn algorithm(self) -> Vec<u8> {
der::algorithm(self.oid(), None)
}
}
/// RSASSA-PSS parameters (RFC 4055 §3.1) besides the message digest.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PssParams {
/// The MGF1 hash.
pub mgf: DigestAlg,
/// The declared salt length; `None` recovers it from the signature.
pub salt_len: Option<usize>,
}
/// How a signature value is computed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Scheme {
RsaPkcs1,
RsaPss(PssParams),
/// ECDSA over a digest, the signature DER or plain `r ‖ s`.
Ecdsa,
/// EdDSA over the message itself (RFC 8419), not over a digest.
Ed25519,
}
/// Signature algorithm OIDs that name both the scheme and the digest.
const SIGNATURE_OIDS: &[(&str, Scheme, DigestAlg)] = &[
("1.2.840.113549.1.1.5", Scheme::RsaPkcs1, DigestAlg::Sha1),
("1.2.840.113549.1.1.14", Scheme::RsaPkcs1, DigestAlg::Sha224),
("1.2.840.113549.1.1.11", Scheme::RsaPkcs1, DigestAlg::Sha256),
("1.2.840.113549.1.1.12", Scheme::RsaPkcs1, DigestAlg::Sha384),
("1.2.840.113549.1.1.13", Scheme::RsaPkcs1, DigestAlg::Sha512),
("1.2.840.113549.1.1.15", Scheme::RsaPkcs1, DigestAlg::Sha512_224),
("1.2.840.113549.1.1.16", Scheme::RsaPkcs1, DigestAlg::Sha512_256),
("2.16.840.1.101.3.4.3.13", Scheme::RsaPkcs1, DigestAlg::Sha3_224),
("2.16.840.1.101.3.4.3.14", Scheme::RsaPkcs1, DigestAlg::Sha3_256),
("2.16.840.1.101.3.4.3.15", Scheme::RsaPkcs1, DigestAlg::Sha3_384),
("2.16.840.1.101.3.4.3.16", Scheme::RsaPkcs1, DigestAlg::Sha3_512),
("1.3.36.3.3.1.2", Scheme::RsaPkcs1, DigestAlg::Ripemd160),
("1.2.840.10045.4.1", Scheme::Ecdsa, DigestAlg::Sha1),
("1.2.840.10045.4.3.1", Scheme::Ecdsa, DigestAlg::Sha224),
("1.2.840.10045.4.3.2", Scheme::Ecdsa, DigestAlg::Sha256),
("1.2.840.10045.4.3.3", Scheme::Ecdsa, DigestAlg::Sha384),
("1.2.840.10045.4.3.4", Scheme::Ecdsa, DigestAlg::Sha512),
("2.16.840.1.101.3.4.3.9", Scheme::Ecdsa, DigestAlg::Sha3_224),
("2.16.840.1.101.3.4.3.10", Scheme::Ecdsa, DigestAlg::Sha3_256),
("2.16.840.1.101.3.4.3.11", Scheme::Ecdsa, DigestAlg::Sha3_384),
("2.16.840.1.101.3.4.3.12", Scheme::Ecdsa, DigestAlg::Sha3_512),
// BSI TR-03111 "plain" ECDSA (r ‖ s), as German signature cards write it.
("0.4.0.127.0.7.1.1.4.1.1", Scheme::Ecdsa, DigestAlg::Sha1),
("0.4.0.127.0.7.1.1.4.1.2", Scheme::Ecdsa, DigestAlg::Sha224),
("0.4.0.127.0.7.1.1.4.1.3", Scheme::Ecdsa, DigestAlg::Sha256),
("0.4.0.127.0.7.1.1.4.1.4", Scheme::Ecdsa, DigestAlg::Sha384),
("0.4.0.127.0.7.1.1.4.1.5", Scheme::Ecdsa, DigestAlg::Sha512),
("0.4.0.127.0.7.1.1.4.1.6", Scheme::Ecdsa, DigestAlg::Ripemd160),
];
/// A signature `AlgorithmIdentifier`: the scheme and, when the identifier names one, the digest.
pub fn signature_algorithm(alg: &Tlv<'_>) -> Result<(Scheme, Option<DigestAlg>), Error> {
let parts = alg.children()?;
let o = parts.first().ok_or_else(|| Error::Malformed("empty algorithm".into()))?.oid()?;
if let Some((_, scheme, digest)) = SIGNATURE_OIDS.iter().find(|(k, _, _)| *k == o) {
return Ok((*scheme, Some(*digest)));
}
Ok(match o.as_str() {
oid::RSA => (Scheme::RsaPkcs1, None),
oid::EC => (Scheme::Ecdsa, None),
oid::ED25519 => (Scheme::Ed25519, None),
oid::RSA_PSS => {
let (hash, params) = pss_params(parts.get(1))?;
(Scheme::RsaPss(params), Some(hash))
}
other => return Err(Error::Unsupported(format!("signature algorithm {other}"))),
})
}
/// `RSASSA-PSS-params` (RFC 4055): `[0]` hash (default SHA-1), `[1]` mask generation function
/// (default MGF1 with SHA-1), `[2]` salt length (default 20). Absent parameters mean the defaults.
fn pss_params(params: Option<&Tlv<'_>>) -> Result<(DigestAlg, PssParams), Error> {
let hash_of = |alg: &Tlv<'_>| -> Result<DigestAlg, Error> {
let o = alg.children()?.first().ok_or_else(|| Error::Malformed("PSS hash".into()))?.oid()?;
DigestAlg::from_oid(&o).ok_or_else(|| Error::Unsupported(format!("digest {o}")))
};
let mut hash = DigestAlg::Sha1;
let mut mgf = DigestAlg::Sha1;
let mut salt_len = 20usize;
if let Some(p) = params.filter(|p| p.tag == tag::SEQUENCE) {
for field in p.children()? {
match field.tag {
t if t == tag::ctx(0) => hash = hash_of(&field.inner()?)?,
t if t == tag::ctx(1) => {
let m = field.inner()?.children()?;
if m.first().map(|o| o.oid()).transpose()?.as_deref() != Some(oid::MGF1) {
return Err(Error::Unsupported("PSS mask generation function other than MGF1".into()));
}
mgf = hash_of(m.get(1).ok_or_else(|| Error::Malformed("MGF1 hash".into()))?)?;
}
t if t == tag::ctx(2) => salt_len = usize::try_from(field.inner()?.u64()?).map_err(|_| Error::Malformed("PSS salt length".into()))?,
_ => {}
}
}
}
Ok((hash, PssParams { mgf, salt_len: Some(salt_len) }))
}
/// The length in bits of the RSA modulus `n` (big-endian; leading zero bytes don't count).
fn rsa_bits(n: &[u8]) -> usize {
let n = n.iter().position(|b| *b != 0).and_then(|i| n.get(i..)).unwrap_or_default();
n.first().map_or(0, |b| n.len() * 8 - b.leading_zeros() as usize)
}
/// A public key from a `SubjectPublicKeyInfo`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PublicKey {
Rsa {
n: Vec<u8>,
e: Vec<u8>,
},
P256(Vec<u8>),
P384(Vec<u8>),
/// Verification only (no signing).
P521(Vec<u8>),
BrainpoolP256(Vec<u8>),
BrainpoolP384(Vec<u8>),
BrainpoolP512(Vec<u8>),
/// The 32-byte public key (RFC 8032). Signs the message itself, see [`Scheme::Ed25519`].
Ed25519(Vec<u8>),
/// A key algorithm or curve this crate can't check signatures with. The certificate still
/// parses (so its name and dates show); verifying with it gives [`Error::Unsupported`].
Unsupported {
what: String,
spki: Vec<u8>,
},
}
/// The private key a platform key store (the Windows certificate store, through CNG) must
/// hold to sign for a certificate, see [`PublicKey::store_signing_key`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StoreKey {
Rsa,
/// ECDSA with a key of this many bits.
Ecdsa(u32),
}
impl PublicKey {
/// The `subjectPublicKey` BIT STRING contents — what OCSP's `issuerKeyHash` covers
/// (RFC 6960 §4.1.1). RSA keys are re-encoded; EC points are the stored bytes.
pub fn key_bits(&self) -> Vec<u8> {
match self {
PublicKey::Rsa { n, e } => der::seq(&[&der::uint(n), &der::uint(e)]),
PublicKey::P256(bits)
| PublicKey::P384(bits)
| PublicKey::P521(bits)
| PublicKey::BrainpoolP256(bits)
| PublicKey::BrainpoolP384(bits)
| PublicKey::BrainpoolP512(bits)
| PublicKey::Ed25519(bits) => bits.clone(),
// The BIT STRING of the stored SubjectPublicKeyInfo (empty if it doesn't parse).
PublicKey::Unsupported { spki, .. } => Tlv::parse(spki)
.ok()
.and_then(|(t, _)| t.children().ok())
.and_then(|c| c.get(1).and_then(|k| k.bits().ok().map(<[u8]>::to_vec)))
.unwrap_or_default(),
}
}
pub fn from_spki(spki: &Tlv<'_>) -> Result<PublicKey, Error> {
let parts = spki.children()?;
let [alg, key] = parts.as_slice() else { return Err(Error::Malformed("SubjectPublicKeyInfo".into())) };
let alg = alg.children()?;
let o = alg.first().ok_or_else(|| Error::Malformed("key algorithm".into()))?.oid()?;
let bits = key.bits()?;
match o.as_str() {
oid::ED25519 => Ok(PublicKey::Ed25519(bits.to_vec())),
oid::RSA => {
let k = Tlv::parse_all(bits)?.children()?;
let [n, e] = k.as_slice() else { return Err(Error::Malformed("RSAPublicKey".into())) };
Ok(PublicKey::Rsa { n: n.uint_bytes().to_vec(), e: e.uint_bytes().to_vec() })
}
oid::EC => match alg.get(1).map(|c| c.oid()).transpose()?.as_deref() {
Some(oid::P256) => Ok(PublicKey::P256(bits.to_vec())),
Some(oid::P384) => Ok(PublicKey::P384(bits.to_vec())),
Some(oid::P521) => Ok(PublicKey::P521(bits.to_vec())),
Some(oid::BRAINPOOL_P256) => Ok(PublicKey::BrainpoolP256(bits.to_vec())),
Some(oid::BRAINPOOL_P384) => Ok(PublicKey::BrainpoolP384(bits.to_vec())),
Some(oid::BRAINPOOL_P512) => Ok(PublicKey::BrainpoolP512(bits.to_vec())),
other => Ok(PublicKey::Unsupported { what: format!("elliptic curve {}", other.unwrap_or("?")), spki: spki.raw.to_vec() }),
},
other => Ok(PublicKey::Unsupported { what: format!("public key algorithm {other}"), spki: spki.raw.to_vec() }),
}
}
/// The `SubjectPublicKeyInfo` encoding.
pub fn spki(&self) -> Vec<u8> {
match self {
PublicKey::Rsa { n, e } => {
let rsa = der::seq(&[&der::uint(n), &der::uint(e)]);
der::seq(&[&der::algorithm(oid::RSA, Some(&der::null())), &der::bit_string(&rsa)])
}
PublicKey::P256(p) => der::seq(&[&der::seq(&[&der::oid(oid::EC), &der::oid(oid::P256)]), &der::bit_string(p)]),
PublicKey::P384(p) => der::seq(&[&der::seq(&[&der::oid(oid::EC), &der::oid(oid::P384)]), &der::bit_string(p)]),
PublicKey::P521(p) => der::seq(&[&der::seq(&[&der::oid(oid::EC), &der::oid(oid::P521)]), &der::bit_string(p)]),
PublicKey::BrainpoolP256(p) => der::seq(&[&der::seq(&[&der::oid(oid::EC), &der::oid(oid::BRAINPOOL_P256)]), &der::bit_string(p)]),
PublicKey::BrainpoolP384(p) => der::seq(&[&der::seq(&[&der::oid(oid::EC), &der::oid(oid::BRAINPOOL_P384)]), &der::bit_string(p)]),
PublicKey::BrainpoolP512(p) => der::seq(&[&der::seq(&[&der::oid(oid::EC), &der::oid(oid::BRAINPOOL_P512)]), &der::bit_string(p)]),
PublicKey::Ed25519(p) => der::seq(&[&der::seq(&[&der::oid(oid::ED25519)]), &der::bit_string(p)]),
PublicKey::Unsupported { spki, .. } => spki.clone(),
}
}
/// "RSA 2048-bit", "ECDSA P-256"…
pub fn describe(&self) -> String {
match self {
PublicKey::Rsa { n, .. } => format!("RSA {}-bit", rsa_bits(n)),
PublicKey::P256(_) => "ECDSA P-256".into(),
PublicKey::P384(_) => "ECDSA P-384".into(),
PublicKey::P521(_) => "ECDSA P-521".into(),
PublicKey::BrainpoolP256(_) => "ECDSA brainpoolP256r1".into(),
PublicKey::BrainpoolP384(_) => "ECDSA brainpoolP384r1".into(),
PublicKey::BrainpoolP512(_) => "ECDSA brainpoolP512r1".into(),
PublicKey::Ed25519(_) => "Ed25519".into(),
PublicKey::Unsupported { what, .. } => format!("unsupported ({what})"),
}
}
// The two decisions below are what a Windows key-store caller asks of a key. They live here, with every
// variant named and no `_` arm, so that a new variant fails to compile on every platform
// rather than only in a Windows-only caller.
/// The key a platform key store must hold to sign for this public key, or `None` when
/// this crate doesn't sign with this kind of key (it only verifies P-521, brainpool and
/// Ed25519 signatures), so the store's identity is skipped.
pub fn store_signing_key(&self) -> Option<StoreKey> {
match self {
PublicKey::Rsa { .. } => Some(StoreKey::Rsa),
PublicKey::P256(_) => Some(StoreKey::Ecdsa(256)),
PublicKey::P384(_) => Some(StoreKey::Ecdsa(384)),
PublicKey::P521(_)
| PublicKey::BrainpoolP256(_)
| PublicKey::BrainpoolP384(_)
| PublicKey::BrainpoolP512(_)
| PublicKey::Ed25519(_)
| PublicKey::Unsupported { .. } => None,
}
}
/// A key store's signature as a CMS signature carries it: RSA unchanged, ECDSA's fixed-size
/// `r ‖ s` (what CNG returns) as a DER `ECDSA-Sig-Value`. Keys [`Self::store_signing_key`]
/// rejects give [`Error::Unsupported`].
pub fn store_signature_der(&self, raw: &[u8]) -> Result<Vec<u8>, Error> {
match self {
PublicKey::Rsa { .. } => Ok(raw.to_vec()),
PublicKey::P256(_) => p256::ecdsa::Signature::from_slice(raw)
.map(|s| s.to_der().as_bytes().to_vec())
.map_err(|e| Error::Crypto(format!("the key store returned an invalid P-256 signature: {e}"))),
PublicKey::P384(_) => p384::ecdsa::Signature::from_slice(raw)
.map(|s| s.to_der().as_bytes().to_vec())
.map_err(|e| Error::Crypto(format!("the key store returned an invalid P-384 signature: {e}"))),
PublicKey::P521(_)
| PublicKey::BrainpoolP256(_)
| PublicKey::BrainpoolP384(_)
| PublicKey::BrainpoolP512(_)
| PublicKey::Ed25519(_)
| PublicKey::Unsupported { .. } => Err(Error::Unsupported(format!("signing with {} keys", self.describe()))),
}
}
/// Check `sig` over a message whose digest (with `alg`) is `digest`. Encodings are read
/// leniently (see [`crate::rsa_pad`], [`ecdsa_rs`]); the cryptographic check never is.
/// `Err(Unsupported)` means "can't tell", as opposed to `Ok(false)`: "doesn't match".
pub fn verify(&self, scheme: Scheme, alg: DigestAlg, digest: &[u8], sig: &[u8]) -> Result<bool, Error> {
self.verify_noting(scheme, alg, digest, sig, &mut Vec::new())
}
/// [`PublicKey::verify`], noting in `notes` each encoding irregularity it tolerated, and a key
/// too small to rely on ([`PublicKey::weakness`]). Only meaningful when it returns `Ok(true)`:
/// a signature that doesn't verify may leave notes about how far it got.
pub fn verify_noting(&self, scheme: Scheme, alg: DigestAlg, digest: &[u8], sig: &[u8], notes: &mut Vec<&'static str>) -> Result<bool, Error> {
let verified = self.verify_scheme(scheme, alg, digest, sig, notes);
if let Ok(true) = verified
&& let Some(weak) = self.weakness()
{
notes.push(weak);
}
verified
}
/// What to tell the user when a signature that still validates was made with this key, or
/// `None` for one that is fine, as [`DigestAlg::weakness`] does for hashes.
pub fn weakness(&self) -> Option<&'static str> {
match self {
PublicKey::Rsa { n, .. } if rsa_bits(n) < 2048 => Some(
"This signature uses an RSA key shorter than 2048 bits, which is no longer considered strong enough for signatures. It still validates, but it should not be relied on.",
),
_ => None,
}
}
fn verify_scheme(&self, scheme: Scheme, alg: DigestAlg, digest: &[u8], sig: &[u8], notes: &mut Vec<&'static str>) -> Result<bool, Error> {
match (self, scheme) {
(PublicKey::Unsupported { what, .. }, _) => Err(Error::Unsupported(what.clone())),
(PublicKey::Rsa { n, e }, Scheme::RsaPkcs1) => crate::rsa_pad::pkcs1_verify(n, e, alg, digest, sig, notes),
(PublicKey::Rsa { n, e }, Scheme::RsaPss(params)) => crate::rsa_pad::pss_verify(n, e, alg, params, digest, sig, notes),
(PublicKey::Ed25519(_), _) => Err(Error::Unsupported("Ed25519 signs the message, not a digest".into())),
(PublicKey::P256(p), Scheme::Ecdsa) => {
use p256::ecdsa::signature::hazmat::PrehashVerifier;
let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(p).map_err(|_| Error::Malformed("P-256 key".into()))?;
let Some(s) = ecdsa_rs(sig, 32, notes).and_then(|rs| p256::ecdsa::Signature::from_slice(&rs).ok()) else { return Ok(false) };
Ok(key.verify_prehash(digest, &s).is_ok())
}
(PublicKey::P384(p), Scheme::Ecdsa) => {
use p384::ecdsa::signature::hazmat::PrehashVerifier;
let key = p384::ecdsa::VerifyingKey::from_sec1_bytes(p).map_err(|_| Error::Malformed("P-384 key".into()))?;
let Some(s) = ecdsa_rs(sig, 48, notes).and_then(|rs| p384::ecdsa::Signature::from_slice(&rs).ok()) else { return Ok(false) };
Ok(key.verify_prehash(digest, &s).is_ok())
}
(PublicKey::P521(p), Scheme::Ecdsa) => {
use p521::ecdsa::signature::hazmat::PrehashVerifier;
let key = p521::ecdsa::VerifyingKey::from_sec1_bytes(p).map_err(|_| Error::Malformed("P-521 key".into()))?;
let Some(s) = ecdsa_rs(sig, 66, notes).and_then(|rs| p521::ecdsa::Signature::from_slice(&rs).ok()) else { return Ok(false) };
Ok(key.verify_prehash(digest, &s).is_ok())
}
(PublicKey::BrainpoolP256(p), Scheme::Ecdsa) => {
use ecdsa::signature::hazmat::PrehashVerifier;
type Curve = bp256::BrainpoolP256r1;
let key = ecdsa::VerifyingKey::<Curve>::from_sec1_bytes(p).map_err(|_| Error::Malformed("brainpoolP256r1 key".into()))?;
let Some(s) = ecdsa_rs(sig, 32, notes).and_then(|rs| ecdsa::Signature::<Curve>::from_slice(&rs).ok()) else { return Ok(false) };
Ok(key.verify_prehash(digest, &s).is_ok())
}
(PublicKey::BrainpoolP384(p), Scheme::Ecdsa) => {
use ecdsa::signature::hazmat::PrehashVerifier;
type Curve = bp384::BrainpoolP384r1;
let key = ecdsa::VerifyingKey::<Curve>::from_sec1_bytes(p).map_err(|_| Error::Malformed("brainpoolP384r1 key".into()))?;
let Some(s) = ecdsa_rs(sig, 48, notes).and_then(|rs| ecdsa::Signature::<Curve>::from_slice(&rs).ok()) else { return Ok(false) };
Ok(key.verify_prehash(digest, &s).is_ok())
}
(PublicKey::BrainpoolP512(p), Scheme::Ecdsa) => {
let Some(rs) = ecdsa_rs(sig, 64, notes) else { return Ok(false) };
crate::ec512::verify(p, digest, &rs).ok_or_else(|| Error::Malformed("brainpoolP512r1 key or signature".into()))
}
_ => Ok(false),
}
}
/// Check `sig` over `msg` itself, for the schemes that don't sign a digest (Ed25519,
/// RFC 8419: with signed attributes, the message is their DER encoding).
pub fn verify_message(&self, scheme: Scheme, msg: &[u8], sig: &[u8]) -> Result<bool, Error> {
match (self, scheme) {
(PublicKey::Unsupported { what, .. }, _) => Err(Error::Unsupported(what.clone())),
(PublicKey::Ed25519(k), Scheme::Ed25519) => {
let bytes: [u8; 32] = k.as_slice().try_into().map_err(|_| Error::Malformed("Ed25519 key".into()))?;
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(|_| Error::Malformed("Ed25519 key".into()))?;
let Ok(sig) = ed25519_dalek::Signature::from_slice(sig) else { return Ok(false) };
Ok(key.verify_strict(msg, &sig).is_ok())
}
_ => Ok(false),
}
}
}
/// An ECDSA signature as the fixed-width `r ‖ s` the curve crates read: from the standard DER
/// `SEQUENCE { INTEGER r, INTEGER s }` (also with integers that have redundant leading zeros or
/// the sign byte missing), or already raw `r ‖ s` as PKCS #11 tokens return it. `notes` gets an
/// entry when the signature was not canonical DER.
fn ecdsa_rs(sig: &[u8], field_len: usize, notes: &mut Vec<&'static str>) -> Option<Vec<u8>> {
let pad = |int: &[u8]| -> Option<Vec<u8>> {
let int = int.get(int.iter().position(|b| *b != 0).unwrap_or(int.len())..).unwrap_or_default();
let zeros = field_len.checked_sub(int.len())?;
Some(std::iter::repeat_n(0u8, zeros).chain(int.iter().copied()).collect())
};
if let Ok((t, rest)) = Tlv::parse(sig)
&& t.tag == tag::SEQUENCE
&& rest.iter().all(|b| *b == 0)
&& let Ok(parts) = t.children()
&& let [r, s] = parts.as_slice()
&& r.tag == tag::INTEGER
&& s.tag == tag::INTEGER
{
// Canonical DER: nothing after the sequence, minimal positive integers.
let minimal = |i: &Tlv<'_>| i.value.first().is_some_and(|b| b & 0x80 == 0) && !matches!(i.value, [0, b, ..] if b & 0x80 == 0);
if !rest.is_empty() || !minimal(r) || !minimal(s) {
notes.push("The ECDSA signature is not in canonical DER (trailing bytes, padding or a missing sign byte); it was read anyway.");
}
let mut out = pad(r.value)?;
out.extend(pad(s.value)?);
return Some(out);
}
(sig.len() == field_len * 2).then(|| {
notes.push("The ECDSA signature is the raw r and s values instead of a DER sequence; it was read anyway.");
sig.to_vec()
})
}
enum Inner {
#[cfg(not(target_arch = "wasm32"))]
Rsa(aws_lc_rs::signature::RsaKeyPair),
#[cfg(target_arch = "wasm32")]
Rsa,
P256(p256::ecdsa::SigningKey),
P384(p384::ecdsa::SigningKey),
/// A key held elsewhere (the macOS Keychain, a token) that signs on request.
External(std::sync::Arc<dyn ExternalKey>),
}
/// A private key this crate can't read, only ask to sign (OS key stores, tokens).
pub trait ExternalKey: Send + Sync {
/// Sign `msg`, hashing it with `alg` (PKCS #1 v1.5 for RSA, DER-encoded ECDSA).
fn sign(&self, alg: DigestAlg, msg: &[u8]) -> Result<Vec<u8>, Error>;
}
/// A private key that can sign.
pub struct PrivateKey {
inner: Inner,
public: PublicKey,
/// The PKCS#8 `PrivateKeyInfo` it came from (to write it back into a PKCS#12 file).
pub(crate) pkcs8: Zeroizing<Vec<u8>>,
}
impl std::fmt::Debug for PrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PrivateKey({})", self.public.describe())
}
}
impl PrivateKey {
/// A key held outside this process's memory (its public half is `public`). It can't be
/// exported to a PKCS #12 file.
pub fn external(public: PublicKey, key: std::sync::Arc<dyn ExternalKey>) -> PrivateKey {
PrivateKey { inner: Inner::External(key), public, pkcs8: Zeroizing::new(Vec::new()) }
}
/// Whether the key lives outside the app (and so can't be exported).
pub fn is_external(&self) -> bool {
matches!(self.inner, Inner::External(_))
}
/// From a PKCS#8 `PrivateKeyInfo`.
pub fn from_pkcs8(info: &[u8]) -> Result<PrivateKey, Error> {
let t = Tlv::parse_all(info)?.children()?;
let [_version, alg, key, ..] = t.as_slice() else { return Err(Error::Malformed("PrivateKeyInfo".into())) };
let alg = alg.children()?;
let o = alg.first().ok_or_else(|| Error::Malformed("key algorithm".into()))?.oid()?;
let key = key.expect(tag::OCTET_STRING, "private key")?;
match o.as_str() {
oid::RSA => {
let k = Tlv::parse_all(key.value)?.children()?;
let (n, e) = match k.as_slice() {
[_, n, e, ..] => (n.uint_bytes().to_vec(), e.uint_bytes().to_vec()),
_ => return Err(Error::Malformed("RSAPrivateKey".into())),
};
#[cfg(not(target_arch = "wasm32"))]
let inner = Inner::Rsa(
aws_lc_rs::signature::RsaKeyPair::from_pkcs8(info)
.or_else(|_| aws_lc_rs::signature::RsaKeyPair::from_der(key.value))
.map_err(|e| Error::Malformed(format!("RSA private key: {e}")))?,
);
#[cfg(target_arch = "wasm32")]
let inner = Inner::Rsa;
Ok(PrivateKey { inner, public: PublicKey::Rsa { n, e }, pkcs8: Zeroizing::new(info.to_vec()) })
}
oid::EC => {
// ECPrivateKey: version, privateKey OCTET STRING, [0] parameters, [1] publicKey.
let ec = Tlv::parse_all(key.value)?.children()?;
let d = ec.get(1).ok_or_else(|| Error::Malformed("ECPrivateKey".into()))?.expect(tag::OCTET_STRING, "EC private key")?.value;
let curve = match alg.get(1).map(|c| c.oid()).transpose()? {
Some(c) => c,
None => ec
.iter()
.find(|t| t.tag == tag::ctx(0))
.and_then(|p| p.inner().ok())
.and_then(|p| p.oid().ok())
.ok_or_else(|| Error::Malformed("EC curve".into()))?,
};
match curve.as_str() {
oid::P256 => {
let k = p256::ecdsa::SigningKey::from_slice(d).map_err(|_| Error::Malformed("P-256 key".into()))?;
let public = PublicKey::P256(k.verifying_key().to_sec1_point(false).as_bytes().to_vec());
Ok(PrivateKey { inner: Inner::P256(k), public, pkcs8: Zeroizing::new(info.to_vec()) })
}
oid::P384 => {
let k = p384::ecdsa::SigningKey::from_slice(d).map_err(|_| Error::Malformed("P-384 key".into()))?;
let public = PublicKey::P384(k.verifying_key().to_sec1_point(false).as_bytes().to_vec());
Ok(PrivateKey { inner: Inner::P384(k), public, pkcs8: Zeroizing::new(info.to_vec()) })
}
other => Err(Error::Unsupported(format!("elliptic curve {other}"))),
}
}
other => Err(Error::Unsupported(format!("private key algorithm {other}"))),
}
}
/// A new RSA key (Acrobat's default for a self-signed digital ID is 2048-bit RSA).
#[cfg(not(target_arch = "wasm32"))]
pub fn generate_rsa(bits: usize) -> Result<PrivateKey, Error> {
use aws_lc_rs::encoding::AsDer;
use aws_lc_rs::rsa::KeySize;
let size = match bits {
2048 => KeySize::Rsa2048,
3072 => KeySize::Rsa3072,
4096 => KeySize::Rsa4096,
_ => return Err(Error::Unsupported(format!("{bits}-bit RSA keys"))),
};
let kp = aws_lc_rs::signature::RsaKeyPair::generate(size).map_err(|_| Error::Crypto("RSA key generation failed".into()))?;
let der = kp.as_der().map_err(|_| Error::Crypto("RSA key encoding failed".into()))?;
PrivateKey::from_pkcs8(der.as_ref())
}
/// RSA keys are generated only on native targets.
#[cfg(target_arch = "wasm32")]
pub fn generate_rsa(_bits: usize) -> Result<PrivateKey, Error> {
Err(Error::Unsupported("creating RSA keys in the browser (use a P-256 key)".into()))
}
/// A new P-256 key.
pub fn generate_p256() -> Result<PrivateKey, Error> {
loop {
let mut d = Zeroizing::new([0u8; 32]);
getrandom::fill(&mut *d).map_err(|e| Error::Crypto(format!("no randomness: {e}")))?;
if let Ok(k) = p256::ecdsa::SigningKey::from_slice(&*d) {
let point = k.verifying_key().to_sec1_point(false).as_bytes().to_vec();
// PKCS#8 with an ECPrivateKey (RFC 5915) carrying the public key.
let d_octets = Zeroizing::new(der::octets(&*d));
let ec = Zeroizing::new(der::seq(&[&der::int(1), &d_octets, &der::explicit(1, &der::bit_string(&point))]));
let ec_octets = Zeroizing::new(der::octets(&ec));
let info = Zeroizing::new(der::seq(&[&der::int(0), &der::seq(&[&der::oid(oid::EC), &der::oid(oid::P256)]), &ec_octets]));
return PrivateKey::from_pkcs8(&info);
}
}
}
pub fn public_key(&self) -> &PublicKey {
&self.public
}
pub fn pkcs8(&self) -> &[u8] {
&self.pkcs8
}
/// The signature `AlgorithmIdentifier` this key writes with `alg`.
pub fn signature_algorithm(&self, alg: DigestAlg) -> Vec<u8> {
let rsa = matches!(self.public, PublicKey::Rsa { .. });
let scheme = if rsa { Scheme::RsaPkcs1 } else { Scheme::Ecdsa };
// `sign` only writes SHA-256/384/512, which the table has for both schemes.
let oid = SIGNATURE_OIDS
.iter()
.find(|(_, s, d)| *s == scheme && *d == alg)
.map_or(if rsa { "1.2.840.113549.1.1.11" } else { "1.2.840.10045.4.3.2" }, |(o, _, _)| *o);
der::algorithm(oid, rsa.then(der::null).as_deref())
}
/// The digest a signature with this key should use: SHA-256, or SHA-384 for P-384.
pub fn preferred_digest(&self) -> DigestAlg {
match self.public {
PublicKey::P384(_) => DigestAlg::Sha384,
_ => DigestAlg::Sha256,
}
}
/// Sign `msg` (hashed with `alg`). New signatures never use SHA-1.
pub fn sign(&self, alg: DigestAlg, msg: &[u8]) -> Result<Vec<u8>, Error> {
if !matches!(alg, DigestAlg::Sha256 | DigestAlg::Sha384 | DigestAlg::Sha512) {
return Err(Error::Unsupported(format!("{} for new signatures", alg.name())));
}
match &self.inner {
#[cfg(not(target_arch = "wasm32"))]
Inner::Rsa(kp) => {
use aws_lc_rs::signature::{RSA_PKCS1_SHA256, RSA_PKCS1_SHA384, RSA_PKCS1_SHA512};
let enc: &'static dyn aws_lc_rs::signature::RsaEncoding = match alg {
DigestAlg::Sha384 => &RSA_PKCS1_SHA384,
DigestAlg::Sha512 => &RSA_PKCS1_SHA512,
_ => &RSA_PKCS1_SHA256,
};
let mut sig = vec![0u8; kp.public_modulus_len()];
kp.sign(enc, &aws_lc_rs::rand::SystemRandom::new(), msg, &mut sig).map_err(|_| Error::Crypto("RSA signing failed".into()))?;
Ok(sig)
}
#[cfg(target_arch = "wasm32")]
Inner::Rsa => Err(Error::Unsupported("RSA signing in the browser".into())),
Inner::P256(k) => {
use p256::ecdsa::signature::hazmat::PrehashSigner;
let s: p256::ecdsa::Signature = k.sign_prehash(&alg.digest(&[msg])).map_err(|e| Error::Crypto(e.to_string()))?;
Ok(s.to_der().as_bytes().to_vec())
}
Inner::External(k) => k.sign(alg, msg),
Inner::P384(k) => {
use p384::ecdsa::signature::hazmat::PrehashSigner;
let s: p384::ecdsa::Signature = k.sign_prehash(&alg.digest(&[msg])).map_err(|e| Error::Crypto(e.to_string()))?;
Ok(s.to_der().as_bytes().to_vec())
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
//! The formats of digital signatures: ASN.1 DER, X.509 certificates, CMS SignedData (PKCS #7),
//! PKCS #12 identity files, RFC 3161 timestamps, and CRL / OCSP revocation evidence.
//!
//! - [`der`]: a small BER/DER reader and writer; a [`der::Tlv`] keeps its exact encoding, so
//! signed structures are verified over the bytes that were signed.
//! - [`x509`]: certificates (RFC 5280), the checks along a chain of issuers, certificate files
//! (DER, PEM, PKCS #7 bundles), and a self-signed certificate builder.
//! - [`cms`]: CMS SignedData (RFC 5652), parsed, verified and written (detached, or with the
//! content encapsulated), with RFC 3161 tokens attached as unsigned attributes.
//! - [`pkcs12`]: `.p12` / `.pfx` files (RFC 7292), read and written, with the key-derivation
//! work a file may ask for capped.
//! - [`timestamp`]: RFC 3161 requests, responses and tokens. No transport.
//! - [`revocation`]: CRLs and OCSP responses, parsed, verified and looked up. Nothing is fetched.
//! - [`keys`]: digests, public keys (verification) and private keys (signing).
//!
//! The cryptography is RustCrypto's, and aws-lc-rs's for RSA private-key operations on native
//! targets. Two things are done here because no audited crate offers them, both verification
//! only and on public data: the RSA signature paddings for any digest, MGF1 hash and salt
//! length (over the `rsa` crate's raw public-key operation), and ECDSA on brainpoolP512r1.
//!
//! What a signature means for a document or a message (which bytes it covers, what changed
//! after it, whom the reader trusts) is the caller's.
//!
//! Input is hostile: nesting is bounded, lengths are checked against what is there, and
//! nothing here panics on any bytes.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod cms;
pub mod der;
mod ec512;
pub mod keys;
pub mod pkcs12;
pub mod revocation;
mod rsa_pad;
pub mod timestamp;
pub mod x509;
pub use der::Time;
pub use keys::{DigestAlg, PrivateKey, PublicKey};
pub use pkcs12::DigitalId;
pub use timestamp::{TimestampAuthority, TimestampQuery, TimestampToken};
pub use x509::{Certificate, Name};
/// What went wrong reading, checking or writing one of these formats.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("malformed data: {0}")]
Malformed(String),
#[error("not supported: {0}")]
Unsupported(String),
#[error("the password is incorrect")]
WrongPassword,
#[error("{0}")]
Crypto(String),
}
+561
View File
@@ -0,0 +1,561 @@
//! PKCS #12 digital ID files (RFC 7292): `.p12` / `.pfx`, as Acrobat, OpenSSL, macOS Keychain
//! and Windows export them.
//!
//! Reading supports PBES2 (PBKDF2 with HMAC-SHA-1/256/384/512; AES-128/192/256-CBC or
//! 3DES-CBC) and the legacy PKCS #12 PBE schemes (SHA-1 with 3DES or RC2), and checks the MAC.
//! Files that aren't clean DER still open: trailing whitespace, PEM armour or a bare
//! base64 body.
//! Writing uses PBES2 AES-256-CBC with PBKDF2-HMAC-SHA-256 and an HMAC-SHA-256 MAC, OpenSSL 3's
//! algorithms, at [`WRITE_ROUNDS`] rather than OpenSSL's 2048. Opening stops at a fixed amount
//! of key-derivation work, whatever the file asks for.
//!
//! The envelope is read as BER, not DER: Windows writes `.p12` files with indefinite lengths and
//! the AuthenticatedSafe split into OCTET STRING segments, and nothing in the envelope is signed
//! over its own bytes. The certificates inside it are still read as strict DER, which is how they
//! were signed.
use std::borrow::Cow;
use base64::Engine as _;
use cbc::cipher::block_padding::Pkcs7;
use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt, InnerIvInit, KeyIvInit};
use hmac::Mac;
use zeroize::Zeroizing;
use crate::Error;
use crate::der::{self, Tlv, tag};
use crate::keys::{DigestAlg, PrivateKey};
use crate::x509::Certificate;
const DATA: &str = "1.2.840.113549.1.7.1";
const ENCRYPTED_DATA: &str = "1.2.840.113549.1.7.6";
const KEY_BAG: &str = "1.2.840.113549.1.12.10.1.1";
const SHROUDED_KEY_BAG: &str = "1.2.840.113549.1.12.10.1.2";
const CERT_BAG: &str = "1.2.840.113549.1.12.10.1.3";
const X509_CERT: &str = "1.2.840.113549.1.9.22.1";
const FRIENDLY_NAME: &str = "1.2.840.113549.1.9.20";
const LOCAL_KEY_ID: &str = "1.2.840.113549.1.9.21";
const PBES2: &str = "1.2.840.113549.1.5.13";
const PBKDF2: &str = "1.2.840.113549.1.5.12";
const HMAC_SHA1: &str = "1.2.840.113549.2.7";
const HMAC_SHA256: &str = "1.2.840.113549.2.9";
const HMAC_SHA384: &str = "1.2.840.113549.2.10";
const HMAC_SHA512: &str = "1.2.840.113549.2.11";
const AES128_CBC: &str = "2.16.840.1.101.3.4.1.2";
const AES192_CBC: &str = "2.16.840.1.101.3.4.1.22";
const AES256_CBC: &str = "2.16.840.1.101.3.4.1.42";
const DES_EDE3_CBC: &str = "1.2.840.113549.3.7";
const PBE_SHA_3DES: &str = "1.2.840.113549.1.12.1.3";
const PBE_SHA_2DES: &str = "1.2.840.113549.1.12.1.4";
const PBE_SHA_RC2_128: &str = "1.2.840.113549.1.12.1.5";
const PBE_SHA_RC2_40: &str = "1.2.840.113549.1.12.1.6";
/// Key-derivation rounds for new files: OWASP's figure for PBKDF2-HMAC-SHA-256. The MAC uses the
/// same count, because checking it alone tells whether a guessed password is right.
pub const WRITE_ROUNDS: u32 = 600_000;
/// The shortest password a new digital ID may have (NIST SP 800-63B). Opening an existing file
/// takes any password, so nobody is locked out of an ID made before this minimum.
pub const MIN_PASSWORD_CHARS: usize = 8;
/// The most key-derivation work one [`open`] does, in hash blocks summed over every derivation
/// in the file. The file names its own iteration counts, so without a budget a crafted file
/// hangs the caller on the password check. A file written by [`write`] needs about
/// 2 million; real files from other tools need far less.
const OPEN_BUDGET: u64 = 10_000_000;
/// What is left of [`OPEN_BUDGET`] while one file is opened.
struct Budget(u64);
impl Budget {
/// Spend `rounds` iterations to derive `len` bytes with `alg`, and return them as the `u32`
/// the derivation takes, or refuse the file if the budget runs out.
fn spend(&mut self, alg: DigestAlg, rounds: u64, len: usize) -> Result<u32, Error> {
// Every derivation hashes at least once, and repeats the rounds for each output block.
let blocks = len.div_ceil(alg.digest(&[]).len().max(1)).max(1) as u64;
let cost = rounds.max(1).saturating_mul(blocks);
match (self.0.checked_sub(cost), u32::try_from(rounds)) {
(Some(left), Ok(rounds)) => {
self.0 = left;
Ok(rounds)
}
_ => Err(Error::Unsupported(format!("PKCS #12 files that need more than {OPEN_BUDGET} rounds of key derivation ({rounds} asked for)"))),
}
}
}
/// What a digital ID file holds: the signing key, its certificate and any others (issuers).
#[derive(Debug)]
pub struct DigitalId {
pub key: PrivateKey,
pub certificate: Certificate,
/// The other certificates in the file (the chain), without the signer's.
pub chain: Vec<Certificate>,
pub friendly_name: Option<String>,
}
fn bad(what: &str) -> Error {
Error::Malformed(format!("PKCS #12: {what}"))
}
/// The password as a BMPString with its terminating NUL (RFC 7292 Appendix B.1).
fn bmp(password: &str) -> Zeroizing<Vec<u8>> {
let mut v = Zeroizing::new(password.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>());
v.extend([0, 0]);
v
}
/// The PKCS #12 key derivation function (RFC 7292 Appendix B.2).
fn pkcs12_kdf(alg: DigestAlg, password: &[u8], salt: &[u8], id: u8, iterations: u32, n: usize) -> Zeroizing<Vec<u8>> {
// Callers only pass the SHA-1/SHA-2 digests PKCS #12 files use (`open` restricts the MAC digest).
let (u, v) = match alg {
DigestAlg::Sha1 => (20, 64),
DigestAlg::Sha224 => (28, 64),
DigestAlg::Sha256 => (32, 64),
DigestAlg::Sha384 => (48, 128),
DigestAlg::Sha512 => (64, 128),
_ => return Zeroizing::new(Vec::new()),
};
let _ = u;
let fill = |s: &[u8]| -> Zeroizing<Vec<u8>> {
if s.is_empty() {
return Zeroizing::new(Vec::new());
}
let len = v * s.len().div_ceil(v);
Zeroizing::new(s.iter().cycle().take(len).copied().collect())
};
let d = vec![id; v];
let mut i = Zeroizing::new([fill(salt).as_slice(), fill(password).as_slice()].concat());
let mut out = Zeroizing::new(Vec::with_capacity(n));
while out.len() < n {
let mut a = Zeroizing::new(alg.digest(&[&d, &i]));
for _ in 1..iterations.max(1) {
a = Zeroizing::new(alg.digest(&[&a]));
}
let b = Zeroizing::new(a.iter().cycle().take(v).copied().collect::<Vec<u8>>());
for chunk in i.chunks_mut(v) {
// chunk = (chunk + b + 1) mod 2^(8v)
let mut carry = 1u16;
for k in (0..v).rev() {
let s = chunk[k] as u16 + b[k] as u16 + carry;
chunk[k] = s as u8;
carry = s >> 8;
}
}
out.extend_from_slice(&a);
}
out.truncate(n);
out
}
#[derive(Clone, Copy)]
enum Cipher {
Aes128,
Aes192,
Aes256,
TripleDes,
Rc2(usize),
}
impl Cipher {
fn key_len(self) -> usize {
match self {
Cipher::Aes128 => 16,
Cipher::Aes192 => 24,
Cipher::Aes256 => 32,
Cipher::TripleDes => 24,
Cipher::Rc2(bits) => bits / 8,
}
}
fn decrypt(self, key: &[u8], iv: &[u8], ct: &[u8]) -> Result<Zeroizing<Vec<u8>>, Error> {
let wrong = || Error::WrongPassword;
let r = match self {
Cipher::Aes128 => cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv).map_err(|_| wrong())?.decrypt_padded_vec::<Pkcs7>(ct),
Cipher::Aes192 => cbc::Decryptor::<aes::Aes192>::new_from_slices(key, iv).map_err(|_| wrong())?.decrypt_padded_vec::<Pkcs7>(ct),
Cipher::Aes256 => cbc::Decryptor::<aes::Aes256>::new_from_slices(key, iv).map_err(|_| wrong())?.decrypt_padded_vec::<Pkcs7>(ct),
Cipher::TripleDes => cbc::Decryptor::<des::TdesEde3>::new_from_slices(key, iv).map_err(|_| wrong())?.decrypt_padded_vec::<Pkcs7>(ct),
Cipher::Rc2(bits) => {
let c = rc2::Rc2::new_with_eff_key_len(key, bits);
cbc::Decryptor::<rc2::Rc2>::inner_iv_slice_init(c, iv).map_err(|_| wrong())?.decrypt_padded_vec::<Pkcs7>(ct)
}
};
r.map(Zeroizing::new).map_err(|_| wrong())
}
}
fn hmac(alg: DigestAlg, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Error> {
fn run<D: hmac::EagerHash>(key: &[u8], data: &[u8]) -> Result<Vec<u8>, Error>
where
hmac::Hmac<D>: hmac::KeyInit + Mac,
{
// HMAC takes any key length, so this never fails in practice.
let mut m = <hmac::Hmac<D> as hmac::KeyInit>::new_from_slice(key).map_err(|_| bad("MAC key"))?;
m.update(data);
Ok(m.finalize().into_bytes().to_vec())
}
match alg {
DigestAlg::Sha1 => run::<sha1::Sha1>(key, data),
DigestAlg::Sha224 => run::<sha2::Sha224>(key, data),
DigestAlg::Sha256 => run::<sha2::Sha256>(key, data),
DigestAlg::Sha384 => run::<sha2::Sha384>(key, data),
DigestAlg::Sha512 => run::<sha2::Sha512>(key, data),
other => Err(Error::Unsupported(format!("{} in a PKCS #12 MAC", other.name()))),
}
}
fn pbkdf2(prf: DigestAlg, password: &[u8], salt: &[u8], rounds: u32, len: usize) -> Result<Zeroizing<Vec<u8>>, Error> {
let mut out = Zeroizing::new(vec![0u8; len]);
match prf {
DigestAlg::Sha1 => pbkdf2::pbkdf2_hmac::<sha1::Sha1>(password, salt, rounds, &mut out),
DigestAlg::Sha224 => pbkdf2::pbkdf2_hmac::<sha2::Sha224>(password, salt, rounds, &mut out),
DigestAlg::Sha256 => pbkdf2::pbkdf2_hmac::<sha2::Sha256>(password, salt, rounds, &mut out),
DigestAlg::Sha384 => pbkdf2::pbkdf2_hmac::<sha2::Sha384>(password, salt, rounds, &mut out),
DigestAlg::Sha512 => pbkdf2::pbkdf2_hmac::<sha2::Sha512>(password, salt, rounds, &mut out),
other => return Err(Error::Unsupported(format!("PBKDF2 with {}", other.name()))),
}
Ok(out)
}
/// Decrypt `ct` with the password-based scheme `alg` (an AlgorithmIdentifier).
fn pbe_decrypt(alg: &Tlv<'_>, password: &str, ct: &[u8], budget: &mut Budget) -> Result<Zeroizing<Vec<u8>>, Error> {
let parts = alg.children()?;
let o = parts.first().ok_or_else(|| bad("algorithm"))?.oid()?;
let params = parts.get(1).ok_or_else(|| bad("algorithm parameters"))?;
match o.as_str() {
PBES2 => {
let p = params.children()?;
let [kdf, scheme] = p.as_slice() else { return Err(bad("PBES2 parameters")) };
let kdf = kdf.children()?;
if kdf.first().map(|o| o.oid()).transpose()?.as_deref() != Some(PBKDF2) {
return Err(Error::Unsupported("PBES2 key derivation other than PBKDF2".into()));
}
let kp = kdf.get(1).ok_or_else(|| bad("PBKDF2 parameters"))?.children()?;
let salt = kp.first().ok_or_else(|| bad("salt"))?.octets("salt")?;
let rounds = kp.get(1).ok_or_else(|| bad("iterations"))?.u64()?;
let prf = match kp
.iter()
.skip(2)
.find(|t| t.tag == tag::SEQUENCE)
.and_then(|a| a.children().ok())
.and_then(|a| a.first().and_then(|o| o.oid().ok()))
{
None => DigestAlg::Sha1,
Some(o) => match o.as_str() {
HMAC_SHA1 => DigestAlg::Sha1,
HMAC_SHA256 => DigestAlg::Sha256,
HMAC_SHA384 => DigestAlg::Sha384,
HMAC_SHA512 => DigestAlg::Sha512,
other => return Err(Error::Unsupported(format!("PBKDF2 PRF {other}"))),
},
};
let sc = scheme.children()?;
let cipher = match sc.first().map(|o| o.oid()).transpose()?.as_deref() {
Some(AES128_CBC) => Cipher::Aes128,
Some(AES192_CBC) => Cipher::Aes192,
Some(AES256_CBC) => Cipher::Aes256,
Some(DES_EDE3_CBC) => Cipher::TripleDes,
other => return Err(Error::Unsupported(format!("PBES2 cipher {}", other.unwrap_or("?")))),
};
let iv = sc.get(1).ok_or_else(|| bad("IV"))?.octets("IV")?;
let rounds = budget.spend(prf, rounds, cipher.key_len())?;
let key = pbkdf2(prf, password.as_bytes(), &salt, rounds, cipher.key_len())?;
cipher.decrypt(&key, &iv, ct)
}
PBE_SHA_3DES | PBE_SHA_2DES | PBE_SHA_RC2_128 | PBE_SHA_RC2_40 => {
let p = params.children()?;
let salt = p.first().ok_or_else(|| bad("salt"))?.octets("salt")?;
let rounds = p.get(1).ok_or_else(|| bad("iterations"))?.u64()?;
let (cipher, key_len, iv_len) = match o.as_str() {
PBE_SHA_3DES => (Cipher::TripleDes, 24, 8),
PBE_SHA_2DES => (Cipher::TripleDes, 16, 8),
PBE_SHA_RC2_128 => (Cipher::Rc2(128), 16, 8),
_ => (Cipher::Rc2(40), 5, 8),
};
let pw = bmp(password);
// Two derivations from the same count: the key, then the IV.
let rounds = budget.spend(DigestAlg::Sha1, rounds, key_len)?;
budget.spend(DigestAlg::Sha1, u64::from(rounds), iv_len)?;
let mut key = pkcs12_kdf(DigestAlg::Sha1, &pw, &salt, 1, rounds, key_len);
if key.len() == 16 && matches!(cipher, Cipher::TripleDes) {
// Two-key 3DES: K1 K2 K1.
key.extend_from_within(..8);
}
let iv = pkcs12_kdf(DigestAlg::Sha1, &pw, &salt, 2, rounds, iv_len);
cipher.decrypt(&key, &iv, ct)
}
other => Err(Error::Unsupported(format!("PKCS #12 encryption {other}"))),
}
}
struct Bag {
kind: BagKind,
local_key_id: Option<Vec<u8>>,
friendly_name: Option<String>,
}
enum BagKind {
Key(Zeroizing<Vec<u8>>),
Cert(Vec<u8>),
}
fn bags(safe_contents: &[u8], password: &str, out: &mut Vec<Bag>, budget: &mut Budget) -> Result<(), Error> {
for bag in Tlv::parse_all_ber(safe_contents)?.children()? {
let f = bag.children()?;
let Some(id) = f.first().and_then(|o| o.oid().ok()) else { continue };
let Some(value) = f.get(1).filter(|v| v.tag == tag::ctx(0)).and_then(|v| v.inner().ok()) else { continue };
let (mut local_key_id, mut friendly_name) = (None, None);
if let Some(attrs) = f.get(2) {
for a in attrs.children()? {
let p = a.children()?;
let v = p.get(1).and_then(|s| s.children().ok()).and_then(|s| s.into_iter().next());
match (p.first().and_then(|o| o.oid().ok()).as_deref(), v) {
(Some(LOCAL_KEY_ID), Some(v)) => local_key_id = v.octets("local key id").ok().map(Cow::into_owned),
(Some(FRIENDLY_NAME), Some(v)) => friendly_name = v.text(),
_ => {}
}
}
}
let kind = match id.as_str() {
KEY_BAG => BagKind::Key(Zeroizing::new(value.raw.to_vec())),
SHROUDED_KEY_BAG => {
let p = value.children()?;
let [alg, data] = p.as_slice() else { return Err(bad("EncryptedPrivateKeyInfo")) };
BagKind::Key(pbe_decrypt(alg, password, &data.octets("encrypted private key")?, budget)?)
}
CERT_BAG => {
let p = value.children()?;
if p.first().and_then(|o| o.oid().ok()).as_deref() != Some(X509_CERT) {
continue;
}
let Some(c) = p.get(1).and_then(|c| c.inner().ok()).and_then(|c| c.octets("certificate").ok()) else { continue };
BagKind::Cert(c.into_owned())
}
_ => continue,
};
out.push(Bag { kind, local_key_id, friendly_name });
}
Ok(())
}
fn normalize(bytes: &[u8]) -> Result<Cow<'_, [u8]>, Error> {
let start = bytes.iter().position(|b| !b.is_ascii_whitespace()).unwrap_or(0);
let end = bytes.iter().rposition(|b| !b.is_ascii_whitespace()).map_or(0, |i| i + 1);
let trimmed = bytes.get(start..end).unwrap_or(b""); // hostile input: no slicing by hand
// 1. A DER file starts with a SEQUENCE tag: borrow, no work.
if trimmed.first() == Some(&0x30) {
return Ok(Cow::Borrowed(trimmed));
}
// 2. PEM armor: base64 body between the BEGIN/END lines.
if trimmed.starts_with(b"-----BEGIN") {
let body = pem_body(trimmed)?;
return Ok(Cow::Owned(decode_base64(body)?));
}
// 3. Raw base64 text, armour or not (a real DER PFX always contains bytes
// outside the base64 alphabet, so this can't swallow a valid file).
if trimmed.iter().all(|b| b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/' || *b == b'=' || b.is_ascii_whitespace()) {
return Ok(Cow::Owned(decode_base64(trimmed)?));
}
// 4. Not recognisable: hand the bytes to Tlv::parse_all for a clear error.
Ok(Cow::Borrowed(trimmed))
}
fn decode_base64(input: &[u8]) -> Result<Vec<u8>, Error> {
let compact: Vec<u8> = input.iter().copied().filter(|b| !b.is_ascii_whitespace()).collect();
base64::engine::general_purpose::STANDARD.decode(&compact).map_err(|e| Error::Malformed(format!("PKCS #12: base64: {e}")))
}
/// The base64 lines between "-----BEGIN …-----" and "-----END …-----".
fn pem_body(pem: &[u8]) -> Result<&[u8], Error> {
let text = std::str::from_utf8(pem).map_err(|_| bad("PEM file is not valid text"))?;
let begin = text.find("-----BEGIN ").ok_or_else(|| bad("PEM file has no BEGIN marker"))?;
let body_start = text[begin..].find('\n').map_or(text.len(), |i| begin + i + 1);
let body_end = text.find("-----END ").filter(|end| *end >= body_start).ok_or_else(|| bad("PEM file has no END marker"))?;
let body = text.as_bytes().get(body_start..body_end).ok_or_else(|| bad("PEM body"))?;
if !body.iter().any(|b| !b.is_ascii_whitespace()) {
return Err(bad("PEM file has an empty body"));
}
Ok(body)
}
/// Open a `.p12` / `.pfx` file. A wrong password is [`Error::WrongPassword`].
pub fn open(bytes: &[u8], password: &str) -> Result<DigitalId, Error> {
let der = normalize(bytes)?;
let pfx = Tlv::parse_all_ber(&der)
.map_err(|e| match e {
// Keep "not a PKCS #12 file" but carry the exact reason through.
Error::Malformed(why) => bad(&format!("not a PKCS #12 file: {why}")),
e => e,
})?
.children()?;
let auth_safe = pfx.get(1).ok_or_else(|| bad("authSafe"))?.children()?;
if auth_safe.first().map(|o| o.oid()).transpose()?.as_deref() != Some(DATA) {
return Err(Error::Unsupported("public-key protected PKCS #12 files".into()));
}
let content = auth_safe.get(1).ok_or_else(|| bad("authSafe content"))?.inner()?.octets("authSafe")?;
let mut budget = Budget(OPEN_BUDGET);
// The MAC proves the password before anything is decrypted. It covers the contents of that
// OCTET STRING — the segments joined, if the file is segmented, which is what wrote them.
if let Some(mac) = pfx.get(2) {
let m = mac.children()?;
let digest_info = m.first().ok_or_else(|| bad("MacData"))?.children()?;
let alg = digest_info.first().ok_or_else(|| bad("MAC algorithm"))?.children()?;
let alg_oid = alg.first().ok_or_else(|| bad("MAC algorithm"))?.oid()?;
let alg = DigestAlg::from_oid(&alg_oid)
.filter(|d| matches!(d, DigestAlg::Sha1 | DigestAlg::Sha224 | DigestAlg::Sha256 | DigestAlg::Sha384 | DigestAlg::Sha512))
.ok_or_else(|| Error::Unsupported(format!("MAC digest {alg_oid}")))?;
let expected = digest_info.get(1).ok_or_else(|| bad("MAC"))?.octets("MAC")?;
let salt = m.get(1).ok_or_else(|| bad("MAC salt"))?.octets("MAC salt")?;
let iterations = m.get(2).map(|i| i.u64()).transpose()?.unwrap_or(1);
let key_len = alg.digest(&[]).len();
let iterations = budget.spend(alg, iterations, key_len)?;
let key = pkcs12_kdf(alg, &bmp(password), &salt, 3, iterations, key_len);
if hmac(alg, &key, &content)? != *expected {
return Err(Error::WrongPassword);
}
}
let mut all = Vec::new();
for ci in Tlv::parse_all_ber(&content)?.children()? {
let p = ci.children()?;
let kind = p.first().ok_or_else(|| bad("ContentInfo"))?.oid()?;
let body = p.get(1).ok_or_else(|| bad("ContentInfo content"))?.inner()?;
match kind.as_str() {
DATA => bags(&body.octets("SafeContents")?, password, &mut all, &mut budget)?,
ENCRYPTED_DATA => {
let ed = body.children()?;
let eci = ed.get(1).ok_or_else(|| bad("EncryptedContentInfo"))?.children()?;
let alg = eci.get(1).ok_or_else(|| bad("content encryption algorithm"))?;
let ct: Cow<'_, [u8]> = match eci.get(2) {
Some(t) if t.tag == tag::ctx_prim(0) => Cow::Borrowed(t.value),
// Constructed form: the OCTET STRING segments, joined.
Some(t) if t.tag == tag::ctx(0) => {
let mut v = Vec::with_capacity(t.value.len());
for s in t.children()? {
v.extend_from_slice(&s.octets("encryptedContent")?);
}
Cow::Owned(v)
}
_ => return Err(bad("encryptedContent")),
};
let plain = pbe_decrypt(alg, password, &ct, &mut budget)?;
bags(&plain, password, &mut all, &mut budget)?;
}
_ => {}
}
}
let (key_bag, pkcs8) = all
.iter()
.find_map(|b| match &b.kind {
BagKind::Key(k) => Some((b, k)),
_ => None,
})
.ok_or_else(|| bad("no private key"))?;
let key = PrivateKey::from_pkcs8(pkcs8)?;
let certs: Vec<(Certificate, &Bag)> = all
.iter()
.filter_map(|b| match &b.kind {
BagKind::Cert(c) => Certificate::parse(c).ok().map(|c| (c, b)),
_ => None,
})
.collect();
// The signer's certificate: the same local key id, else the one with the key's public key.
let idx = certs
.iter()
.position(|(_, b)| key_bag.local_key_id.is_some() && b.local_key_id == key_bag.local_key_id)
.or_else(|| certs.iter().position(|(c, _)| &c.public_key == key.public_key()))
.ok_or_else(|| bad("no certificate for the private key"))?;
let friendly_name = key_bag.friendly_name.clone().or_else(|| certs[idx].1.friendly_name.clone());
let mut certs: Vec<Certificate> = certs.into_iter().map(|(c, _)| c).collect();
let certificate = certs.remove(idx);
if &certificate.public_key != key.public_key() {
return Err(bad("the certificate does not match the private key"));
}
Ok(DigitalId { key, certificate, chain: certs, friendly_name })
}
fn random(n: usize) -> Result<Vec<u8>, Error> {
let mut v = vec![0u8; n];
getrandom::fill(&mut v).map_err(|e| Error::Crypto(format!("no randomness: {e}")))?;
Ok(v)
}
/// Write a digital ID as a `.p12` protected by `password`.
pub fn write(id: &DigitalId, password: &str) -> Result<Vec<u8>, Error> {
let encrypt = |plain: &[u8]| -> Result<Vec<u8>, Error> {
let (salt, iv) = (random(16)?, random(16)?);
let key = pbkdf2(DigestAlg::Sha256, password.as_bytes(), &salt, WRITE_ROUNDS, 32)?;
let ct =
cbc::Encryptor::<aes::Aes256>::new_from_slices(&key, &iv).map_err(|_| Error::Crypto("AES".into()))?.encrypt_padded_vec::<Pkcs7>(plain);
let kdf = der::seq(&[
&der::oid(PBKDF2),
&der::seq(&[&der::octets(&salt), &der::int(WRITE_ROUNDS as u64), &der::seq(&[&der::oid(HMAC_SHA256), &der::null()])]),
]);
let alg = der::seq(&[&der::oid(PBES2), &der::seq(&[&kdf, &der::seq(&[&der::oid(AES256_CBC), &der::octets(&iv)])])]);
Ok(der::seq(&[&alg, &der::octets(&ct)]))
};
let local_id = DigestAlg::Sha1.digest(&[&id.certificate.raw]);
let mut attrs: Vec<Vec<u8>> = vec![der::seq(&[&der::oid(LOCAL_KEY_ID), &der::set_of(&[&der::octets(&local_id)])])];
if let Some(n) = &id.friendly_name {
let v: Vec<u8> = n.encode_utf16().flat_map(u16::to_be_bytes).collect();
attrs.push(der::seq(&[&der::oid(FRIENDLY_NAME), &der::set_of(&[&der::tlv(tag::BMP_STRING, &v)])]));
}
let attr_refs: Vec<&[u8]> = attrs.iter().map(Vec::as_slice).collect();
let attr_set = der::set_of(&attr_refs);
let cert_bag = |c: &Certificate, with_attrs: bool| {
let value = der::explicit(0, &der::seq(&[&der::oid(X509_CERT), &der::explicit(0, &der::octets(&c.raw))]));
if with_attrs { der::seq(&[&der::oid(CERT_BAG), &value, &attr_set]) } else { der::seq(&[&der::oid(CERT_BAG), &value]) }
};
let mut cert_bags = vec![cert_bag(&id.certificate, true)];
cert_bags.extend(id.chain.iter().map(|c| cert_bag(c, false)));
let certs_plain = der::seq(&cert_bags.iter().map(Vec::as_slice).collect::<Vec<_>>());
let enc_certs = encrypt(&certs_plain)?;
// EncryptedData: version 0, EncryptedContentInfo { data, algorithm, [0] IMPLICIT ct }.
let enc_parts = Tlv::parse_all(&enc_certs)?.children()?;
let eci = der::seq(&[&der::oid(DATA), enc_parts[0].raw, &der::tlv(tag::ctx_prim(0), enc_parts[1].value)]);
let encrypted_data = der::seq(&[&der::int(0), &eci]);
let ci_certs = der::seq(&[&der::oid(ENCRYPTED_DATA), &der::explicit(0, &encrypted_data)]);
let key_bag = der::seq(&[&der::oid(SHROUDED_KEY_BAG), &der::explicit(0, &encrypt(id.key.pkcs8())?), &attr_set]);
let ci_key = der::seq(&[&der::oid(DATA), &der::explicit(0, &der::octets(&der::seq(&[&key_bag])))]);
let auth_safe = der::seq(&[&ci_certs, &ci_key]);
let salt = random(16)?;
let mac_key = pkcs12_kdf(DigestAlg::Sha256, &bmp(password), &salt, 3, WRITE_ROUNDS, 32);
let mac = hmac(DigestAlg::Sha256, &mac_key, &auth_safe)?;
let mac_data = der::seq(&[&der::seq(&[&DigestAlg::Sha256.algorithm(), &der::octets(&mac)]), &der::octets(&salt), &der::int(WRITE_ROUNDS as u64)]);
Ok(der::seq(&[&der::int(3), &der::seq(&[&der::oid(DATA), &der::explicit(0, &der::octets(&auth_safe))]), &mac_data]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_budget_covers_every_derivation_in_a_file() {
let mut budget = Budget(OPEN_BUDGET);
// What a file from `write` needs: the MAC and two PBKDF2 derivations.
for _ in 0..3 {
assert_eq!(budget.spend(DigestAlg::Sha256, WRITE_ROUNDS.into(), 32).ok(), Some(WRITE_ROUNDS));
}
// Six million rounds fit once, but not twice.
assert!(budget.spend(DigestAlg::Sha256, 6_000_000, 32).is_ok());
assert!(budget.spend(DigestAlg::Sha256, 6_000_000, 32).is_err());
// Each output block repeats the rounds: 24 bytes of SHA-1 are two blocks.
let mut budget = Budget(OPEN_BUDGET);
assert!(budget.spend(DigestAlg::Sha1, 6_000_000, 24).is_err());
// Zero rounds still hashes once.
assert_eq!(budget.spend(DigestAlg::Sha1, 0, 20).ok(), Some(0));
assert_eq!(budget.0, OPEN_BUDGET - 1);
}
#[test]
fn key_and_kdf_buffers_are_zeroizing() {
let key = PrivateKey::generate_p256().unwrap();
let _: &Zeroizing<Vec<u8>> = &key.pkcs8;
let pw: Zeroizing<Vec<u8>> = bmp("secret12");
assert_eq!(&*pw, &[0, b's', 0, b'e', 0, b'c', 0, b'r', 0, b'e', 0, b't', 0, b'1', 0, b'2', 0, 0]);
let kdf: Zeroizing<Vec<u8>> = pkcs12_kdf(DigestAlg::Sha256, &pw, &[1, 2, 3, 4], 1, 1, 32);
assert_eq!(kdf.len(), 32);
let derived: Zeroizing<Vec<u8>> = pbkdf2(DigestAlg::Sha256, b"secret12", &[1, 2, 3, 4], 1, 32).unwrap();
assert_eq!(derived.len(), 32);
}
}
+309
View File
@@ -0,0 +1,309 @@
//! Revocation evidence (RFC 5280 CRLs, RFC 6960 OCSP): parsing, signature verification and
//! status lookup for evidence embedded in a DSS or supplied by the caller. Never fetches:
//! the transport and the choice of evidence stay with the caller.
use crate::der::{Tlv, tag};
use crate::keys::{self, DigestAlg, PublicKey};
use crate::x509::Certificate;
use crate::{Error, Time};
/// A DER size cap: real CRLs and OCSP responses are far smaller.
pub(crate) const MAX_EVIDENCE: usize = 2 * 1024 * 1024;
const ID_PKIX_OCSP_BASIC: &str = "1.3.6.1.5.5.7.48.1.1";
const EKU_OCSP_SIGNING: &str = "1.3.6.1.5.5.7.3.9";
/// What a piece of revocation evidence says about a certificate at a point in time.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RevocationStatus {
/// The evidence covers the certificate and does not revoke it.
Good,
/// The evidence covers the certificate and revokes it.
Revoked { at: Time },
/// The evidence does not cover the certificate, is outside its window, or could not be
/// verified.
Unknown,
}
fn bad(what: impl Into<String>) -> Error {
Error::Malformed(format!("revocation: {}", what.into()))
}
fn strip(b: &[u8]) -> &[u8] {
match b {
[0, rest @ ..] if !rest.is_empty() => rest,
v => v,
}
}
fn valid_in_window(this: Time, next: Option<Time>, at: Time) -> bool {
at >= this && next.is_none_or(|n| at <= n)
}
/// Verify `signature` over the raw `signed` bytes with `key`, where the signature's algorithm
/// identifier names the digest.
fn verify_signature(signed: &[u8], alg_raw: &[u8], signature: &[u8], key: &PublicKey) -> bool {
let Ok(alg) = Tlv::parse_all(alg_raw) else { return false };
let Ok((scheme, Some(digest))) = keys::signature_algorithm(&alg) else { return false };
let d = digest.digest(&[signed]);
key.verify(scheme, digest, &d, signature).unwrap_or(false)
}
// ── X.509 CertificateList (CRL) ─────────────────────────────────────────────────────────────
/// A parsed CRL (RFC 5280 §5.1).
pub struct CertificateList {
tbs: Vec<u8>,
sig_alg: Vec<u8>,
signature: Vec<u8>,
pub issuer_raw: Vec<u8>,
pub this_update: Time,
pub next_update: Option<Time>,
/// (serial, revocation date), top to bottom.
pub revoked: Vec<(Vec<u8>, Time)>,
}
impl CertificateList {
pub fn parse(bytes: &[u8]) -> Result<CertificateList, Error> {
if bytes.is_empty() || bytes.len() > MAX_EVIDENCE {
return Err(bad("CRL is empty or exceeds the 2 MiB limit"));
}
let parts = Tlv::parse_all(bytes)?.expect(tag::SEQUENCE, "CertificateList")?.children()?;
let [tbs, alg, sig] = parts.as_slice() else { return Err(bad("CertificateList")) };
let mut f = tbs.children()?.into_iter().peekable();
if f.peek().is_some_and(|t| t.tag == tag::INTEGER) {
f.next(); // version
}
let _alg = f.next().ok_or_else(|| bad("tbsCertList"))?;
let issuer = f.next().ok_or_else(|| bad("tbsCertList"))?.raw.to_vec();
let this_update = f.next().ok_or_else(|| bad("tbsCertList"))?.time()?;
let mut next_update = None;
if f.peek().is_some_and(|t| t.tag == tag::UTC_TIME || t.tag == tag::GENERALIZED_TIME) {
next_update = f.next().map(|t| t.time()).transpose()?;
}
let mut revoked = Vec::new();
if f.peek().is_some_and(|t| t.tag == tag::SEQUENCE) {
for entry in f.next().ok_or_else(|| bad("revokedCertificates"))?.children()? {
let e = entry.children()?;
let serial = e.first().ok_or_else(|| bad("revoked entry"))?.uint_bytes().to_vec();
let at = e.get(1).ok_or_else(|| bad("revoked entry"))?.time()?;
revoked.push((serial, at));
}
}
Ok(CertificateList {
tbs: tbs.raw.to_vec(),
sig_alg: alg.raw.to_vec(),
signature: sig.bits()?.to_vec(),
issuer_raw: issuer,
this_update,
next_update,
revoked,
})
}
/// Check `cert` against this CRL at `at`. The CRL must be issued by `cert`'s issuer, its
/// signature must verify with the issuer's key, and `at` must fall inside its window.
pub fn check(&self, cert: &Certificate, issuer: &Certificate, at: Time) -> RevocationStatus {
if self.issuer_raw != issuer.subject.raw || issuer.subject.raw != cert.issuer.raw {
return RevocationStatus::Unknown;
}
if !verify_signature(&self.tbs, &self.sig_alg, &self.signature, &issuer.public_key) {
return RevocationStatus::Unknown;
}
if !valid_in_window(self.this_update, self.next_update, at) {
return RevocationStatus::Unknown;
}
match self.revoked.iter().find(|(serial, _)| strip(serial) == strip(&cert.serial)) {
Some((_, t)) => RevocationStatus::Revoked { at: *t },
None => RevocationStatus::Good,
}
}
}
// ── OCSP BasicOCSPResponse ──────────────────────────────────────────────────────────────────
/// One `CertID` (RFC 6960 §4.1.1): hashes of the issuer's name and key, and the serial.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CertId {
pub hash_alg: DigestAlg,
pub issuer_name_hash: Vec<u8>,
pub issuer_key_hash: Vec<u8>,
pub serial: Vec<u8>,
}
impl CertId {
/// The `CertID` naming `cert` as issued by `issuer`, using `hash_alg`.
pub fn build(cert: &Certificate, issuer: &Certificate, hash_alg: DigestAlg) -> CertId {
CertId {
hash_alg,
issuer_name_hash: hash_alg.digest(&[&cert.issuer.raw]),
issuer_key_hash: hash_alg.digest(&[&issuer.public_key.key_bits()]),
serial: cert.serial.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum CertStatus {
Good,
Revoked(Time),
Unknown,
}
/// One SingleResponse: the status of one certificate at a time.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SingleResponse {
pub cert_id: CertId,
status: CertStatus,
pub this_update: Time,
pub next_update: Option<Time>,
}
enum ResponderId {
ByName(Vec<u8>),
ByKey(Vec<u8>),
}
/// A parsed OCSP response (RFC 6960 §4.2.1): the responder's identity, per-certificate
/// responses, and the delegated responder's certificate when it is embedded.
pub struct OcspResponse {
tbs: Vec<u8>,
sig_alg: Vec<u8>,
signature: Vec<u8>,
responder_id: ResponderId,
pub responses: Vec<SingleResponse>,
responder_cert: Option<Certificate>,
}
impl OcspResponse {
pub fn parse(bytes: &[u8]) -> Result<OcspResponse, Error> {
if bytes.is_empty() || bytes.len() > MAX_EVIDENCE {
return Err(bad("OCSP response is empty or exceeds the 2 MiB limit"));
}
let resp = Tlv::parse_all(bytes)?.expect(tag::SEQUENCE, "OCSPResponse")?.children()?;
let status = resp.first().ok_or_else(|| bad("missing responseStatus"))?.enumerated()?;
if status != 0 {
return Err(bad(format!("responder reported an error (status {status})")));
}
let response_bytes =
resp.get(1).ok_or_else(|| bad("successful response has no responseBytes"))?.expect(tag::ctx(0), "responseBytes")?.inner()?.children()?;
let [type_oid, wrapped] = response_bytes.as_slice() else { return Err(bad("responseBytes")) };
if type_oid.oid()? != ID_PKIX_OCSP_BASIC {
return Err(bad("unsupported response type"));
}
let basic = Tlv::parse_all(wrapped.value)?.expect(tag::SEQUENCE, "BasicOCSPResponse")?.children()?;
let mut it = basic.into_iter();
let tbs = it.next().ok_or_else(|| bad("BasicOCSPResponse"))?;
let alg = it.next().ok_or_else(|| bad("BasicOCSPResponse"))?;
let signature = it.next().ok_or_else(|| bad("BasicOCSPResponse"))?.expect(tag::BIT_STRING, "signature")?.bits()?.to_vec();
let mut responder_cert = None;
for t in it {
if t.tag == tag::ctx(0) {
for c in t.children()? {
if c.tag == tag::SEQUENCE
&& let Ok(cert) = Certificate::parse(c.raw)
{
responder_cert.get_or_insert(cert);
}
}
}
}
let fields = tbs.children()?;
let mut f = fields.into_iter().peekable();
if f.peek().is_some_and(|t| t.tag == tag::ctx(0)) {
f.next(); // version
}
let rid = f.next().ok_or_else(|| bad("ResponseData"))?;
let responder_id = match rid.tag {
t if t == tag::ctx(1) => ResponderId::ByName(rid.value.to_vec()),
t if t == tag::ctx(2) => ResponderId::ByKey(Tlv::parse_all(rid.value)?.expect(tag::OCTET_STRING, "byKey responderID")?.value.to_vec()),
_ => return Err(bad("responderID")),
};
let _produced_at = f.next().ok_or_else(|| bad("ResponseData"))?.time()?;
let mut responses = Vec::new();
for single in f.next().ok_or_else(|| bad("ResponseData"))?.children()? {
let s = single.children()?;
let cert_id_fields = s.first().ok_or_else(|| bad("SingleResponse"))?.children()?;
let [hash_alg, name_hash, key_hash, serial] = cert_id_fields.as_slice() else { return Err(bad("CertID")) };
let alg_oid = hash_alg.children()?.first().ok_or_else(|| bad("CertID"))?.oid()?;
let Some(hash_alg) = DigestAlg::from_oid(&alg_oid) else { return Err(bad("unsupported CertID hash")) };
let cert_id = CertId {
hash_alg,
issuer_name_hash: name_hash.value.to_vec(),
issuer_key_hash: key_hash.value.to_vec(),
serial: serial.uint_bytes().to_vec(),
};
let status = match s.get(1).map(|t| t.tag) {
// RFC 6960 §4.2.1: CertStatus is CHOICE with IMPLICIT tags — good [0] NULL,
// revoked [1] RevokedInfo (the value is the RevokedInfo's own contents),
// unknown [2] UnknownInfo.
Some(t) if t == tag::ctx_prim(0) => CertStatus::Good,
Some(t) if t == tag::ctx(1) => {
let info = s.get(1).unwrap_or(&single);
let (date, _) = Tlv::parse(info.value)?;
CertStatus::Revoked(date.time()?)
}
Some(t) if t == tag::ctx_prim(2) => CertStatus::Unknown,
_ => return Err(bad("certStatus")),
};
let this_update = s.get(2).ok_or_else(|| bad("SingleResponse"))?.time()?;
let next_update = s.get(3).filter(|t| t.tag == tag::ctx(0)).map(|t| t.inner().and_then(|g| g.time())).transpose()?;
responses.push(SingleResponse { cert_id, status, this_update, next_update });
}
Ok(OcspResponse { tbs: tbs.raw.to_vec(), sig_alg: alg.raw.to_vec(), signature, responder_id, responses, responder_cert })
}
/// Check `cert` (issued by `issuer`) at `at`. The responder must be the issuer itself, or
/// a delegated responder with the OCSP-signing EKU whose certificate is issued by the
/// issuer; the response signature and CertID must match, and `at` must fall inside the
/// response window.
pub fn check(&self, cert: &Certificate, issuer: &Certificate, at: Time) -> RevocationStatus {
// Who signed the response? The issuer identified by responderID, or a delegated,
// embedded responder identified the same way. Which of the two it is comes from that
// choice, never from the embedded certificate's names, which anyone can copy.
let (responder, delegated): (Option<&Certificate>, bool) = match &self.responder_id {
ResponderId::ByName(name) => {
if *name == issuer.subject.raw {
(Some(issuer), false)
} else {
(self.responder_cert.as_ref().filter(|c| *name == c.subject.raw), true)
}
}
ResponderId::ByKey(hash) => {
if *hash == DigestAlg::Sha1.digest(&[&issuer.public_key.key_bits()]) {
(Some(issuer), false)
} else {
(self.responder_cert.as_ref().filter(|c| *hash == DigestAlg::Sha1.digest(&[&c.public_key.key_bits()])), true)
}
}
};
let Some(responder) = responder else { return RevocationStatus::Unknown };
// A delegated responder must carry the OCSP-signing EKU and be issued by the issuer.
if delegated {
if responder.issuer.raw != issuer.subject.raw || !responder.signed_by(&issuer.public_key) {
return RevocationStatus::Unknown;
}
if !responder.extended_key_usage.as_ref().is_some_and(|e| e.iter().any(|o| o == EKU_OCSP_SIGNING)) {
return RevocationStatus::Unknown;
}
}
if !verify_signature(&self.tbs, &self.sig_alg, &self.signature, &responder.public_key) {
return RevocationStatus::Unknown;
}
// Find the SingleResponse for this certificate: the CertID hashes must recompute.
let single = self.responses.iter().find(|r| {
let name_hash = r.cert_id.hash_alg.digest(&[&cert.issuer.raw]);
let key_hash = r.cert_id.hash_alg.digest(&[&issuer.public_key.key_bits()]);
name_hash == r.cert_id.issuer_name_hash && key_hash == r.cert_id.issuer_key_hash && strip(&r.cert_id.serial) == strip(&cert.serial)
});
let Some(single) = single else { return RevocationStatus::Unknown };
if !valid_in_window(single.this_update, single.next_update, at) {
return RevocationStatus::Unknown;
}
match single.status {
CertStatus::Good => RevocationStatus::Good,
CertStatus::Revoked(at) => RevocationStatus::Revoked { at },
CertStatus::Unknown => RevocationStatus::Unknown,
}
}
}
+147
View File
@@ -0,0 +1,147 @@
//! RSA signature checks with the padding done here: the raw public-key operation comes from the
//! `rsa` crate, and the EMSA-PKCS1-v1_5 / EMSA-PSS encodings (RFC 8017 §9) are checked against
//! any digest, any MGF1 hash and any salt length, which the `rsa` crate's typed API can't do.
//! Verification only; public data, so nothing here needs to be constant-time.
use rsa::{BoxedUint, RsaPublicKey};
use crate::Error;
use crate::der;
use crate::keys::{DigestAlg, PssParams};
/// The encoded message `sig^e mod n` as exactly `n.len()` bytes, or `None` if the signature is
/// out of range. `n` is the modulus without leading zeros. `notes` gets an entry when the
/// signature value was not written with exactly the modulus' length (RFC 8017 §8.2.2 wants
/// that); some signers drop or add leading zero bytes.
fn encoded_message(n: &[u8], e: &[u8], sig: &[u8], notes: &mut Vec<&'static str>) -> Result<Option<Vec<u8>>, Error> {
// Redundant leading zeros would size the big-integer arithmetic (and the backend's own size
// limit) by the encoding rather than the number, so they are dropped before anything is built.
let (n, e) = (trim_zeros(n), trim_zeros(e));
let k = n.len();
let key = RsaPublicKey::new(BoxedUint::from_be_slice_vartime(n), BoxedUint::from_be_slice_vartime(e))
.map_err(|e| Error::Malformed(format!("RSA key: {e}")))?;
if sig.len() != k {
notes
.push("The RSA signature value does not have the length of the key's modulus (leading zero bytes dropped or added); it was read anyway.");
}
let sig = match sig.iter().position(|b| *b != 0) {
Some(i) => sig.get(i..).unwrap_or_default(),
None => return Ok(None),
};
// RFC 8017 §5.2.2: the signature representative must be below the modulus. Otherwise s and
// s + n are the same number modulo n and both would verify. Both are big-endian without
// leading zeros here, so a longer one is larger and equal lengths compare byte by byte.
let modulus = n.get(n.iter().position(|b| *b != 0).unwrap_or(n.len())..).unwrap_or_default();
if sig.len() > modulus.len() || (sig.len() == modulus.len() && sig >= modulus) {
return Ok(None);
}
let Ok(em) = rsa::hazmat::rsa_encrypt(&key, &BoxedUint::from_be_slice_vartime(sig)) else { return Ok(None) };
let bytes = em.to_be_bytes();
let mut out = vec![0u8; k.saturating_sub(bytes.len())];
let skip = bytes.len().saturating_sub(k);
out.extend_from_slice(bytes.get(skip..).unwrap_or_default());
Ok(Some(out))
}
fn trim_zeros(b: &[u8]) -> &[u8] {
b.get(b.iter().position(|x| *x != 0).unwrap_or(b.len())..).unwrap_or_default()
}
/// `DigestInfo ::= SEQUENCE { AlgorithmIdentifier, OCTET STRING }`.
fn digest_info(alg: DigestAlg, digest: &[u8], null_params: bool) -> Vec<u8> {
let id = der::algorithm(alg.oid(), null_params.then(der::null).as_deref());
der::seq(&[&id, &der::octets(digest)])
}
/// RSASSA-PKCS1-v1_5 (RFC 8017 §8.2.2): the signature is checked against the one encoding the
/// message digest should have, built here and compared byte for byte, so there is nothing for a
/// forger to put in trailing or padding bytes. Two `DigestInfo` encodings are accepted: with the
/// NULL parameter (the standard one) and without it, which older signers write (RFC 8017 App. B.1
/// has verifiers accept both); the second is noted in `notes`. A signature over the bare digest,
/// without any `DigestInfo`, is not accepted: it does not say which hash algorithm it uses.
pub fn pkcs1_verify(n: &[u8], e: &[u8], alg: DigestAlg, digest: &[u8], sig: &[u8], notes: &mut Vec<&'static str>) -> Result<bool, Error> {
let Some(em) = encoded_message(n, e, sig, notes)? else { return Ok(false) };
let k = em.len();
let forms = [
(digest_info(alg, digest, true), None),
(
digest_info(alg, digest, false),
Some("The RSA signature's DigestInfo has no NULL parameter (older signers write it so); it was read anyway."),
),
];
for (t, note) in forms {
let Some(ps) = k.checked_sub(t.len() + 3).filter(|ps| *ps >= 8) else { continue };
let mut expected = vec![0x00, 0x01];
expected.extend(std::iter::repeat_n(0xFF, ps));
expected.push(0x00);
expected.extend_from_slice(&t);
if expected == em {
notes.extend(note);
return Ok(true);
}
}
Ok(false)
}
/// MGF1 (RFC 8017 §B.2.1).
fn mgf1(hash: DigestAlg, seed: &[u8], len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len + hash.output_len());
let mut counter = 0u32;
while out.len() < len {
out.extend(hash.digest(&[seed, &counter.to_be_bytes()]));
counter = counter.wrapping_add(1);
}
out.truncate(len);
out
}
/// EMSA-PSS-VERIFY (RFC 8017 §9.1.2) with `digest` (of the message, with `alg`) as `mHash`.
pub fn pss_verify(
n: &[u8],
e: &[u8],
alg: DigestAlg,
params: PssParams,
digest: &[u8],
sig: &[u8],
notes: &mut Vec<&'static str>,
) -> Result<bool, Error> {
let Some(raw) = encoded_message(n, e, sig, notes)? else { return Ok(false) };
let n = trim_zeros(n);
let mod_bits = n.first().map_or(0, |b| (n.len() * 8) as u32 - b.leading_zeros());
let Some(em_bits) = mod_bits.checked_sub(1) else { return Ok(false) };
let em_len = em_bits.div_ceil(8) as usize;
// emLen is k, or k - 1 when the modulus is 8m+1 bits (then the top byte must be zero).
let Some(lead) = raw.len().checked_sub(em_len) else { return Ok(false) };
if raw.get(..lead).is_none_or(|l| l.iter().any(|b| *b != 0)) {
return Ok(false);
}
let em = raw.get(lead..).unwrap_or_default();
let h_len = alg.output_len();
if digest.len() != h_len {
return Ok(false);
}
let Some(db_len) = em_len.checked_sub(h_len + 1).filter(|l| *l >= 1) else { return Ok(false) };
if em.last() != Some(&0xBC) {
return Ok(false);
}
let Some((masked_db, rest)) = em.split_at_checked(db_len) else { return Ok(false) };
let Some(h) = rest.get(..h_len) else { return Ok(false) };
let spare = (8 * em_len as u32).saturating_sub(em_bits);
let top_mask = 0xFFu8.checked_shr(spare).unwrap_or(0);
if masked_db.first().is_none_or(|b| b & !top_mask != 0) {
return Ok(false);
}
let mut db: Vec<u8> = masked_db.iter().zip(mgf1(params.mgf, h, db_len)).map(|(a, b)| a ^ b).collect();
if let Some(b) = db.first_mut() {
*b &= top_mask;
}
let Some(sep) = db.iter().position(|b| *b != 0) else { return Ok(false) };
if db.get(sep) != Some(&1) {
return Ok(false);
}
let salt = db.get(sep + 1..).unwrap_or_default();
if params.salt_len.is_some_and(|l| l != salt.len()) {
return Ok(false);
}
Ok(alg.digest(&[&[0u8; 8], digest, salt]) == h)
}
+253
View File
@@ -0,0 +1,253 @@
//! RFC 3161 timestamp queries and tokens.
//!
//! Transport-free by design: callers encode a request, send it through an explicitly
//! configured authority at the application boundary, and validate the returned DER response
//! before the token is attached to a CMS signature. [`respond`] is the TSA-side signer, used
//! by the deterministic test authority and by a locally configured stamping identity.
use crate::cms::SignedData;
use crate::der::{self, Tlv, tag};
use crate::keys::{DigestAlg, PrivateKey};
use crate::x509::Certificate;
use crate::{Error, Time};
const TST_INFO: &str = "1.2.840.113549.1.9.16.1.4";
const MAX_RESPONSE: usize = 4 * 1024 * 1024;
const MAX_TOKEN: usize = 2 * 1024 * 1024;
const MAX_REQUEST: usize = 1024 * 1024;
/// RFC 3161 request data. `nonce` is optional but recommended for online requests.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TimestampQuery {
pub digest: DigestAlg,
pub imprint: Vec<u8>,
pub policy: Option<String>,
pub nonce: Option<u64>,
pub certificate_request: bool,
}
/// Identity and digest algorithm used by a timestamp authority to sign tokens.
pub struct TimestampSigner<'a> {
pub key: &'a PrivateKey,
pub certificate: &'a Certificate,
pub chain: &'a [Certificate],
pub digest: DigestAlg,
}
/// Issuance details for one timestamp response.
pub struct TimestampIssuance<'a> {
pub query: &'a TimestampQuery,
pub policy: &'a str,
pub generation_time: Time,
pub serial: u64,
}
impl TimestampQuery {
pub fn new(digest: DigestAlg, imprint: Vec<u8>) -> Result<Self, Error> {
if imprint.len() != digest.digest(&[&[]]).len() {
return Err(Error::Malformed("RFC 3161 imprint length does not match its digest algorithm".into()));
}
Ok(Self { digest, imprint, policy: None, nonce: None, certificate_request: true })
}
/// Encode a DER `TimeStampReq` (RFC 3161 §2.4.1).
pub fn encode(&self) -> Result<Vec<u8>, Error> {
let alg = self.digest.algorithm();
let imprint = der::seq(&[&alg, &der::octets(&self.imprint)]);
let mut parts: Vec<Vec<u8>> = vec![der::int(1), imprint];
if let Some(policy) = &self.policy {
parts.push(der::try_oid(policy)?);
}
if let Some(nonce) = self.nonce {
parts.push(der::int(nonce));
}
if self.certificate_request {
parts.push(der::boolean(true));
}
let refs: Vec<&[u8]> = parts.iter().map(Vec::as_slice).collect();
Ok(der::seq(&refs))
}
}
/// A validated RFC 3161 time-stamp token.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TimestampToken {
pub raw: Vec<u8>,
pub digest: DigestAlg,
pub imprint: Vec<u8>,
pub policy: String,
pub serial: Vec<u8>,
pub gen_time: Time,
/// The TSTInfo nonce, when it has one that fits a `u64` (the size requests use).
pub nonce: Option<u64>,
}
impl TimestampToken {
/// The TSA's certificate, from the token's embedded certificates.
pub fn signer_certificate(&self) -> Option<Certificate> {
let token = SignedData::parse(&self.raw).ok()?;
token.signer_certificate().cloned()
}
}
/// A caller-supplied transport. The sign crate never creates sockets.
pub trait TimestampAuthority {
fn timestamp(&self, request: &[u8]) -> Result<Vec<u8>, Error>;
}
/// The certificates carried inside a token's CMS (for trust anchoring at validation time).
pub fn token_certs(raw: &[u8]) -> Vec<Certificate> {
SignedData::parse(raw).map(|sd| sd.certificates).unwrap_or_default()
}
fn bad(message: impl Into<String>) -> Error {
Error::Malformed(format!("RFC 3161: {}", message.into()))
}
/// Decode a `TimeStampReq` as [`TimestampQuery::encode`] wrote it.
pub fn parse_request(bytes: &[u8]) -> Result<TimestampQuery, Error> {
if bytes.is_empty() || bytes.len() > MAX_REQUEST {
return Err(bad("request is empty or exceeds the 1 MiB limit"));
}
let fields = Tlv::parse_all(bytes)?.expect(tag::SEQUENCE, "TimeStampReq")?.children()?;
let mut it = fields.into_iter();
let _version = it.next().ok_or_else(|| bad("missing version"))?.u64()?;
let imprint = it.next().ok_or_else(|| bad("missing message imprint"))?.children()?;
let alg_oid =
imprint.first().ok_or_else(|| bad("missing imprint algorithm"))?.children()?.first().ok_or_else(|| bad("missing algorithm OID"))?.oid()?;
let digest = DigestAlg::from_oid(&alg_oid).ok_or_else(|| bad("unsupported imprint digest algorithm"))?;
let hash = imprint.get(1).ok_or_else(|| bad("missing imprint digest"))?.expect(tag::OCTET_STRING, "imprint digest")?.value.to_vec();
let mut q = TimestampQuery::new(digest, hash)?;
for f in it {
match f.tag {
tag::OID => q.policy = Some(f.oid()?),
tag::INTEGER => q.nonce = Some(f.u64()?),
tag::BOOLEAN => q.certificate_request = f.value.first() == Some(&0xFF),
_ => {}
}
}
Ok(q)
}
/// Parse and validate a token (a bare CMS `ContentInfo`): TSTInfo content, bounded size, the
/// TSA's CMS signature and its message digest — but not the imprint, which callers compare
/// against the signed data they hold.
pub fn parse_token(raw: &[u8]) -> Result<TimestampToken, Error> {
if raw.is_empty() || raw.len() > MAX_TOKEN {
return Err(bad("token is empty or exceeds the 2 MiB limit"));
}
let token = SignedData::parse(raw)?;
if token.signer.content_type.as_deref() != Some(TST_INFO) {
return Err(bad("token content is not TSTInfo"));
}
let info = token.content.as_deref().ok_or_else(|| bad("token has no TSTInfo content"))?;
let fields = Tlv::parse_all(info)?.expect(tag::SEQUENCE, "TSTInfo")?.children()?;
let policy = fields.get(1).ok_or_else(|| bad("TSTInfo has no policy"))?.oid()?;
let imprint_fields = fields.get(2).ok_or_else(|| bad("TSTInfo has no message imprint"))?.children()?;
let alg_fields = imprint_fields.first().ok_or_else(|| bad("message imprint has no algorithm"))?.children()?;
let digest = alg_fields
.first()
.ok_or_else(|| bad("message imprint has no digest algorithm"))?
.oid()
.and_then(|o| DigestAlg::from_oid(&o).ok_or_else(|| bad("unsupported imprint digest algorithm")))?;
let imprint = imprint_fields
.get(1)
.ok_or_else(|| bad("message imprint has no digest"))?
.expect(tag::OCTET_STRING, "message imprint digest")?
.value
.to_vec();
let serial = fields.get(3).ok_or_else(|| bad("TSTInfo has no serial number"))?.uint_bytes().to_vec();
let gen_time = fields.get(4).ok_or_else(|| bad("TSTInfo has no generation time"))?.time()?;
// The nonce is the only INTEGER after genTime (RFC 3161 §2.4.2): accuracy is a SEQUENCE and
// ordering a BOOLEAN.
let nonce = fields.iter().skip(5).find(|f| f.tag == tag::INTEGER).and_then(|f| f.u64().ok());
// The signed messageDigest uses the SignerInfo's digest algorithm, which need not be the
// imprint's (RFC 5652 §5.4).
let content_digest = token.signer.digest.digest(&[info]);
if token.signer.message_digest.as_deref() != Some(content_digest.as_slice()) {
return Err(bad("token signed message digest does not match TSTInfo"));
}
let cert = token.signer_certificate().ok_or_else(|| bad("TSA certificate is missing"))?;
// An algorithm that can't be checked stays `Unsupported` rather than reading as invalid.
if !token.verify_signature(cert, &content_digest)? {
return Err(bad("TSA signature is invalid"));
}
Ok(TimestampToken { raw: raw.to_vec(), digest, imprint, policy, serial, gen_time, nonce })
}
/// Parse and validate a `TimeStampResp`, including the CMS signature and message imprint.
pub fn parse_response(bytes: &[u8], query: &TimestampQuery) -> Result<TimestampToken, Error> {
if bytes.is_empty() || bytes.len() > MAX_RESPONSE {
return Err(bad("response is empty or exceeds the 4 MiB limit"));
}
let fields = Tlv::parse_all(bytes)?.expect(tag::SEQUENCE, "TimeStampResp")?.children()?;
let status_info = fields.first().ok_or_else(|| bad("missing status"))?.children()?;
let status = status_info.first().ok_or_else(|| bad("missing status value"))?.u64()?;
if status != 0 && status != 1 {
return Err(bad(format!("TSA rejected the request (status {status})")));
}
let token = fields.get(1).ok_or_else(|| bad("granted response has no time-stamp token"))?;
let token = parse_token(token.raw)?;
if token.digest != query.digest || token.imprint != query.imprint {
return Err(bad("message imprint does not match the requested digest"));
}
if query.nonce.is_some() && token.nonce != query.nonce {
return Err(bad("time-stamp token does not carry the requested nonce"));
}
if query.policy.as_ref().is_some_and(|p| *p != token.policy) {
return Err(bad("time-stamp token was issued under another policy than the one requested"));
}
Ok(token)
}
/// The TSA side: sign a `TimeStampResp` for `query` at `gen_time`. Used by deterministic test
/// authorities and a locally configured stamping identity, never implied to be a remote TSA.
pub fn respond(signer: TimestampSigner<'_>, issuance: TimestampIssuance<'_>) -> Result<Vec<u8>, Error> {
let TimestampSigner { key, certificate: cert, chain, digest: alg } = signer;
let TimestampIssuance { query, policy, generation_time: gen_time, serial } = issuance;
let mut info = vec![
der::int(1),
der::try_oid(policy)?,
der::seq(&[&query.digest.algorithm(), &der::octets(&query.imprint)]),
der::int(serial),
gen_time.encode(),
];
if let Some(nonce) = query.nonce {
info.push(der::int(nonce));
}
let refs: Vec<&[u8]> = info.iter().map(Vec::as_slice).collect();
let tst_info = der::seq(&refs);
let token = crate::cms::sign_encapsulated(key, cert, chain, alg, TST_INFO, &tst_info)?;
let status = der::seq(&[&der::int(0)]);
Ok(der::seq(&[&status, &token]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips_with_options() {
let imprint = DigestAlg::Sha256.digest(&[b"document"]);
let mut q = TimestampQuery::new(DigestAlg::Sha256, imprint.clone()).unwrap();
q.policy = Some("1.2.3.4".into());
q.nonce = Some(42);
let encoded = q.encode().unwrap();
let parsed = parse_request(&encoded).unwrap();
assert_eq!(parsed, q);
// The encoded fields themselves.
let fields = Tlv::parse_all(&encoded).unwrap().children().unwrap();
assert_eq!(fields[0].u64().unwrap(), 1);
assert_eq!(fields[1].children().unwrap()[1].value, imprint.as_slice());
assert_eq!(fields[2].oid().unwrap(), "1.2.3.4");
assert_eq!(fields[3].u64().unwrap(), 42);
assert_eq!(fields[4].value, &[0xff]);
}
#[test]
fn rejects_oversized_response_before_parsing() {
let q = TimestampQuery::new(DigestAlg::Sha256, vec![0; 32]).unwrap();
assert!(parse_response(&vec![0; MAX_RESPONSE + 1], &q).is_err());
assert!(parse_request(&vec![0; MAX_REQUEST + 1]).is_err());
}
}
+704
View File
@@ -0,0 +1,704 @@
//! X.509 certificates (RFC 5280): the fields signing and validation need, signature checks
//! along a chain, and a self-signed certificate builder for new digital IDs.
use crate::Error;
use crate::der::{self, Time, Tlv, tag};
use crate::keys::{self, DigestAlg, PrivateKey, PublicKey};
const CN: &str = "2.5.4.3";
const C: &str = "2.5.4.6";
const O: &str = "2.5.4.10";
const OU: &str = "2.5.4.11";
const EMAIL: &str = "1.2.840.113549.1.9.1";
/// A distinguished name: (attribute OID, value) in order.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Name {
pub attrs: Vec<(String, String)>,
/// The DER encoding, compared byte for byte when matching issuers to subjects.
pub raw: Vec<u8>,
}
impl Name {
fn parse(t: &Tlv<'_>) -> Result<Name, Error> {
let mut attrs = Vec::new();
for rdn in t.expect(tag::SEQUENCE, "Name")?.children()? {
for atv in rdn.children()? {
let parts = atv.children()?;
if let [o, v] = parts.as_slice() {
attrs.push((o.oid()?, v.text().unwrap_or_default()));
}
}
}
Ok(Name { attrs, raw: t.raw.to_vec() })
}
fn get(&self, oid: &str) -> Option<&str> {
self.attrs.iter().find(|(o, _)| o == oid).map(|(_, v)| v.as_str())
}
pub fn common_name(&self) -> Option<&str> {
self.get(CN)
}
pub fn organization(&self) -> Option<&str> {
self.get(O)
}
pub fn unit(&self) -> Option<&str> {
self.get(OU)
}
pub fn email(&self) -> Option<&str> {
self.get(EMAIL)
}
pub fn country(&self) -> Option<&str> {
self.get(C)
}
/// The name Acrobat shows: the common name, else the organization, else the DN.
pub fn display_name(&self) -> String {
self.common_name().or(self.organization()).map(str::to_string).unwrap_or_else(|| self.display())
}
/// "CN=Ada Lovelace, O=Example, E=ada@example.com"
pub fn display(&self) -> String {
self.attrs
.iter()
.map(|(o, v)| {
let k = match o.as_str() {
CN => "CN",
C => "C",
O => "O",
OU => "OU",
EMAIL => "E",
"2.5.4.7" => "L",
"2.5.4.8" => "ST",
other => other,
};
format!("{k}={v}")
})
.collect::<Vec<_>>()
.join(", ")
}
/// A name from its parts (empty ones left out), in the usual order.
pub fn build(cn: &str, ou: &str, o: &str, email: &str, country: &str) -> Name {
let mut rdns: Vec<Vec<u8>> = Vec::new();
let mut attrs = Vec::new();
let mut add = |oid: &str, enc: Vec<u8>, v: &str| {
if !v.trim().is_empty() {
rdns.push(der::set_of(&[&der::seq(&[&der::oid(oid), &enc])]));
attrs.push((oid.to_string(), v.trim().to_string()));
}
};
let country = country.trim().to_ascii_uppercase();
if country.len() == 2 {
add(C, der::printable(&country), &country);
}
add(O, der::utf8(o.trim()), o);
add(OU, der::utf8(ou.trim()), ou);
add(CN, der::utf8(cn.trim()), cn);
add(EMAIL, der::ia5(email.trim()), email);
let raw = der::tlv(tag::SEQUENCE, &rdns.concat());
Name { attrs, raw }
}
}
/// A parsed certificate.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Certificate {
/// The whole DER encoding.
pub raw: Vec<u8>,
tbs: Vec<u8>,
sig_alg: Vec<u8>,
signature: Vec<u8>,
pub serial: Vec<u8>,
pub issuer: Name,
pub subject: Name,
pub not_before: Time,
pub not_after: Time,
pub public_key: PublicKey,
/// Basic constraints: a CA certificate.
pub is_ca: bool,
/// Whether the certificate has a basicConstraints extension at all (old v1 roots don't).
pub has_basic_constraints: bool,
/// Basic constraints `pathLenConstraint`: how many CA certificates may follow this one
/// before the end-entity certificate.
pub path_len: Option<u32>,
/// Key usage bits (bit 0 = digitalSignature, 1 = nonRepudiation, 5 = keyCertSign), if present.
pub key_usage: Option<u16>,
pub subject_key_id: Option<Vec<u8>>,
/// Authority key identifier (2.5.29.35), when present.
pub authority_key_id: Option<Vec<u8>>,
/// Extended key usage OIDs (2.5.29.37), when present.
pub extended_key_usage: Option<Vec<String>>,
/// OCSP responder URLs from the Authority Information Access (1.3.6.1.5.5.7.1.1).
pub ocsp_urls: Vec<String>,
/// CRL distribution point URLs (2.5.29.31).
pub crl_urls: Vec<String>,
/// The `version` field: 0 for v1, 1 for v2, 2 for v3.
pub version: u64,
/// OIDs of extensions marked critical that this crate does not process (RFC 5280 §4.2).
pub unknown_critical: Vec<String>,
}
/// Extensions this crate reads or may safely ignore when they are critical: the ones
/// `Extensions::read` handles, subject/issuer alternative names, and certificate policies
/// (accepted as any policy).
/// qcStatements (1.3.6.1.5.5.7.1.3) is included: RFC 3739 lets qualified (eIDAS and other national)
/// certificates mark it critical, and it declares the certificate's status rather than limiting
/// what the key may sign.
const PROCESSED_EXTENSIONS: [&str; 11] = [
"2.5.29.19",
"2.5.29.15",
"2.5.29.14",
"2.5.29.35",
"2.5.29.37",
"1.3.6.1.5.5.7.1.1",
"2.5.29.31",
"2.5.29.17",
"2.5.29.18",
"2.5.29.32",
"1.3.6.1.5.5.7.1.3",
];
/// id-kp-timeStamping (RFC 5280 §4.2.1.12), required of a timestamp authority (RFC 3161 §2.3).
const EKU_TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
/// Extended key usages that allow signing documents: anyExtendedKeyUsage, emailProtection,
/// codeSigning, documentSigning (RFC 9336), Adobe Authentic Documents and Microsoft document
/// signing.
const DOCUMENT_SIGNING_EKUS: [&str; 6] =
["2.5.29.37.0", "1.3.6.1.5.5.7.3.4", "1.3.6.1.5.5.7.3.3", "1.3.6.1.5.5.7.3.36", "1.2.840.113583.1.1.5", "1.3.6.1.4.1.311.10.3.12"];
/// The certificate extensions `Certificate::parse` reads, gathered tolerantly: an
/// extension that does not parse leaves its field at the default instead of failing the
/// certificate (real-world issuers use encodings with edge cases, e.g. a single-URI CRL
/// distribution point whose `fullName [0]` is IMPLICIT over a bare GeneralName).
#[derive(Default)]
struct Extensions {
is_ca: bool,
has_basic_constraints: bool,
path_len: Option<u32>,
key_usage: Option<u16>,
subject_key_id: Option<Vec<u8>>,
authority_key_id: Option<Vec<u8>>,
extended_key_usage: Option<Vec<String>>,
ocsp_urls: Vec<String>,
crl_urls: Vec<String>,
}
impl Extensions {
fn read(&mut self, oid: &str, value: &Tlv<'_>) {
match oid {
"2.5.29.19" => {
// Present at all, even when it doesn't parse: a malformed basicConstraints must
// not make a certificate look like an old v1 root (which may issue). Unparsed,
// it stays "not a CA".
self.has_basic_constraints = true;
let Ok(bc) = Tlv::parse_all(value.value) else { return };
let fields = bc.children().unwrap_or_default();
self.is_ca = fields.first().is_some_and(|b| b.tag == tag::BOOLEAN && b.value != [0]);
// pathLenConstraint is the INTEGER after the optional cA BOOLEAN. One that is
// present but unreadable (negative, too long) allows no CA below, not any number.
self.path_len = fields.iter().find(|t| t.tag == tag::INTEGER).map(|t| t.u64().map_or(0, |n| u32::try_from(n).unwrap_or(u32::MAX)));
}
"2.5.29.15" => {
// A keyUsage that is present restricts the key even when it doesn't parse: an
// unreadable one allows no usage at all, never every usage.
self.key_usage = Some(0);
let Ok(bits) = Tlv::parse_all(value.value) else { return };
let Some((_, b)) = bits.value.split_first() else { return };
// Bit 0 is the most significant bit of the first byte.
let mut u = 0u16;
for (i, byte) in b.iter().take(2).enumerate() {
for j in 0..8 {
if byte & (0x80 >> j) != 0 {
u |= 1 << (i * 8 + j);
}
}
}
self.key_usage = Some(u);
}
"2.5.29.14" => {
if let Ok(ski) = Tlv::parse_all(value.value) {
self.subject_key_id = Some(ski.value.to_vec());
}
}
// AuthorityKeyIdentifier: the [0] keyIdentifier inside.
"2.5.29.35" => {
if let Ok(aki) = Tlv::parse_all(value.value) {
self.authority_key_id = aki.children().ok().and_then(|c| c.into_iter().find(|t| t.tag == tag::ctx(0))).map(|t| t.value.to_vec());
}
}
// ExtendedKeyUsage: a SEQUENCE OF OID. Like keyUsage, one that is present restricts
// the key even when it doesn't parse: an unreadable one allows no purpose at all.
"2.5.29.37" => {
let purposes = Tlv::parse_all(value.value)
.ok()
.filter(|eku| eku.tag == tag::SEQUENCE)
.and_then(|eku| eku.children().ok())
.and_then(|c| c.iter().map(|t| t.oid().ok()).collect::<Option<Vec<_>>>());
self.extended_key_usage = Some(purposes.unwrap_or_default());
}
// Authority Information Access: OCSP and CA-issuer locations.
"1.3.6.1.5.5.7.1.1" => {
let Ok(aia) = Tlv::parse_all(value.value) else { return };
for access in aia.children().unwrap_or_default() {
let Ok(a) = access.children() else { continue };
if a.len() >= 2 && a[0].oid().ok().as_deref() == Some("1.3.6.1.5.5.7.48.1") && a[1].tag == tag::ctx_prim(6) {
self.ocsp_urls.push(String::from_utf8_lossy(a[1].value).into_owned());
}
}
}
// CRL distribution points: full-name URIs.
"2.5.29.31" => {
let Ok(dps) = Tlv::parse_all(value.value) else { return };
for dp in dps.children().unwrap_or_default() {
let Ok(fields) = dp.children() else { continue };
for field in fields {
if field.tag != tag::ctx(0) {
continue;
}
// distributionPoint [0] DistributionPointName, whose fullName choice is
// [0] IMPLICIT GeneralNames: the value is a run of GeneralNames, not a
// wrapped SEQUENCE (and often a single URI).
let Ok(name) = field.inner() else { continue };
if name.tag != tag::ctx(0) {
continue;
}
let mut rest = name.value;
while !rest.is_empty() {
let Ok((n, r)) = Tlv::parse(rest) else { break };
rest = r;
if n.tag == tag::ctx_prim(6) {
self.crl_urls.push(String::from_utf8_lossy(n.value).into_owned());
}
}
}
}
}
_ => {}
}
}
}
/// Why a certificate may not sign documents ([`Certificate::signing_problem`]).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SigningProblem {
/// Its key usage has neither digitalSignature nor nonRepudiation.
KeyUsage,
/// Its extended key usage has no document-signing purpose.
ExtendedKeyUsage,
/// It has critical extensions this crate does not process: their OIDs.
UnknownCritical(Vec<String>),
}
/// Why a certificate that matched an issuer by name and signature was not used as the issuer
/// ([`build_chain_noted`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RefusedWhy {
/// It is not a CA certificate that may issue certificates.
NotCa,
/// It has critical extensions this crate does not process (and is not self-signed).
UnknownCritical,
/// Its path length constraint does not allow this many CA certificates below it.
PathLength,
/// It was not valid at the time asked about.
NotValid,
}
/// A candidate issuer [`build_chain_noted`] refused, and the certificate it would have vouched for.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Refused<'a> {
pub why: RefusedWhy,
pub issuer: &'a Certificate,
pub subject: &'a Certificate,
}
impl Certificate {
pub fn parse(raw: &[u8]) -> Result<Certificate, Error> {
let cert = Tlv::parse_all(raw)?.expect(tag::SEQUENCE, "Certificate")?;
let parts = cert.children()?;
let [tbs, sig_alg, sig] = parts.as_slice() else { return Err(Error::Malformed("Certificate".into())) };
let mut f = tbs.children()?.into_iter().peekable();
// version [0] EXPLICIT INTEGER DEFAULT v1 (0); v3 is 2. Unreadable counts as v3, the
// stricter reading (no v1-root exception in `may_issue`).
let mut version = 0u64;
if f.peek().is_some_and(|t| t.tag == tag::ctx(0)) {
version = f.next().and_then(|v| v.inner().ok()).and_then(|v| v.u64().ok()).unwrap_or(2);
}
let serial = f.next().ok_or_else(|| Error::Malformed("serial".into()))?.expect(tag::INTEGER, "serial")?.value.to_vec();
let _inner_alg = f.next();
let issuer = Name::parse(&f.next().ok_or_else(|| Error::Malformed("issuer".into()))?)?;
let validity = f.next().ok_or_else(|| Error::Malformed("validity".into()))?.children()?;
let [nb, na] = validity.as_slice() else { return Err(Error::Malformed("validity".into())) };
let subject = Name::parse(&f.next().ok_or_else(|| Error::Malformed("subject".into()))?)?;
let public_key = PublicKey::from_spki(&f.next().ok_or_else(|| Error::Malformed("public key".into()))?)?;
let mut ext = Extensions::default();
let mut unknown_critical = Vec::new();
for t in f {
if t.tag != tag::ctx(3) {
continue;
}
for ext_tlv in t.inner()?.children()? {
let e = ext_tlv.children()?;
let Some(o) = e.first().and_then(|o| o.oid().ok()) else { continue };
let Some(value) = e.last().filter(|v| v.tag == tag::OCTET_STRING) else { continue };
let critical = e.len() == 3 && e.get(1).is_some_and(|b| b.tag == tag::BOOLEAN && b.value != [0]);
if critical && !PROCESSED_EXTENSIONS.contains(&o.as_str()) {
unknown_critical.push(o.clone());
}
// A malformed or unreadable extension must never reject the whole certificate:
// unreadable ones are skipped and the fields they carry keep their defaults.
ext.read(o.as_str(), value);
}
}
let Extensions {
is_ca,
has_basic_constraints,
path_len,
key_usage,
subject_key_id,
authority_key_id,
extended_key_usage,
ocsp_urls,
crl_urls,
} = ext;
Ok(Certificate {
raw: raw.to_vec(),
tbs: tbs.raw.to_vec(),
sig_alg: sig_alg.raw.to_vec(),
signature: sig.bits()?.to_vec(),
serial,
issuer,
subject,
not_before: nb.time()?,
not_after: na.time()?,
public_key,
is_ca,
has_basic_constraints,
path_len,
key_usage,
subject_key_id,
authority_key_id,
extended_key_usage,
ocsp_urls,
crl_urls,
version,
unknown_critical,
})
}
/// Why this certificate may not sign documents, if it may not (RFC 5280 §4.2.1.3,
/// §4.2.1.12, §4.2): a key usage without digitalSignature or nonRepudiation, an extended key
/// usage without a document-signing purpose, or a critical extension this crate doesn't process.
pub fn signing_problem(&self) -> Option<SigningProblem> {
/// keyUsage bits 0 (digitalSignature) and 1 (nonRepudiation).
const SIGNING_BITS: u16 = 0b11;
if self.key_usage.is_some_and(|u| u & SIGNING_BITS == 0) {
Some(SigningProblem::KeyUsage)
} else if self.extended_key_usage.as_ref().is_some_and(|e| !e.iter().any(|o| DOCUMENT_SIGNING_EKUS.contains(&o.as_str()))) {
Some(SigningProblem::ExtendedKeyUsage)
} else if !self.unknown_critical.is_empty() {
Some(SigningProblem::UnknownCritical(self.unknown_critical.clone()))
} else {
None
}
}
/// Whether this certificate may stamp trusted time (RFC 3161 §2.3): its extended key usage
/// includes timeStamping, its key usage (if any) allows digital signatures or
/// non-repudiation, and it has no critical extension this crate doesn't process. A certificate
/// issued for another purpose proves who signed, not when.
pub fn may_timestamp(&self) -> bool {
/// keyUsage bits 0 (digitalSignature) and 1 (nonRepudiation).
const SIGNING_BITS: u16 = 0b11;
self.extended_key_usage.as_ref().is_some_and(|e| e.iter().any(|o| o == EKU_TIME_STAMPING))
&& self.key_usage.is_none_or(|u| u & SIGNING_BITS != 0)
&& self.unknown_critical.is_empty()
}
/// Issued by itself (subject = issuer and its own key verifies it).
pub fn is_self_signed(&self) -> bool {
self.issuer.raw == self.subject.raw && self.signed_by(&self.public_key)
}
/// Whether `key` verifies this certificate's signature.
pub fn signed_by(&self, key: &PublicKey) -> bool {
self.signed_by_cached(key, &mut None)
}
/// [`Self::signed_by`] keeping the digest of the signed part in `digest`, so checking one
/// certificate against many candidate keys hashes it once.
fn signed_by_cached(&self, key: &PublicKey, digest: &mut Option<Vec<u8>>) -> bool {
let Ok(alg) = Tlv::parse_all(&self.sig_alg) else { return false };
match keys::signature_algorithm(&alg) {
Ok((scheme @ keys::Scheme::Ed25519, _)) => key.verify_message(scheme, &self.tbs, &self.signature).unwrap_or(false),
Ok((scheme, Some(d))) => {
let hash = digest.get_or_insert_with(|| d.digest(&[&self.tbs]));
key.verify(scheme, d, hash, &self.signature).unwrap_or(false)
}
_ => false,
}
}
/// Whether this certificate may issue certificates (RFC 5280 §4.2.1.9, §4.2.1.3): it is a
/// CA by its basic constraints, and if it has a key usage, that includes `keyCertSign`. A
/// self-signed v1 or v2 certificate (an old root, which predates extensions) with no basic
/// constraints counts as a CA; a v3 certificate must say it is one.
pub fn may_issue(&self) -> bool {
/// keyUsage bit 5.
const KEY_CERT_SIGN: u16 = 1 << 5;
let old_root = self.version < 2 && !self.has_basic_constraints && self.is_self_signed();
(self.is_ca || old_root) && self.key_usage.is_none_or(|u| u & KEY_CERT_SIGN != 0)
}
/// Valid at `t`.
pub fn valid_at(&self, t: Time) -> bool {
self.not_before <= t && t <= self.not_after
}
/// The display name Acrobat uses: the common name, else the organization, else the DN.
pub fn display_name(&self) -> String {
self.subject.display_name()
}
/// SHA-256 fingerprint as upper-case hex pairs.
pub fn fingerprint(&self) -> String {
DigestAlg::Sha256.digest(&[&self.raw]).iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" ")
}
pub fn serial_hex(&self) -> String {
self.serial.iter().map(|b| format!("{b:02X}")).collect()
}
/// A self-signed certificate for `key`: what Acrobat's "Create a new digital ID ▸ save to
/// file" makes. Valid from `from` for `years`; usable for signing (digital signature and
/// non-repudiation).
pub fn self_signed(name: &Name, key: &PrivateKey, from: Time, years: u32, serial: &[u8]) -> Result<Certificate, Error> {
let alg = key.preferred_digest();
let sig_alg = key.signature_algorithm(alg);
let until = Time { year: from.year + years, ..from };
let spki = key.public_key().spki();
let ski = DigestAlg::Sha1.digest(&[&spki]);
let ext = |o: &str, critical: bool, value: &[u8]| {
if critical {
der::seq(&[&der::oid(o), &der::boolean(true), &der::octets(value)])
} else {
der::seq(&[&der::oid(o), &der::octets(value)])
}
};
// keyUsage: digitalSignature (bit 0) and nonRepudiation (bit 1) → 0b1100_0000, 6 unused bits.
let key_usage = der::tlv(tag::BIT_STRING, &[6, 0b1100_0000]);
let extensions = der::seq(&[
&ext("2.5.29.15", true, &key_usage),
&ext("2.5.29.14", false, &der::octets(&ski)),
// extKeyUsage: emailProtection and Adobe's document signing (1.2.840.113583.1.1.5).
&ext("2.5.29.37", false, &der::seq(&[&der::oid("1.3.6.1.5.5.7.3.4"), &der::oid("1.2.840.113583.1.1.5")])),
]);
let tbs = der::seq(&[
&der::explicit(0, &der::int(2)),
&der::uint(serial),
&sig_alg,
&name.raw,
&der::seq(&[&from.encode(), &until.encode()]),
&name.raw,
&spki,
&der::explicit(3, &extensions),
]);
let signature = key.sign(alg, &tbs)?;
let cert = der::seq(&[&tbs, &sig_alg, &der::bit_string(&signature)]);
Certificate::parse(&cert)
}
}
/// The subject alone, to name a certificate `Certificate::parse` rejects (one with a key type
/// this crate doesn't support, say). `None` when even that can't be read.
pub fn subject_of(raw: &[u8]) -> Option<Name> {
let cert = Tlv::parse_all(raw).ok()?.expect(tag::SEQUENCE, "Certificate").ok()?;
let tbs = cert.children().ok()?.into_iter().next()?;
let mut f = tbs.children().ok()?.into_iter().peekable();
if f.peek().is_some_and(|t| t.tag == tag::ctx(0)) {
f.next();
}
// serial, signature algorithm, issuer, validity, then the subject.
Name::parse(&f.nth(4)?).ok()
}
/// The chain from `leaf` up through `pool`, as far as issuers can be found and may issue: each
/// issuer's key verifies the certificate below it, it is a CA allowed to sign certificates, it
/// has no critical extension this crate doesn't process (unless it is self-signed), its
/// `pathLenConstraint` allows the CA certificates below it, and (when `at` is given) it was valid
/// then. Stops at a self-signed certificate. Certificates embedded in a document are in `pool`
/// too, so an ordinary end-entity certificate must never be accepted as an issuer.
pub fn build_chain<'a>(leaf: &'a Certificate, pool: &'a [Certificate], at: Option<Time>) -> Vec<&'a Certificate> {
build_chain_noted(leaf, pool, at).0
}
/// [`build_chain`], and why it stopped where it did if a certificate that matched the issuer by
/// name and signature was refused.
pub fn build_chain_noted<'a>(leaf: &'a Certificate, pool: &'a [Certificate], at: Option<Time>) -> (Vec<&'a Certificate>, Option<Refused<'a>>) {
let mut chain = vec![leaf];
let mut refused = None;
while chain.len() < 10 {
let Some(&last) = chain.last() else { break };
if last.issuer.raw == last.subject.raw {
break;
}
// CA certificates between `last` and the leaf: what a candidate's pathLenConstraint limits.
let below = chain.len() - 1;
let mut found = None;
let mut digest = None;
for c in pool.iter().filter(|c| c.subject.raw == last.issuer.raw && !chain.contains(c) && last.signed_by_cached(&c.public_key, &mut digest)) {
let why = if !c.may_issue() {
Some(RefusedWhy::NotCa)
} else if !c.unknown_critical.is_empty() && !c.is_self_signed() {
// What such an extension limits (name constraints, policy constraints) would
// go unchecked. A self-signed root's extensions are the trust store's business.
Some(RefusedWhy::UnknownCritical)
} else if c.path_len.is_some_and(|n| below > n as usize) {
Some(RefusedWhy::PathLength)
} else if at.is_some_and(|t| !c.valid_at(t)) {
Some(RefusedWhy::NotValid)
} else {
None
};
match why {
None => {
found = Some(c);
break;
}
Some(why) => refused = Some(Refused { why, issuer: c, subject: last }),
}
}
let Some(issuer) = found else { break };
refused = None;
chain.push(issuer);
}
(chain, refused)
}
/// Certificates from a file: DER, or PEM with one or more `CERTIFICATE` blocks (`.cer`, `.crt`,
/// `.pem`, Acrobat's `.fdf`-free exports), or a PKCS #7 bundle (`.p7b`, `.p7c`) in DER or as a
/// PEM `PKCS7`/`CMS` block, as Windows, Acrobat and certificate authorities export chains.
pub fn load_certificates(bytes: &[u8]) -> Result<Vec<Certificate>, Error> {
if bytes.first() == Some(&0x30) {
// One DER certificate, else a DER bundle; a file that is neither reports the
// certificate's error, the common case.
return match Certificate::parse(bytes) {
Ok(cert) => Ok(vec![cert]),
Err(e) => crate::cms::certificate_bundle(bytes).map_err(|_| e),
};
}
let text = String::from_utf8_lossy(bytes);
let mut out = Vec::new();
// The block being read: its label and its base64 so far.
let mut block: Option<(&str, String)> = None;
for line in text.lines().map(str::trim) {
if let Some(label) = line.strip_prefix("-----BEGIN ").and_then(|l| l.strip_suffix("-----")) {
block = matches!(label, "CERTIFICATE" | "PKCS7" | "CMS").then(|| (label, String::new()));
} else if let Some(label) = line.strip_prefix("-----END ").and_then(|l| l.strip_suffix("-----")) {
// An END that doesn't close the open block ends nothing.
if let Some((open, b)) = block.take_if(|(open, _)| *open == label) {
let der = base64(&b).ok_or_else(|| Error::Malformed("PEM base64".into()))?;
if open == "CERTIFICATE" {
out.push(Certificate::parse(&der)?);
} else {
out.extend(crate::cms::certificate_bundle(&der)?);
}
}
} else if let Some((_, b)) = block.as_mut() {
b.push_str(line);
}
}
// Certificates before a block that never ends are not a smaller set: the file is damaged.
if block.is_some() {
return Err(Error::Malformed("a PEM block is not closed".into()));
}
if out.is_empty() {
return Err(Error::Malformed("no certificate found (expected DER, PEM or a PKCS #7 bundle)".into()));
}
Ok(out)
}
/// The certificate as PEM text (Export certificate).
pub fn to_pem(cert: &Certificate) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut s = String::new();
for c in cert.raw.chunks(3) {
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
for i in 0..4 {
s.push(if i <= c.len() { T[(n >> (18 - 6 * i) & 63) as usize] as char } else { '=' });
}
}
let body: Vec<String> = s.as_bytes().chunks(64).map(|l| String::from_utf8_lossy(l).into_owned()).collect();
format!("-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n", body.join("\n"))
}
fn base64(s: &str) -> Option<Vec<u8>> {
let val = |c: u8| -> Option<u32> {
Some(match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
_ => return None,
} as u32)
};
let digits: Vec<u8> = s.bytes().filter(|c| !c.is_ascii_whitespace() && *c != b'=').collect();
let mut out = Vec::with_capacity(digits.len() * 3 / 4);
for chunk in digits.chunks(4) {
let mut n = 0u32;
for (i, c) in chunk.iter().enumerate() {
n |= val(*c)? << (18 - 6 * i);
}
for i in 0..chunk.len().saturating_sub(1) {
out.push((n >> (16 - 8 * i)) as u8);
}
}
Some(out)
}
#[cfg(test)]
mod xfa_ext_tests {
use super::*;
#[test]
fn a_single_uri_crl_distribution_point_is_read() {
// The real extnValue of the Entrust Class 3 Client CA: DistributionPoints with one
// full-name URI. `fullName [0]` is IMPLICIT over GeneralNames, so with a single
// GeneralName there is no inner SEQUENCE — a run of one element.
let uri = b"http://crl.entrust.net/2048ca.crl";
let value = der::seq(&[&der::seq(&[&der::tlv(tag::ctx(0), &der::tlv(tag::ctx(0), &der::tlv(tag::ctx_prim(6), uri)))])]);
let wrapped = der::tlv(tag::OCTET_STRING, &value);
let extn = Tlv::parse_all(&wrapped).unwrap();
let mut ext = Extensions::default();
ext.read("2.5.29.31", &extn);
assert_eq!(ext.crl_urls, vec!["http://crl.entrust.net/2048ca.crl".to_string()]);
// Two URIs in one full-name run.
let names = [der::tlv(tag::ctx_prim(6), uri), der::tlv(tag::ctx_prim(6), b"http://crl2.example/x.crl")].concat();
let two = der::seq(&[&der::seq(&[&der::tlv(tag::ctx(0), &der::tlv(tag::ctx(0), &names))])]);
let wrapped = der::tlv(tag::OCTET_STRING, &two);
let extn = Tlv::parse_all(&wrapped).unwrap();
let mut ext = Extensions::default();
ext.read("2.5.29.31", &extn);
assert_eq!(ext.crl_urls.len(), 2);
}
#[test]
fn a_malformed_extension_is_skipped_not_fatal() {
for garbage in [b"".as_slice(), &[0xFF, 0xFF, 0xFF], &[0x30, 0x99], b"not der"] {
let wrapped = der::tlv(tag::OCTET_STRING, garbage);
let extn = Tlv::parse_all(&wrapped).unwrap();
for oid in ["2.5.29.19", "2.5.29.15", "2.5.29.14", "2.5.29.35", "2.5.29.37", "1.3.6.1.5.5.7.1.1", "2.5.29.31"] {
let mut ext = Extensions::default();
ext.read(oid, &extn); // must not panic and must leave defaults
assert!(ext.ocsp_urls.is_empty() && ext.crl_urls.is_empty() && !ext.is_ca);
}
}
}
}
+62
View File
@@ -0,0 +1,62 @@
# Test data
Test-only keys and certificates, generated for these tests with OpenSSL 3. They protect nothing.
The password of every `.p12` is `test`.
| File | What |
|---|---|
| `rsa-aes.p12` | RSA 2048 self-signed "Test Signer RSA"; PBES2 AES-256-CBC, SHA-256 MAC (OpenSSL 3 default) |
| `rsa-legacy.p12` | the same key and certificate; RC2-40 certificates, 3DES key, SHA-1 MAC (`-legacy`) |
| `ec-p256.p12` | P-256 self-signed "Test Signer EC", no friendly name |
| `ec-legacy.p12` | `ec-p256.p12` re-exported with `-legacy` (3DES, SHA-1 MAC): the format macOS `security import` accepts |
| `ec-p384.p12` | P-384 self-signed "Test Signer P384"; AES-128-CBC, SHA-1 MAC |
| `chain.p12` | RSA 2048 "Ada Lovelace" issued by a P-256 test root, chain included |
| `chain-legacy.p12` | `chain.p12` re-exported with `-legacy`, for the Keychain tests |
| `rsa.crt.pem`, `ca.crt.pem` | the RSA certificate and the test root alone |
Regenerate with:
```sh
openssl req -x509 -newkey rsa:2048 -nodes -keyout rsa.key -out rsa.crt -days 3650 -subj "/CN=Test Signer RSA/O=PrintCraft Tests/C=US" -set_serial 1001
openssl pkcs12 -export -inkey rsa.key -in rsa.crt -out rsa-aes.p12 -passout pass:test -name "Test Signer RSA"
openssl pkcs12 -export -legacy -inkey rsa.key -in rsa.crt -out rsa-legacy.p12 -passout pass:test -name "Test Signer RSA"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout ec.key -out ec.crt -days 3650 -subj "/CN=Test Signer EC/O=PrintCraft Tests" -set_serial 2002
openssl pkcs12 -export -inkey ec.key -in ec.crt -out ec-p256.p12 -passout pass:test
openssl pkcs12 -export -legacy -inkey ec.key -in ec.crt -out ec-legacy.p12 -passout pass:test -name "Test Signer EC"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-384 -nodes -keyout ec3.key -out ec3.crt -days 3650 -subj "/CN=Test Signer P384" -set_serial 3003
openssl pkcs12 -export -inkey ec3.key -in ec3.crt -out ec-p384.p12 -passout pass:test -certpbe AES-128-CBC -keypbe AES-128-CBC -macalg sha1
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout ca.key -out ca.crt -days 3650 -subj "/CN=PrintCraft Test Root CA/O=PrintCraft Tests" -set_serial 1 -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl req -newkey rsa:2048 -nodes -keyout leaf.key -out leaf.csr -subj "/CN=Ada Lovelace/O=PrintCraft Tests/emailAddress=ada@example.com"
printf "basicConstraints=CA:FALSE\nkeyUsage=critical,digitalSignature,nonRepudiation\n" > leaf.ext
openssl x509 -req -in leaf.csr -CA ca.crt -CAkey ca.key -set_serial 77 -days 3650 -extfile leaf.ext -out leaf.crt
openssl pkcs12 -export -inkey leaf.key -in leaf.crt -certfile ca.crt -out chain.p12 -passout pass:test -name "Ada Lovelace"
openssl pkcs12 -export -legacy -inkey leaf.key -in leaf.crt -certfile ca.crt -out chain-legacy.p12 -passout pass:test -name "Ada Lovelace"
```
### RSA signature variants (hex constants in `tests/it/crypto.rs`)
Over SHA-256("hello") with the key of `rsa-aes.p12`:
`openssl dgst -sha256 -sign rsa.key` (standard DigestInfo); `openssl pkeyutl -sign -pkeyopt rsa_padding_mode:pkcs1`
over a hand-built DigestInfo without the NULL parameter, and over the bare digest;
`openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -sign rsa.key`.
### Other algorithms (hex constants in `tests/it/algorithms.rs`)
All sign the message `hello` with throw-away keys, OpenSSL 3.6. RSA ones use the key of `rsa-aes.p12`.
```sh
cms() { openssl cms -sign -binary -in msg -outform DER "$@"; }
cms -md sha3-256 -signer rsa.crt -inkey rsa.key -out c_rsa_sha3_256.der
cms -md sha512-256 -signer rsa.crt -inkey rsa.key -out c_rsa_sha512_256.der
cms -provider legacy -provider default -md ripemd160 -signer rsa.crt -inkey rsa.key -out c_rsa_ripemd160.der
cms -md sha256 -signer rsa.crt -inkey rsa.key -keyopt rsa_padding_mode:pss -keyopt rsa_mgf1_md:sha1 -keyopt rsa_pss_saltlen:20 -out c_pss_sha256_mgf1sha1_salt20.der
cms -md sha384 -signer rsa.crt -inkey rsa.key -keyopt rsa_padding_mode:pss -keyopt rsa_mgf1_md:sha256 -keyopt rsa_pss_saltlen:0 -out c_pss_sha384_mgf1sha256_salt0.der
openssl req -x509 -newkey ed25519 -nodes -keyout ed.key -out ed.crt -subj "/CN=Test Signer Ed25519"
cms -md sha512 -signer ed.crt -inkey ed.key -out c_ed25519.der
openssl ecparam -name brainpoolP512r1 -genkey -noout -out bp512.key
openssl req -x509 -new -key bp512.key -sha512 -out bp512.crt -subj "/CN=Test Signer BP512"
cms -md sha512 -signer bp512.crt -inkey bp512.key -out c_bp512_sha512.der
cms -md sha3-256 -signer bp512.crt -inkey bp512.key -out c_bp512_sha3_256.der
openssl dgst -sha3-256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -sign rsa.key -out pss_sha3_256.sig msg
openssl dgst -sha512-256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:0 -sign rsa.key -out pss_sha512_256.sig msg
```
+12
View File
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIByzCCAXKgAwIBAgIBATAKBggqhkjOPQQDAjA9MSAwHgYDVQQDDBdQcmludENy
YWZ0IFRlc3QgUm9vdCBDQTEZMBcGA1UECgwQUHJpbnRDcmFmdCBUZXN0czAeFw0y
NjEwMDIwNjE3NDlaFw0zNjA5MjkwNjE3NDlaMD0xIDAeBgNVBAMMF1ByaW50Q3Jh
ZnQgVGVzdCBSb290IENBMRkwFwYDVQQKDBBQcmludENyYWZ0IFRlc3RzMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEHzN6qxrMUelog5zRgDla65Dp0XLVvK+0I/0M
uNhWDorE1CqrUrVqLkYBPSIWFfu36o0h5vF3RI0JBq932TsrS6NjMGEwHQYDVR0O
BBYEFENYzD54+vd7a43BOQZRdfvhb8DwMB8GA1UdIwQYMBaAFENYzD54+vd7a43B
OQZRdfvhb8DwMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqG
SM49BAMCA0cAMEQCIAR4KSUsZM8EG6IiDCtbVvF7aadd652Hbs5ZVguYAJYPAiAX
zSun5lH712+mzakkZtbfhuutsYYliZt84UuiSI+SpA==
-----END CERTIFICATE-----
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUzCCAjugAwIBAgICA+kwDQYJKoZIhvcNAQELBQAwQjEYMBYGA1UEAwwPVGVz
dCBTaWduZXIgUlNBMRkwFwYDVQQKDBBQcmludENyYWZ0IFRlc3RzMQswCQYDVQQG
EwJVUzAeFw0yNjEwMDIwNjE3MzRaFw0zNjA5MjkwNjE3MzRaMEIxGDAWBgNVBAMM
D1Rlc3QgU2lnbmVyIFJTQTEZMBcGA1UECgwQUHJpbnRDcmFmdCBUZXN0czELMAkG
A1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDL4UlgowrY
WL1zAdrL1hQ38oFd/flQYncADv5AK45helQhv9zwGTIbuAuGJPtydzeUvKgY3Rdl
WLJFJeaudoN1iJ6ilR2i0gLBEYF2QhCqebgHkNE5up1wCKmV3KZMTWJZ0eKlL+ae
kSOOVmAowXHX5iWo8fv2EdIN2VYqrJe0UBu4HkRFybxesQrcx7d6CQrzlyS0WZns
LGAJARyOGofztI6zFly7JwdeqBtZ0PIz/XeLwII6wf2Sd2IwO2KsJ5X3HarN80M9
BxASkaSDkUMoLsB1Z6fKrPwGYX5BUKVMXj5b6b4cq5Cd0a7kT8YMuCQJrLIiSorY
i7pvLAlfiC2hAgMBAAGjUzBRMB0GA1UdDgQWBBRZCY3Rc9d2HvE5zJUEt9yi19b5
nzAfBgNVHSMEGDAWgBRZCY3Rc9d2HvE5zJUEt9yi19b5nzAPBgNVHRMBAf8EBTAD
AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCbH/lGas10CJTyIXuyA+m12nkqIpVVvd8W
s7Mw+wa3J3sGcBUKvMdeMl753Yclyow6w3FO8a7o2Ra1qG0YzU4ml7WKOlCMHFgl
n9Idwj8xsqbNIpkZ6gNCluABY/NcxZEv6XxfxC0b1cKbbIXybnYgu1xiogZ4Oy9E
0ySTcjSGFo17TBkUwsBDz2erTeMzhkU0UKBxt6EGWdR6uFJOw6SKf1HjiGxW1jJD
fi6jC3CmChnV6xOuhWu661QDhYBRLV2r2EDmNiKwaDq0ELS1kSIMEwMTorHHGR0X
1d+swWSwo2DPABkXh1WJ54usCpgSqDYnbJf5clzhz7ZThhPO/kJC
-----END CERTIFICATE-----
+140
View File
@@ -0,0 +1,140 @@
//! Signatures by OpenSSL 3.6 with the algorithms beyond SHA-2/RSA/NIST-curve ECDSA: SHA-3,
//! SHA-512/256, RIPEMD-160, RSA-PSS with other MGF1 hashes and salt lengths, Ed25519 and
//! brainpoolP512r1. Every vector signs the message "hello" with a throw-away key (see
//! tests/data/README.md for the commands). The CMS files are detached, `openssl cms -sign -binary`.
use craft_pkcs7::cms::SignedData;
use craft_pkcs7::keys::{DigestAlg, PssParams, Scheme};
fn unhex(s: &str) -> Vec<u8> {
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
}
const C_RSA_SHA3_256: &str = "308205e906092a864886f70d010702a08205da308205d6020101310d300b0609608648016503040208300b06092a864886f70d010701a0820357308203533082023ba003020102020203e9300d06092a864886f70d01010b050030423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553301e170d3236313030323036313733345a170d3336303932393036313733345a30423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100cbe14960a30ad858bd7301dacbd61437f2815dfdf9506277000efe402b8e617a5421bfdcf019321bb80b8624fb72773794bca818dd176558b24525e6ae768375889ea2951da2d202c11181764210aa79b80790d139ba9d7008a995dca64c4d6259d1e2a52fe69e91238e566028c171d7e625a8f1fbf611d20dd9562aac97b4501bb81e4445c9bc5eb10adcc7b77a090af39724b45999ec2c6009011c8e1a87f3b48eb3165cbb27075ea81b59d0f233fd778bc0823ac1fd927762303b62ac2795f71daacdf3433d07101291a4839143282ec07567a7caacfc06617e4150a54c5e3e5be9be1cab909dd1aee44fc60cb82409acb2224a8ad88bba6f2c095f882da10203010001a3533051301d0603551d0e0416041459098dd173d7761ef139cc9504b7dca2d7d6f99f301f0603551d2304183016801459098dd173d7761ef139cc9504b7dca2d7d6f99f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101009b1ff9466acd740894f2217bb203e9b5da792a229555bddf16b3b330fb06b7277b0670150abcc75e325ef9dd8725ca8c3ac3714ef1aee8d916b5a86d18cd4e2697b58a3a508c1c58259fd21dc23f31b2a6cd229919ea034296e00163f35cc5912fe97c5fc42d1bd5c29b6c85f26e7620bb5c62a206783b2f44d32493723486168d7b4c1914c2c043cf67ab4de33386453450a071b7a10659d47ab8524ec3a48a7f51e3886c56d632437e2ea30b70a60a19d5eb13ae856bbaeb54038580512d5dabd840e63622b0683ab410b4b591220c130313a2b1c7191d17d5dfacc164b0a360cf001917875589e78bac0a9812a836276c97f9725ce1cfb6538613cefe42423182025830820254020101304830423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553020203e9300b0609608648016503040208a081e4301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303831315a302f06092a864886f70d010904312204203338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128300d06092a864886f70d01010105000482010004884153af307d5b12f87d673598b15b1c722ed15091d97af4d36e40942b009f849b55e4429ee550ad7f001db9a1cb293c2319b253a1d998266897f27c35e9670c87080a71e932beb932d594c4f9735e5dd20d3145db26c4352a1ebea7c7692e3272032e1a1736445eb238b93825315369beed81839f71525848fb8a725b9804cc2ac6c8756754f46fc182df5a1bf0dbc97bcf8a5dbc0d4d9da26f7b68029d1e7fc35893958d6c7084f3199605e103788e2a61cd8a68847588d8607a1a31cb1106753d35109cdd309a098553dfd0f33c2269fd5b396318f35e06c4d67be6a7b05dbacd2c265a3bbfa11c1a4c192052e4130aac97addb703514d43e62dc8b629b";
const C_RSA_SHA512_256: &str = "308205e906092a864886f70d010702a08205da308205d6020101310d300b0609608648016503040206300b06092a864886f70d010701a0820357308203533082023ba003020102020203e9300d06092a864886f70d01010b050030423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553301e170d3236313030323036313733345a170d3336303932393036313733345a30423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100cbe14960a30ad858bd7301dacbd61437f2815dfdf9506277000efe402b8e617a5421bfdcf019321bb80b8624fb72773794bca818dd176558b24525e6ae768375889ea2951da2d202c11181764210aa79b80790d139ba9d7008a995dca64c4d6259d1e2a52fe69e91238e566028c171d7e625a8f1fbf611d20dd9562aac97b4501bb81e4445c9bc5eb10adcc7b77a090af39724b45999ec2c6009011c8e1a87f3b48eb3165cbb27075ea81b59d0f233fd778bc0823ac1fd927762303b62ac2795f71daacdf3433d07101291a4839143282ec07567a7caacfc06617e4150a54c5e3e5be9be1cab909dd1aee44fc60cb82409acb2224a8ad88bba6f2c095f882da10203010001a3533051301d0603551d0e0416041459098dd173d7761ef139cc9504b7dca2d7d6f99f301f0603551d2304183016801459098dd173d7761ef139cc9504b7dca2d7d6f99f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101009b1ff9466acd740894f2217bb203e9b5da792a229555bddf16b3b330fb06b7277b0670150abcc75e325ef9dd8725ca8c3ac3714ef1aee8d916b5a86d18cd4e2697b58a3a508c1c58259fd21dc23f31b2a6cd229919ea034296e00163f35cc5912fe97c5fc42d1bd5c29b6c85f26e7620bb5c62a206783b2f44d32493723486168d7b4c1914c2c043cf67ab4de33386453450a071b7a10659d47ab8524ec3a48a7f51e3886c56d632437e2ea30b70a60a19d5eb13ae856bbaeb54038580512d5dabd840e63622b0683ab410b4b591220c130313a2b1c7191d17d5dfacc164b0a360cf001917875589e78bac0a9812a836276c97f9725ce1cfb6538613cefe42423182025830820254020101304830423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553020203e9300b0609608648016503040206a081e4301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303831315a302f06092a864886f70d01090431220420e30d87cfa2a75db545eac4d61baf970366a8357c7f72fa95b52d0accb698f13a307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128300d06092a864886f70d010101050004820100293b265b6bf4eaa2d3b3777c6116fd922696578bd835f77728809758d7af89aa96afab9da9e320183c63ead9e499508d1f892aea5e1e5b96645b838062f4b14062fa417bdd23257021c05de0e9614ed8141715e9354f21f2ab389194a447c80dda6c87ea80d551abbb0cfc450663359c747cdc41aa296f1a71f4e2b53e97c94d30aa094edbd0fefe53835e550a1a9c55404f8f41d32cc8033df7e0b877008ac6f75f02a6071b02647e5adf756a56f683f452d5f9a180fa62f7c7089019b99037b8e3488798238b85ae5b773b4dfd0d157afe4688209520244bd77a56dd98bda615b2e00312a1a5595c439e84af24749e6d6e44abd7da2794523faba3d0acb737";
const C_RSA_RIPEMD160: &str = "308205d906092a864886f70d010702a08205ca308205c6020101310b300906052b240302010500300b06092a864886f70d010701a0820357308203533082023ba003020102020203e9300d06092a864886f70d01010b050030423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553301e170d3236313030323036313733345a170d3336303932393036313733345a30423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100cbe14960a30ad858bd7301dacbd61437f2815dfdf9506277000efe402b8e617a5421bfdcf019321bb80b8624fb72773794bca818dd176558b24525e6ae768375889ea2951da2d202c11181764210aa79b80790d139ba9d7008a995dca64c4d6259d1e2a52fe69e91238e566028c171d7e625a8f1fbf611d20dd9562aac97b4501bb81e4445c9bc5eb10adcc7b77a090af39724b45999ec2c6009011c8e1a87f3b48eb3165cbb27075ea81b59d0f233fd778bc0823ac1fd927762303b62ac2795f71daacdf3433d07101291a4839143282ec07567a7caacfc06617e4150a54c5e3e5be9be1cab909dd1aee44fc60cb82409acb2224a8ad88bba6f2c095f882da10203010001a3533051301d0603551d0e0416041459098dd173d7761ef139cc9504b7dca2d7d6f99f301f0603551d2304183016801459098dd173d7761ef139cc9504b7dca2d7d6f99f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101009b1ff9466acd740894f2217bb203e9b5da792a229555bddf16b3b330fb06b7277b0670150abcc75e325ef9dd8725ca8c3ac3714ef1aee8d916b5a86d18cd4e2697b58a3a508c1c58259fd21dc23f31b2a6cd229919ea034296e00163f35cc5912fe97c5fc42d1bd5c29b6c85f26e7620bb5c62a206783b2f44d32493723486168d7b4c1914c2c043cf67ab4de33386453450a071b7a10659d47ab8524ec3a48a7f51e3886c56d632437e2ea30b70a60a19d5eb13ae856bbaeb54038580512d5dabd840e63622b0683ab410b4b591220c130313a2b1c7191d17d5dfacc164b0a360cf001917875589e78bac0a9812a836276c97f9725ce1cfb6538613cefe42423182024a30820246020101304830423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553020203e9300906052b240302010500a081d8301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303831315a302306092a864886f70d01090431160414108f07b8382412612c048d07d13f814118445acd307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128300d06092a864886f70d0101010500048201007373c1c0747a77c073a62cf99b97ac43c15677c668ebd6c965296ec1a8c30f33e5b4cb35c652db0c0fe3184259bae01d6f7df64026ee4b58ac5005c46cbf603e45524fba7bb54e81cf0b10a5e7843dcbb8a72d60ffa9a503d0bc5c2306374688b95ff7f4d31366385a69fee1f6c657bd3a82418c56afc53b30446ef0f5776e17769761f1812379c83835f17614f1b2e9adf2ad5cc645cd861cd1e6cf2ebe14a963f208655769e1d6139b4bdeaecc366b0e3924ec46fcfdef05979674fdfb33ada6a0f185abd36c6cd3ff3ba6684b99a72154d933ffc90e8950d72066bf5e3524c09632ebdac06c2944084573c2a6e5344c6a09643f832441a93ef7dc39affd45";
const C_PSS_SHA256_MGF1SHA1_SALT20: &str = "308205fa06092a864886f70d010702a08205eb308205e7020101310d300b0609608648016503040201300b06092a864886f70d010701a0820357308203533082023ba003020102020203e9300d06092a864886f70d01010b050030423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553301e170d3236313030323036313733345a170d3336303932393036313733345a30423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100cbe14960a30ad858bd7301dacbd61437f2815dfdf9506277000efe402b8e617a5421bfdcf019321bb80b8624fb72773794bca818dd176558b24525e6ae768375889ea2951da2d202c11181764210aa79b80790d139ba9d7008a995dca64c4d6259d1e2a52fe69e91238e566028c171d7e625a8f1fbf611d20dd9562aac97b4501bb81e4445c9bc5eb10adcc7b77a090af39724b45999ec2c6009011c8e1a87f3b48eb3165cbb27075ea81b59d0f233fd778bc0823ac1fd927762303b62ac2795f71daacdf3433d07101291a4839143282ec07567a7caacfc06617e4150a54c5e3e5be9be1cab909dd1aee44fc60cb82409acb2224a8ad88bba6f2c095f882da10203010001a3533051301d0603551d0e0416041459098dd173d7761ef139cc9504b7dca2d7d6f99f301f0603551d2304183016801459098dd173d7761ef139cc9504b7dca2d7d6f99f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101009b1ff9466acd740894f2217bb203e9b5da792a229555bddf16b3b330fb06b7277b0670150abcc75e325ef9dd8725ca8c3ac3714ef1aee8d916b5a86d18cd4e2697b58a3a508c1c58259fd21dc23f31b2a6cd229919ea034296e00163f35cc5912fe97c5fc42d1bd5c29b6c85f26e7620bb5c62a206783b2f44d32493723486168d7b4c1914c2c043cf67ab4de33386453450a071b7a10659d47ab8524ec3a48a7f51e3886c56d632437e2ea30b70a60a19d5eb13ae856bbaeb54038580512d5dabd840e63622b0683ab410b4b591220c130313a2b1c7191d17d5dfacc164b0a360cf001917875589e78bac0a9812a836276c97f9725ce1cfb6538613cefe42423182026930820265020101304830423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553020203e9300b0609608648016503040201a081e4301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303831315a302f06092a864886f70d010904312204202cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128301e06092a864886f70d01010a3011a00f300d0609608648016503040201050004820100159800c2d48396d32cb1e3dddf841f0bad186da462fc45cab82bf6d2d4bb8f82d200560b16f2ecfea672af07af82ae967453193e130071033d47bba03a7d81685b29812293a6d33f4126103ebcd4552ced5c8d13ea6fcac5920b5ab0641b1884919cbed2cb280cdc19569094e0ecf8fdebe0b53a546f46cdb2c3c41b030e520302dae89d9d10206c78ad0fd2ead13dba8b067abf185de957ba4daaa8de7437ca014567d7cfbe8d0f6f9cb8de8dceae5d7292ed731170323e0def553653d4af11239d52eacf1f1a406e6d8c1d7f597af25a2154582d1ad2e9c3b8951ba1350ee3322ef76ac988829de28917c7b153fb493913195250e95185ecac4081e6f18355";
const C_PSS_SHA384_MGF1SHA256_SALT0: &str = "3082062d06092a864886f70d010702a082061e3082061a020101310d300b0609608648016503040202300b06092a864886f70d010701a0820357308203533082023ba003020102020203e9300d06092a864886f70d01010b050030423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553301e170d3236313030323036313733345a170d3336303932393036313733345a30423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100cbe14960a30ad858bd7301dacbd61437f2815dfdf9506277000efe402b8e617a5421bfdcf019321bb80b8624fb72773794bca818dd176558b24525e6ae768375889ea2951da2d202c11181764210aa79b80790d139ba9d7008a995dca64c4d6259d1e2a52fe69e91238e566028c171d7e625a8f1fbf611d20dd9562aac97b4501bb81e4445c9bc5eb10adcc7b77a090af39724b45999ec2c6009011c8e1a87f3b48eb3165cbb27075ea81b59d0f233fd778bc0823ac1fd927762303b62ac2795f71daacdf3433d07101291a4839143282ec07567a7caacfc06617e4150a54c5e3e5be9be1cab909dd1aee44fc60cb82409acb2224a8ad88bba6f2c095f882da10203010001a3533051301d0603551d0e0416041459098dd173d7761ef139cc9504b7dca2d7d6f99f301f0603551d2304183016801459098dd173d7761ef139cc9504b7dca2d7d6f99f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101009b1ff9466acd740894f2217bb203e9b5da792a229555bddf16b3b330fb06b7277b0670150abcc75e325ef9dd8725ca8c3ac3714ef1aee8d916b5a86d18cd4e2697b58a3a508c1c58259fd21dc23f31b2a6cd229919ea034296e00163f35cc5912fe97c5fc42d1bd5c29b6c85f26e7620bb5c62a206783b2f44d32493723486168d7b4c1914c2c043cf67ab4de33386453450a071b7a10659d47ab8524ec3a48a7f51e3886c56d632437e2ea30b70a60a19d5eb13ae856bbaeb54038580512d5dabd840e63622b0683ab410b4b591220c130313a2b1c7191d17d5dfacc164b0a360cf001917875589e78bac0a9812a836276c97f9725ce1cfb6538613cefe42423182029c30820298020101304830423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553020203e9300b0609608648016503040202a081f4301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303831315a303f06092a864886f70d0109043132043059e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128304106092a864886f70d01010a3034a00f300d06096086480165030402020500a11c301a06092a864886f70d010108300d06096086480165030402010500a203020100048201003ce51d5b4168c11bb39613de1c2633c8daa86622bdacc1313cf6d597bfb9c15311dbdde51b0f0d3ff67dd922e13e79b84a414c8c9cfffbe35bcf83eefb32b7c5cc371dc56af704cf5b3269dca13c48f27d64a06bc98710f1e3f254b23ba7b41e7ec7027087dea4f4fbb9119d86926220f93d7e8d7be9ad3c5c41e3a4e1d94f09a9f494aac93e13429eec6358d77d83b937686fd41beaeb4d551c3a48ac743ba27592a5435aa8add806200ea3ec61712b8ba4580f2bf80c488b3bc5020c13ecac37a68e3ad3895379a788dbd8a080ed19178dcdaf35d0d8a45d1c7c81e4ddc6916eb74266bba6c3b616b421f78ceeddda3331f3148b6e6145f5b2c4eca9e35b41";
const C_ED25519: &str = "3082032c06092a864886f70d010702a082031d30820319020101310d300b0609608648016503040203300b06092a864886f70d010701a08201553082015130820103a003020102021447398d434ffa904166d2a7d30fe4df35890dab29300506032b6570301e311c301a06035504030c1354657374205369676e65722045643235353139301e170d3236313030373130303735365a170d3336313030343130303735365a301e311c301a06035504030c1354657374205369676e65722045643235353139302a300506032b657003210007233d4f6ee95134d0232c25b1b5bef159b0c0c32c8911773fa68e99bfee67c2a3533051301d0603551d0e04160414e5fdf0f2f76d4aa4f9cb70dc9c3d6066aa6efb23301f0603551d23041830168014e5fdf0f2f76d4aa4f9cb70dc9c3d6066aa6efb23300f0603551d130101ff040530030101ff300506032b657003410085df55271628b581a5b5c6531253eeca4a3aea556007bfc3cb15132b3ddfc117c5c632a150a025cde2a509346e0497411169812dea45b22257c5d50eaf8c2a0c3182019d308201990201013036301e311c301a06035504030c1354657374205369676e65722045643235353139021447398d434ffa904166d2a7d30fe4df35890dab29300b0609608648016503040203a0820104301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303833395a304f06092a864886f70d010904314204409b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128300506032b65700440e3924e19b88b24d8b614a29cee8c3190c9b0a753273705b794bc2e37ca1e5b1d567ea724e5c4a4189ad102ea51d0506e645aed78ed1835e94eb1543cc9a57c06";
const C_BP512_SHA512: &str = "3082043a06092a864886f70d010702a082042b30820427020101310d300b0609608648016503040203300b06092a864886f70d010701a08202173082021330820176a003020102021417327c0f542434a70893e2caaf25132f86d924a8300a06082a8648ce3d040304301c311a301806035504030c1154657374205369676e6572204250353132301e170d3236313030373130303735365a170d3336313030343130303735365a301c311a301806035504030c1154657374205369676e657220425035313230819b301406072a8648ce3d020106092b240303020801010d03818200042bcc6a56c9e35aff784fb796021f73923d0d8fafe4573df9a3a834f04dabd67fcaaa23c2a20d15a95d42db3ffcb9e9bc5a09cc0d052afdb830e01600782e1c2a307d843f1d804a2083891608b5f05e34273fb00fca1f8a0ef82a821bbac7194ff34c2ab09f6efa4fab871d8150a0fd1625990bd04afcdee025eb585fbb2178c2a3533051301d0603551d0e041604149e51d60b2b4c8459d59e41a99d57a79503b5362d301f0603551d230418301680149e51d60b2b4c8459d59e41a99d57a79503b5362d300f0603551d130101ff040530030101ff300a06082a8648ce3d04030403818a003081860241008126d9a4de09333ad8db822c0718d7a1d1c3a41fdc2f8d5f551109092bd24b8d60c305885a1aa91faf3d82208efa5e92ad0fd16e145dca5da3e8ef026242e1eb0241009b124b54fbc4793a50cc6bd9af3e2d0f459943a71999d9175522ff9668844cce4600ffddbef2a2ca4f49c0c3389d37c6ceed14dea1c3c7320e76ffff327ffd1c318201e9308201e50201013034301c311a301806035504030c1154657374205369676e6572204250353132021417327c0f542434a70893e2caaf25132f86d924a8300b0609608648016503040203a0820104301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303831315a304f06092a864886f70d010904314204409b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128300a06082a8648ce3d040304048188308185024066a840051f4caad67c7df0d7db8454f3dbe9184ead3287447c9a5f62eaaaf0cc538979d228b8943d0abd103ac9d5c2ea6aa64384ffe284ce0233dc13b25f6594024100900d2f6251b75aa6224228d076870d014824f79c9670c6d83e8772105c0668c1d96f617c9b09057da442df5aae0dca9dc127cd0ce3de7bfe165270b018143fd6";
const C_BP512_SHA3_256: &str = "3082041a06092a864886f70d010702a082040b30820407020101310d300b0609608648016503040208300b06092a864886f70d010701a08202173082021330820176a003020102021417327c0f542434a70893e2caaf25132f86d924a8300a06082a8648ce3d040304301c311a301806035504030c1154657374205369676e6572204250353132301e170d3236313030373130303735365a170d3336313030343130303735365a301c311a301806035504030c1154657374205369676e657220425035313230819b301406072a8648ce3d020106092b240303020801010d03818200042bcc6a56c9e35aff784fb796021f73923d0d8fafe4573df9a3a834f04dabd67fcaaa23c2a20d15a95d42db3ffcb9e9bc5a09cc0d052afdb830e01600782e1c2a307d843f1d804a2083891608b5f05e34273fb00fca1f8a0ef82a821bbac7194ff34c2ab09f6efa4fab871d8150a0fd1625990bd04afcdee025eb585fbb2178c2a3533051301d0603551d0e041604149e51d60b2b4c8459d59e41a99d57a79503b5362d301f0603551d230418301680149e51d60b2b4c8459d59e41a99d57a79503b5362d300f0603551d130101ff040530030101ff300a06082a8648ce3d04030403818a003081860241008126d9a4de09333ad8db822c0718d7a1d1c3a41fdc2f8d5f551109092bd24b8d60c305885a1aa91faf3d82208efa5e92ad0fd16e145dca5da3e8ef026242e1eb0241009b124b54fbc4793a50cc6bd9af3e2d0f459943a71999d9175522ff9668844cce4600ffddbef2a2ca4f49c0c3389d37c6ceed14dea1c3c7320e76ffff327ffd1c318201c9308201c50201013034301c311a301806035504030c1154657374205369676e6572204250353132021417327c0f542434a70893e2caaf25132f86d924a8300b0609608648016503040208a081e4301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3236313030373130303831315a302f06092a864886f70d010904312204203338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392307906092a864886f70d01090f316c306a300b060960864801650304012a300b0609608648016503040116300b0609608648016503040102300a06082a864886f70d0307300e06082a864886f70d030202020080300d06082a864886f70d0302020140300706052b0e030207300d06082a864886f70d0302020128300b060960864801650304030a04818830818502410097943cf78c3e6440f03e00d7ca3a96d986190e7cc56db1b1d3dd806810c9d731d5ab576df900173082372fd1beeb206da3bf1958c11c375f5a38aa2766f083a402402c495a25cc16836322d724606b34912ed08904c97e2c1557ca13796ce02ad37d4a2bd8ce0001f020fb131c3c4e7602c8346e30b0a23a5f030d6885aa07a90c40";
const PSS_SHA3_256: &str = "be8d2369801beaea9c45935787bd005a6d6085003cb235c5bf8c867cafd8edf9ed288aa3a16b019d64b0668be302a5ca6fa0388774a85471804154f9411fd0abd6738b6fb5a7acf38b60fe70d65e1f93447f57e34fdb502c741001c184113117cd3b7daec1614ebdd152f071117bb1012587f93df00321cc22d1ac53788e1c9906c2c5a31f9364220c1b1e913b2ac3409aca39296a51b4fbc192a87d671dc83726831fa2e59ce1783c09fee8c95d53d8caf50da32d896839a03ab4ff4a390a6dee006dfa686c60368bfc2d64e5d974acb3c022edb1a9fd57a167ce03a31de8eef3a9a7d9460013d636dab53b00ed6e24bf736cdfd56c2be7e8b8aeaeabf74842";
const PSS_SHA512_256: &str = "4aeff996324c1aae8216497d7be896d949361f80ae5cfeee82a959f9a8ff7c8d2e0ec783b435f1952dc768ea97a949acca1783210e8754a71c9c2255689d370a349dfb9845e69595d3386ee88aafd2369ecdbba53de692eacc68fd5f0e3e2451f6fe000d680d860d235958f666eddfb4dba69ed730b6f9f82889617e744c0ec4fda72fb9e8f32190913dc77bb281713c5f45a7bd9c346645b401fb4a58348d4eb86511bcdeeca113cb9a9c8c4b587da763ce16b104580dee0ccbbac4a3e969ea53835a8067bdd9b1f8109940fb61abb8b03381b5272c2bbc381a50961130fb3a7c4fe02689111ac76f3a4c1f4439d25326aeec75afc3839478b3289a2ff2482d";
/// A CMS signature must verify, name the digest it should, and fail if the signature is damaged.
fn check_cms(name: &str, hex: &str, digest: DigestAlg, scheme: impl Fn(Scheme) -> bool) {
let sd = SignedData::parse(&unhex(hex)).unwrap_or_else(|e| panic!("{name}: {e}"));
let s = &sd.signer;
assert_eq!(s.digest, digest, "{name}");
assert!(scheme(s.scheme), "{name}: {:?}", s.scheme);
let content = digest.digest(&[b"hello"]);
assert_eq!(s.message_digest.as_deref(), Some(&content[..]), "{name}: the message digest attribute is our digest of the content");
let cert = sd.signer_certificate().unwrap_or_else(|| panic!("{name}: signer certificate"));
assert!(sd.verify_signature(cert, &content).unwrap_or_else(|e| panic!("{name}: {e}")), "{name}");
let mut bad = sd.clone();
let mid = bad.signer.signature.len() / 2;
bad.signer.signature[mid] ^= 0x10;
assert!(!bad.verify_signature(cert, &content).unwrap_or(false), "{name}: damaged signature");
}
#[test]
fn rsa_with_sha3_sha512_256_and_ripemd160() {
check_cms("sha3-256", C_RSA_SHA3_256, DigestAlg::Sha3_256, |s| s == Scheme::RsaPkcs1);
check_cms("sha512/256", C_RSA_SHA512_256, DigestAlg::Sha512_256, |s| s == Scheme::RsaPkcs1);
check_cms("ripemd160", C_RSA_RIPEMD160, DigestAlg::Ripemd160, |s| s == Scheme::RsaPkcs1);
}
#[test]
fn only_weak_digests_leave_a_note_when_a_signature_validates() {
let notes_of = |hex: &str| {
let sd = SignedData::parse(&unhex(hex)).unwrap();
let cert = sd.signer_certificate().unwrap();
let mut notes = Vec::new();
assert!(sd.verify_signature_noting(cert, &sd.signer.digest.digest(&[b"hello"]), &mut notes).unwrap());
notes
};
// RIPEMD-160 is named; SHA3-256 and SHA-512/256 are not.
let weak = notes_of(C_RSA_RIPEMD160);
assert_eq!(weak, [DigestAlg::Ripemd160.weakness().unwrap()], "{weak:?}");
assert!(notes_of(C_RSA_SHA3_256).is_empty());
assert!(notes_of(C_RSA_SHA512_256).is_empty());
}
#[test]
fn rsa_pss_with_its_declared_parameters() {
check_cms("pss sha256 mgf1-sha1 salt 20", C_PSS_SHA256_MGF1SHA1_SALT20, DigestAlg::Sha256, |s| {
s == Scheme::RsaPss(PssParams { mgf: DigestAlg::Sha1, salt_len: Some(20) })
});
check_cms("pss sha384 mgf1-sha256 salt 0", C_PSS_SHA384_MGF1SHA256_SALT0, DigestAlg::Sha384, |s| {
s == Scheme::RsaPss(PssParams { mgf: DigestAlg::Sha256, salt_len: Some(0) })
});
// PSS over SHA-3 and SHA-512/256 digests, MGF1 with the same hash.
let id = craft_pkcs7::pkcs12::open(&std::fs::read(format!("{}/tests/data/rsa-aes.p12", env!("CARGO_MANIFEST_DIR"))).unwrap(), "test").unwrap();
let key = &id.certificate.public_key;
for (name, alg, salt, sig) in [("sha3-256", DigestAlg::Sha3_256, 32, PSS_SHA3_256), ("sha512/256", DigestAlg::Sha512_256, 0, PSS_SHA512_256)] {
let digest = alg.digest(&[b"hello"]);
let pss = Scheme::RsaPss(PssParams { mgf: alg, salt_len: Some(salt) });
assert!(key.verify(pss, alg, &digest, &unhex(sig)).unwrap(), "{name}");
assert!(!key.verify(pss, alg, &alg.digest(&[b"hellp"]), &unhex(sig)).unwrap(), "{name}: other message");
let wrong_mgf = Scheme::RsaPss(PssParams { mgf: DigestAlg::Sha1, salt_len: Some(salt) });
assert!(!key.verify(wrong_mgf, alg, &digest, &unhex(sig)).unwrap(), "{name}: the MGF1 hash matters");
}
}
#[test]
fn ed25519_signs_the_signed_attributes_themselves() {
check_cms("ed25519", C_ED25519, DigestAlg::Sha512, |s| s == Scheme::Ed25519);
let sd = SignedData::parse(&unhex(C_ED25519)).unwrap();
let cert = sd.signer_certificate().unwrap();
assert_eq!(cert.public_key.describe(), "Ed25519");
assert!(cert.is_self_signed(), "the certificate's own Ed25519 signature checks");
// A digest can't be verified with Ed25519: it signs the message.
let r = cert.public_key.verify(Scheme::Ed25519, DigestAlg::Sha512, &[0; 64], &sd.signer.signature);
assert!(matches!(r, Err(craft_pkcs7::Error::Unsupported(_))));
}
#[test]
fn ecdsa_on_brainpool_p512r1() {
check_cms("bp512 sha512", C_BP512_SHA512, DigestAlg::Sha512, |s| s == Scheme::Ecdsa);
check_cms("bp512 sha3-256", C_BP512_SHA3_256, DigestAlg::Sha3_256, |s| s == Scheme::Ecdsa);
let sd = SignedData::parse(&unhex(C_BP512_SHA512)).unwrap();
let cert = sd.signer_certificate().unwrap();
assert_eq!(cert.public_key.describe(), "ECDSA brainpoolP512r1");
assert!(cert.is_self_signed(), "certificate signed with ecdsa-with-SHA512 on brainpoolP512r1");
let spki = cert.public_key.spki();
let back = craft_pkcs7::PublicKey::from_spki(&craft_pkcs7::der::Tlv::parse_all(&spki).unwrap()).unwrap();
assert_eq!(back, cert.public_key, "re-encodes the SubjectPublicKeyInfo");
}
/// `a + b` for big-endian numbers of equal width, or `None` if it does not fit.
fn add_be(a: &[u8], b: &[u8]) -> Option<Vec<u8>> {
let mut out = vec![0u8; a.len()];
let mut carry = 0u16;
for i in (0..a.len()).rev() {
let sum = u16::from(a[i]) + u16::from(b[i]) + carry;
out[i] = sum as u8;
carry = sum >> 8;
}
(carry == 0).then_some(out)
}
#[test]
fn brainpool_p512r1_keys_must_have_coordinates_reduced_modulo_p() {
use craft_pkcs7::PublicKey;
const P: &str =
"aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3";
let sd = SignedData::parse(&unhex(C_BP512_SHA512)).unwrap();
let cert = sd.signer_certificate().unwrap();
let content = DigestAlg::Sha512.digest(&[b"hello"]);
assert!(sd.verify_signature(cert, &content).unwrap(), "the key as written verifies");
let PublicKey::BrainpoolP512(point) = &cert.public_key else { panic!("a brainpoolP512r1 key") };
let (x, y) = (&point[1..65], &point[65..]);
let p = unhex(P);
// The same point written with x + p (or y + p): congruent, but not the canonical encoding.
for (what, x, y) in [("x + p", add_be(x, &p).unwrap(), y.to_vec()), ("y + p", x.to_vec(), add_be(y, &p).unwrap())] {
let mut forged = cert.clone();
forged.public_key = PublicKey::BrainpoolP512([&[4u8][..], &x, &y].concat());
let r = sd.verify_signature(&forged, &content);
assert!(matches!(r, Err(craft_pkcs7::Error::Malformed(_))), "{what}: {r:?}");
}
}
+76
View File
@@ -0,0 +1,76 @@
//! CMS read as BER: the bounds on what a crafted /Contents can make the reader do.
//!
//! The BER reader itself is `der::Tlv::parse_ber`; these tests go through `SignedData::parse`,
//! the way a PDF's signature reaches it.
use craft_pkcs7::cms::SignedData;
use craft_pkcs7::der::{self, tag};
/// `levels` constructed OCTET STRINGs inside one another, each with a definite length, around one
/// empty primitive OCTET STRING. Built from the inside out in linear time.
fn nested_octet_strings(levels: usize) -> Vec<u8> {
let header = |len: usize| -> Vec<u8> {
let mut h = vec![tag::OCTET_STRING_SEGMENTS];
if len < 0x80 {
h.push(len as u8);
} else {
let be: Vec<u8> = len.to_be_bytes().into_iter().skip_while(|b| *b == 0).collect();
h.push(0x80 | be.len() as u8);
h.extend(be);
}
h
};
// The content length of each level, the innermost (the empty `04 00`, 2 bytes) first.
let mut inner = vec![2usize];
for _ in 0..levels {
let last = inner.last().copied().unwrap_or(2);
inner.push(last + header(last).len());
}
let mut out = Vec::new();
for len in inner.iter().rev().skip(1) {
out.extend(header(*len));
}
out.extend([tag::OCTET_STRING, 0x00]);
out
}
/// A ContentInfo holding SignedData whose encapsulated content is `econtent`. It has no
/// SignerInfo, so parsing always ends in an error: what matters is where.
fn cms_with_econtent(econtent: &[u8]) -> Vec<u8> {
let encap = der::seq(&[&der::oid("1.2.840.113549.1.7.1"), &der::explicit(0, econtent)]);
let signed_data = der::seq(&[&der::int(1), &der::tlv(tag::SET, &[]), &encap]);
der::seq(&[&der::oid("1.2.840.113549.1.7.2"), &der::explicit(0, &signed_data)])
}
fn error_of(cms: &[u8]) -> String {
SignedData::parse(cms).err().map(|e| e.to_string()).unwrap_or_default()
}
#[test]
fn deeply_nested_segmented_content_is_an_error_not_a_stack_overflow() {
// Definite lengths nest without any end-of-contents scan to bound them, so a /Contents of a few
// hundred KB can hold 100 000 levels. It used to be able to overflow the stack and abort.
let deep = cms_with_econtent(&nested_octet_strings(100_000));
assert!(deep.len() < 1 << 20, "a plausible /Contents");
let why = error_of(&deep);
assert!(why.contains("nested too deeply"), "{why}");
// A few segments, as a BER signer writes them, are read: the error is the missing SignerInfo.
let shallow = error_of(&cms_with_econtent(&nested_octet_strings(2)));
assert!(shallow.contains("signerInfos"), "{shallow}");
}
#[test]
fn indefinite_length_nesting_bombs_are_refused() {
// 10 000 indefinite-length SEQUENCEs: every level would have to be walked to find its end.
let bomb: Vec<u8> = std::iter::repeat_n([tag::SEQUENCE, 0x80], 10_000).flatten().collect();
assert!(SignedData::parse(&bomb).is_err());
let in_content = cms_with_econtent(&bomb);
assert!(SignedData::parse(&in_content).is_err());
}
#[test]
fn a_truncated_indefinite_length_cms_is_an_error() {
// The end-of-contents octets are missing, as in a /Contents cut short.
let cut = [0x30, 0x80, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02];
assert!(SignedData::parse(&cut).is_err());
}
+312
View File
@@ -0,0 +1,312 @@
//! Chain building: which certificates may vouch for which (RFC 5280 §6.1, the parts that matter
//! for trust). Certificates are issued in-process with chosen extensions, so every refusal is
//! tested on its own.
use craft_pkcs7::PrivateKey;
use craft_pkcs7::der::{self, Time, tag};
use craft_pkcs7::x509::{Certificate, Name, RefusedWhy, SigningProblem, build_chain, build_chain_noted};
fn time(year: u32) -> Time {
Time { year, month: 6, day: 1, hour: 12, minute: 0, second: 0 }
}
fn ext(oid: &str, value: &[u8]) -> Vec<u8> {
der::seq(&[&der::oid(oid), &der::boolean(true), &der::octets(value)])
}
/// basicConstraints: `cA` (omitted when false, as DER does) and an optional `pathLenConstraint`.
fn basic_constraints(ca: bool, path_len: Option<u64>) -> Vec<u8> {
let mut parts: Vec<Vec<u8>> = Vec::new();
if ca {
parts.push(der::boolean(true));
}
if let Some(n) = path_len {
parts.push(der::int(n));
}
let refs: Vec<&[u8]> = parts.iter().map(Vec::as_slice).collect();
ext("2.5.29.19", &der::seq(&refs))
}
const DIGITAL_SIGNATURE: u8 = 0x80;
const KEY_CERT_SIGN: u8 = 0x04;
fn key_usage(bits: u8) -> Vec<u8> {
ext("2.5.29.15", &der::tlv(tag::BIT_STRING, &[1, bits]))
}
struct Party {
name: Name,
key: PrivateKey,
cert: Certificate,
}
/// A certificate for a fresh key, signed by `issuer` (or by itself).
fn issue(cn: &str, issuer: Option<&Party>, valid: (u32, u32), extensions: &[Vec<u8>]) -> Party {
let key = PrivateKey::generate_p256().unwrap();
let name = Name::build(cn, "", "Chain Tests", "", "US");
let signer = issuer.map_or(&key, |i| &i.key);
let issuer_name = issuer.map_or(&name, |i| &i.name);
let alg = signer.preferred_digest();
let sig_alg = signer.signature_algorithm(alg);
let exts: Vec<&[u8]> = extensions.iter().map(Vec::as_slice).collect();
let mut fields: Vec<Vec<u8>> = Vec::new();
// v3 when there are extensions; with none, a v1 certificate (no version field), as old roots.
if !exts.is_empty() {
fields.push(der::explicit(0, &der::int(2)));
}
fields.extend([
der::uint(&[1 + cn.len() as u8]),
sig_alg.clone(),
issuer_name.raw.clone(),
der::seq(&[&time(valid.0).encode(), &time(valid.1).encode()]),
name.raw.clone(),
key.public_key().spki(),
]);
if !exts.is_empty() {
fields.push(der::explicit(3, &der::seq(&exts)));
}
let refs: Vec<&[u8]> = fields.iter().map(Vec::as_slice).collect();
let tbs = der::seq(&refs);
let signature = signer.sign(alg, &tbs).unwrap();
let cert = Certificate::parse(&der::seq(&[&tbs, &sig_alg, &der::bit_string(&signature)])).unwrap();
Party { name, key, cert }
}
const FOREVER: (u32, u32) = (2020, 2040);
fn root() -> Party {
issue("Test Root", None, FOREVER, &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN)])
}
fn ca(cn: &str, under: &Party, path_len: Option<u64>) -> Party {
issue(cn, Some(under), FOREVER, &[basic_constraints(true, path_len), key_usage(KEY_CERT_SIGN)])
}
fn end_entity(cn: &str, under: &Party) -> Party {
issue(cn, Some(under), FOREVER, &[basic_constraints(false, None), key_usage(DIGITAL_SIGNATURE)])
}
fn names(chain: &[&Certificate]) -> Vec<String> {
chain.iter().map(|c| c.subject.common_name().unwrap_or_default().to_string()).collect()
}
#[test]
fn a_proper_chain_is_built() {
let r = root();
let i = ca("Intermediate", &r, None);
let l = end_entity("Leaf", &i);
let pool = [i.cert.clone(), r.cert.clone()];
assert_eq!(names(&build_chain(&l.cert, &pool, None)), ["Leaf", "Intermediate", "Test Root"]);
assert_eq!(names(&build_chain(&l.cert, &pool, Some(time(2026)))), ["Leaf", "Intermediate", "Test Root"]);
}
#[test]
fn an_end_entity_certificate_cannot_issue() {
let r = root();
let e = end_entity("Ordinary Subscriber", &r);
// A certificate its holder made with the end-entity key.
let forged = end_entity("Bank AG", &e);
let pool = [e.cert.clone(), r.cert.clone()];
let (chain, why) = build_chain_noted(&forged.cert, &pool, None);
assert_eq!(names(&chain), ["Bank AG"], "the subscriber certificate does not vouch for what it signed");
let why = why.unwrap();
assert_eq!(why.why, RefusedWhy::NotCa, "the reason is reported");
assert_eq!((why.issuer, why.subject), (&e.cert, &forged.cert), "with the two certificates it is about");
// Neither does one without basicConstraints (not a v1 root: it has an issuer).
let plain = issue("No Constraints", Some(&r), FOREVER, &[]);
let under_plain = end_entity("Under Plain", &plain);
let pool = [plain.cert.clone(), r.cert.clone()];
assert_eq!(names(&build_chain(&under_plain.cert, &pool, None)), ["Under Plain"]);
// CA:TRUE does not help when keyUsage leaves out keyCertSign…
let signing_only = issue("Signing Only CA", Some(&r), FOREVER, &[basic_constraints(true, None), key_usage(DIGITAL_SIGNATURE)]);
let below = end_entity("Below Signing Only", &signing_only);
let pool = [signing_only.cert.clone(), r.cert.clone()];
assert_eq!(names(&build_chain(&below.cert, &pool, None)), ["Below Signing Only"]);
// …and a CA with no keyUsage at all is fine.
let any_usage = issue("Any Usage CA", Some(&r), FOREVER, &[basic_constraints(true, None)]);
let below = end_entity("Below Any Usage", &any_usage);
let pool = [any_usage.cert.clone(), r.cert.clone()];
assert_eq!(names(&build_chain(&below.cert, &pool, None)), ["Below Any Usage", "Any Usage CA", "Test Root"]);
}
#[test]
fn a_path_length_constraint_limits_the_cas_below() {
let r = root();
let i1 = ca("Constrained", &r, Some(0));
let i2 = ca("Sub CA", &i1, None);
let l = end_entity("Leaf", &i2);
let pool = [i1.cert.clone(), i2.cert.clone(), r.cert.clone()];
// pathLen 0: no CA may follow it, so Sub CA's issuer is refused and the chain stops there.
assert_eq!(names(&build_chain(&l.cert, &pool, None)), ["Leaf", "Sub CA"]);
assert_eq!(build_chain_noted(&l.cert, &pool, None).1.map(|w| w.why), Some(RefusedWhy::PathLength));
// pathLen 0 directly above the leaf is fine, and pathLen 1 allows one CA below.
let direct = end_entity("Direct Leaf", &i1);
assert_eq!(names(&build_chain(&direct.cert, &pool, None)), ["Direct Leaf", "Constrained", "Test Root"]);
let i1 = ca("Constrained 1", &r, Some(1));
let i2 = ca("Sub CA 1", &i1, None);
let l = end_entity("Leaf 1", &i2);
let pool = [i1.cert.clone(), i2.cert.clone(), r.cert.clone()];
assert_eq!(names(&build_chain(&l.cert, &pool, None)), ["Leaf 1", "Sub CA 1", "Constrained 1", "Test Root"]);
}
#[test]
fn issuers_must_have_been_valid_when_it_matters() {
let r = root();
let old = issue("Short Lived CA", Some(&r), (2020, 2022), &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN)]);
let l = end_entity("Leaf", &old);
let pool = [old.cert.clone(), r.cert.clone()];
assert_eq!(names(&build_chain(&l.cert, &pool, Some(time(2021)))), ["Leaf", "Short Lived CA", "Test Root"]);
let (chain, why) = build_chain_noted(&l.cert, &pool, Some(time(2023)));
assert_eq!(names(&chain), ["Leaf"], "expired by then");
assert_eq!(why.map(|w| w.why), Some(RefusedWhy::NotValid));
assert_eq!(names(&build_chain(&l.cert, &pool, Some(time(2019)))), ["Leaf"], "not yet valid");
// With no time to judge by, nothing is refused on validity.
assert_eq!(names(&build_chain(&l.cert, &pool, None)).len(), 3);
}
#[test]
fn an_old_v1_self_signed_root_without_constraints_still_anchors() {
// No extensions at all: basicConstraints absent, as in v1 roots.
let r = issue("Old V1 Root", None, FOREVER, &[]);
assert!(r.cert.is_self_signed() && !r.cert.has_basic_constraints && r.cert.may_issue());
let i = ca("Under V1", &r, None);
let l = end_entity("Leaf", &i);
let pool = [i.cert.clone(), r.cert.clone()];
assert_eq!(names(&build_chain(&l.cert, &pool, None)), ["Leaf", "Under V1", "Old V1 Root"]);
// A self-signed certificate that says CA:FALSE is not a root that can issue.
let not_a_ca = issue("Self Signed Leaf", None, FOREVER, &[basic_constraints(false, None)]);
assert!(!not_a_ca.cert.may_issue());
}
#[test]
fn a_v3_self_signed_certificate_without_constraints_is_not_a_root_that_issues() {
// A self-signed v3 ID with no basicConstraints and no key usage (only a key identifier): it
// may sign documents, but it may not vouch for anyone else.
let id = issue("Self Signed V3 ID", None, FOREVER, &[ext("2.5.29.14", &der::octets(&[1, 2, 3, 4]))]);
assert!(id.cert.is_self_signed() && !id.cert.has_basic_constraints && id.cert.version == 2);
assert!(!id.cert.may_issue());
let l = end_entity("Leaf", &id);
assert_eq!(names(&build_chain(&l.cert, std::slice::from_ref(&id.cert), None)), ["Leaf"]);
}
const KEY_ENCIPHERMENT: u8 = 0x20;
const SERVER_AUTH: &str = "1.3.6.1.5.5.7.3.1";
fn extended_key_usage(oids: &[&str]) -> Vec<u8> {
let encoded: Vec<Vec<u8>> = oids.iter().map(|o| der::oid(o)).collect();
let refs: Vec<&[u8]> = encoded.iter().map(Vec::as_slice).collect();
der::seq(&[&der::oid("2.5.29.37"), &der::octets(&der::seq(&refs))])
}
fn signer_with(cn: &str, under: &Party, extensions: &[Vec<u8>]) -> Party {
let mut all = vec![basic_constraints(false, None)];
all.extend_from_slice(extensions);
issue(cn, Some(under), FOREVER, &all)
}
#[test]
fn the_signer_certificate_must_allow_document_signing() {
let r = root();
let problem = |exts: &[Vec<u8>]| signer_with("Signer", &r, exts).cert.signing_problem();
// Key usage without digitalSignature or nonRepudiation.
assert_eq!(problem(&[key_usage(KEY_CERT_SIGN)]), Some(SigningProblem::KeyUsage));
assert!(problem(&[key_usage(KEY_ENCIPHERMENT)]).is_some());
assert!(problem(&[key_usage(DIGITAL_SIGNATURE)]).is_none());
// nonRepudiation alone is enough.
assert!(problem(&[key_usage(0x40)]).is_none());
// Extended key usage without a document-signing purpose.
assert_eq!(problem(&[extended_key_usage(&[SERVER_AUTH])]), Some(SigningProblem::ExtendedKeyUsage));
for ok in ["2.5.29.37.0", "1.3.6.1.5.5.7.3.4", "1.3.6.1.5.5.7.3.36", "1.2.840.113583.1.1.5", "1.3.6.1.4.1.311.10.3.12"] {
assert!(problem(&[extended_key_usage(&[SERVER_AUTH, ok])]).is_none(), "{ok}");
}
// An unknown critical extension refuses; the same extension non-critical does not.
let unknown = "1.2.3.4.5.6";
assert_eq!(problem(&[ext(unknown, &der::octets(&[0]))]), Some(SigningProblem::UnknownCritical(vec![unknown.to_string()])));
assert!(problem(&[der::seq(&[&der::oid(unknown), &der::octets(&der::octets(&[0]))])]).is_none());
// Critical extensions this crate knows, or may accept as they are, don't refuse.
assert!(problem(&[ext("2.5.29.32", &der::seq(&[]))]).is_none());
// No extensions at all, and the self-signed certificates this crate makes, may sign.
assert!(issue("Plain", Some(&r), FOREVER, &[]).cert.signing_problem().is_none());
let key = PrivateKey::generate_p256().unwrap();
let name = Name::build("Own ID", "", "", "", "US");
assert!(Certificate::self_signed(&name, &key, time(2026), 5, &[1]).unwrap().signing_problem().is_none());
}
#[test]
fn a_smart_cards_certificates_that_may_not_sign_say_why() {
// A DoD CAC's four certificates, all under the same name; only the signing one may sign.
let r = root();
let problem = |exts: &[Vec<u8>]| signer_with("DOE.JANE.1234567890", &r, exts).cert.signing_problem();
let encryption = problem(&[key_usage(KEY_ENCIPHERMENT), extended_key_usage(&["1.3.6.1.5.5.7.3.4"])]);
assert_eq!(encryption, Some(SigningProblem::KeyUsage));
let logon = problem(&[key_usage(DIGITAL_SIGNATURE), extended_key_usage(&["1.3.6.1.4.1.311.20.2.2", "1.3.6.1.5.5.7.3.2"])]);
assert_eq!(logon, Some(SigningProblem::ExtendedKeyUsage));
let card_auth = problem(&[key_usage(DIGITAL_SIGNATURE), extended_key_usage(&["2.16.840.1.101.3.6.8"])]);
assert_eq!(card_auth, logon);
let signing = problem(&[key_usage(DIGITAL_SIGNATURE | 0x40), extended_key_usage(&["1.3.6.1.5.5.7.3.4", "1.3.6.1.4.1.311.10.3.12"])]);
assert_eq!(signing, None);
}
#[test]
fn unreadable_constraint_extensions_fail_closed() {
// keyUsage present but not a BIT STRING: no usage at all, so no keyCertSign.
let bad_usage = ext("2.5.29.15", &[0xff, 0x00]);
let c = issue("Bad Usage CA", Some(&root()), FOREVER, &[basic_constraints(true, None), bad_usage]);
assert_eq!(c.cert.key_usage, Some(0));
assert!(!c.cert.may_issue());
// pathLenConstraint present but negative: no CA may follow, not any number.
let negative = ext("2.5.29.19", &der::seq(&[&der::boolean(true), &der::tlv(tag::INTEGER, &[0xff])]));
let c = issue("Negative PathLen CA", Some(&root()), FOREVER, &[negative]);
assert!(c.cert.is_ca);
assert_eq!(c.cert.path_len, Some(0));
// basicConstraints present but unparsable: present, not a CA, so not an "old v1 root".
let garbage = ext("2.5.29.19", &[0x30, 0x05, 0x01]);
let c = issue("Garbled Constraints", None, FOREVER, &[garbage]);
assert!(c.cert.has_basic_constraints && !c.cert.is_ca && !c.cert.may_issue());
}
#[test]
fn an_unreadable_extended_key_usage_allows_no_purpose() {
// An extendedKeyUsage that is present restricts the key even when it doesn't parse:
// truncated, critical or not; a SET instead of a SEQUENCE; or a member that isn't an OID.
// Each would otherwise read as "no restriction", or as only its readable members.
let r = root();
let any = der::oid("2.5.29.37.0");
let truncated = [0x30, 0x05, 0x06];
let malformed = [
ext("2.5.29.37", &truncated),
der::seq(&[&der::oid("2.5.29.37"), &der::octets(&truncated)]),
ext("2.5.29.37", &der::tlv(tag::SET, &any)),
ext("2.5.29.37", &der::seq(&[&der::int(1), &any])),
];
for (i, eku) in malformed.into_iter().enumerate() {
let signer = signer_with("Malformed EKU", &r, &[key_usage(DIGITAL_SIGNATURE), eku]);
assert_eq!(signer.cert.extended_key_usage.as_deref(), Some(&[][..]), "case {i}");
assert_eq!(signer.cert.signing_problem(), Some(SigningProblem::ExtendedKeyUsage), "case {i}");
}
// The same document-signing purpose, well formed, may sign.
let good = signer_with("Good EKU", &r, &[key_usage(DIGITAL_SIGNATURE), extended_key_usage(&["2.5.29.37.0"])]);
assert_eq!(good.cert.signing_problem(), None);
}
/// A CA certificate with a critical extension this crate does not enforce (here name constraints)
/// vouches for nothing: what that extension limits would go unchecked. A self-signed root's
/// extensions are the trust store's business, so such a root still anchors.
#[test]
fn an_issuer_with_an_unprocessed_critical_extension_does_not_vouch() {
let name_constraints = ext("2.5.29.30", &der::seq(&[]));
let r = root();
let constrained =
issue("Name Constrained CA", Some(&r), FOREVER, &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN), name_constraints.clone()]);
let l = end_entity("Outside Its Names", &constrained);
let pool = [constrained.cert.clone(), r.cert.clone()];
let (chain, why) = build_chain_noted(&l.cert, &pool, None);
assert_eq!(names(&chain), ["Outside Its Names"]);
assert_eq!(why.map(|w| w.why), Some(RefusedWhy::UnknownCritical), "the reason is reported");
let constrained_root =
issue("Name Constrained Root", None, FOREVER, &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN), name_constraints]);
let i = ca("Under Constrained Root", &constrained_root, None);
let l = end_entity("Leaf", &i);
let pool = [i.cert.clone(), constrained_root.cert.clone()];
assert_eq!(names(&build_chain(&l.cert, &pool, None)), ["Leaf", "Under Constrained Root", "Name Constrained Root"]);
}
+130
View File
@@ -0,0 +1,130 @@
//! What a CMS's signed attributes may hold. RFC 5652 allows one content-type and one
//! message-digest attribute, each with one value, and the content type must be the type of the
//! encapsulated content. Both content types are object identifiers.
//!
//! These go through `SignedData::parse`, the way a PDF's signature reaches it. Each CMS is built
//! here as the signer builds it, so a case differs from a valid one only where it says.
use craft_pkcs7::cms::SignedData;
use craft_pkcs7::der::{self, tag};
use craft_pkcs7::keys::DigestAlg;
use craft_pkcs7::{Error, pkcs12};
const SIGNED_DATA: &str = "1.2.840.113549.1.7.2";
const DATA: &str = "1.2.840.113549.1.7.1";
const TST_INFO: &str = "1.2.840.113549.1.9.16.1.4";
const CONTENT_TYPE: &str = "1.2.840.113549.1.9.3";
const MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4";
fn identity() -> pkcs12::DigitalId {
let p12 = std::fs::read(format!("{}/tests/data/rsa-aes.p12", env!("CARGO_MANIFEST_DIR"))).unwrap();
pkcs12::open(&p12, "test").unwrap()
}
/// A signed attribute: its type and the SET of its values.
fn attribute(oid: &str, values: &[&[u8]]) -> Vec<u8> {
der::seq(&[&der::oid(oid), &der::set_of(values)])
}
/// A SignedData whose encapsulated content has the type `encap_type` and whose one signer
/// signed the attributes `attrs`.
fn signed_data(id: &pkcs12::DigitalId, encap_type: &str, attrs: &[Vec<u8>]) -> Vec<u8> {
signed_data_with_encap(id, &der::seq(&[&der::oid(encap_type)]), attrs)
}
/// A SignedData with the encapContentInfo `encap`, as it stands, and whose one signer signed the
/// attributes `attrs`.
fn signed_data_with_encap(id: &pkcs12::DigitalId, encap: &[u8], attrs: &[Vec<u8>]) -> Vec<u8> {
let alg = DigestAlg::Sha256;
let refs: Vec<&[u8]> = attrs.iter().map(Vec::as_slice).collect();
let set = der::set_of(&refs);
let signature = id.key.sign(alg, &set).unwrap();
let mut signed_attrs = set;
signed_attrs[0] = tag::ctx(0);
let cert = &id.certificate;
let sid = der::seq(&[&cert.issuer.raw, &der::uint(&cert.serial)]);
let signer = der::seq(&[&der::int(1), &sid, &alg.algorithm(), &signed_attrs, &id.key.signature_algorithm(alg), &der::octets(&signature)]);
let signed_data =
der::seq(&[&der::int(1), &der::set_of(&[&alg.algorithm()]), encap, &der::tlv(tag::ctx(0), &cert.raw), &der::set_of(&[&signer])]);
der::seq(&[&der::oid(SIGNED_DATA), &der::explicit(0, &signed_data)])
}
/// Why `SignedData::parse` refuses `cms`.
fn refused(cms: &[u8]) -> String {
match SignedData::parse(cms) {
Err(Error::Malformed(why)) => why,
other => panic!("expected the CMS to be refused, got {other:?}"),
}
}
#[test]
fn signed_attributes_hold_one_content_type_and_one_message_digest() {
let id = identity();
let digest = DigestAlg::Sha256.digest(&[b"document"]);
let (data, tst, md, other_md) = (der::oid(DATA), der::oid(TST_INFO), der::octets(&digest), der::octets(&[0; 32]));
let content_type = attribute(CONTENT_TYPE, &[&data]);
let message_digest = attribute(MESSAGE_DIGEST, &[&md]);
// As a signer writes them: read, and the signature over them verifies.
let sd = SignedData::parse(&signed_data(&id, DATA, &[content_type.clone(), message_digest.clone()])).unwrap();
assert_eq!(sd.signer.content_type.as_deref(), Some(DATA));
assert_eq!(sd.signer.message_digest.as_deref(), Some(digest.as_slice()));
assert!(sd.verify_signature(&id.certificate, &digest).unwrap());
// A second attribute of either kind, with the same value or another one, and an attribute
// with two values or none.
for (attrs, what) in [
(vec![content_type.clone(), content_type.clone(), message_digest.clone()], "content type"),
(vec![content_type.clone(), attribute(CONTENT_TYPE, &[&tst]), message_digest.clone()], "content type"),
(vec![attribute(CONTENT_TYPE, &[&data, &tst]), message_digest.clone()], "content type"),
(vec![attribute(CONTENT_TYPE, &[]), message_digest.clone()], "content type"),
(vec![content_type.clone(), message_digest.clone(), message_digest.clone()], "message digest"),
(vec![content_type.clone(), message_digest.clone(), attribute(MESSAGE_DIGEST, &[&other_md])], "message digest"),
(vec![content_type.clone(), attribute(MESSAGE_DIGEST, &[&md, &other_md])], "message digest"),
(vec![content_type.clone(), attribute(MESSAGE_DIGEST, &[])], "message digest"),
] {
let why = refused(&signed_data(&id, DATA, &attrs));
assert!(why.contains(&format!("one {what} with one value")), "{why}");
}
}
#[test]
fn the_signed_content_type_is_the_type_of_the_encapsulated_content() {
let id = identity();
let digest = DigestAlg::Sha256.digest(&[b"document"]);
let message_digest = attribute(MESSAGE_DIGEST, &[&der::octets(&digest)]);
let signed_as = |oid: &str| attribute(CONTENT_TYPE, &[&der::oid(oid)]);
// A PDF signature names data in both places, and an RFC 3161 token names TSTInfo in both.
SignedData::parse(&signed_data(&id, DATA, &[signed_as(DATA), message_digest.clone()])).unwrap();
SignedData::parse(&signed_data(&id, TST_INFO, &[signed_as(TST_INFO), message_digest.clone()])).unwrap();
// The signature does not cover eContentType: here it was changed from data to signedData
// (the last byte of the OID), and the signed attributes still say data.
let why = refused(&signed_data(&id, SIGNED_DATA, &[signed_as(DATA), message_digest.clone()]));
assert!(why.contains("not the encapsulated content type"), "{why}");
// Without a content-type attribute there is nothing to compare here. The type is kept, and a
// PDF signature's validation refuses one that is not data.
let sd = SignedData::parse(&signed_data(&id, SIGNED_DATA, std::slice::from_ref(&message_digest))).unwrap();
assert_eq!(sd.econtent_type, SIGNED_DATA);
}
#[test]
fn content_types_are_object_identifiers() {
let id = identity();
let digest = DigestAlg::Sha256.digest(&[b"document"]);
let message_digest = attribute(MESSAGE_DIGEST, &[&der::octets(&digest)]);
let content_type = attribute(CONTENT_TYPE, &[&der::oid(DATA)]);
let not_an_oid = der::octets(DATA.as_bytes());
// A content-type value that is not an OID would leave nothing to compare.
let why = refused(&signed_data(&id, DATA, &[attribute(CONTENT_TYPE, &[&not_an_oid]), message_digest.clone()]));
assert!(why.contains("expected an OID"), "{why}");
// eContentType is required (RFC 5652 §5.2) and is an OID.
let why = refused(&signed_data_with_encap(&id, &der::seq(&[&not_an_oid]), &[content_type.clone(), message_digest.clone()]));
assert!(why.contains("expected an OID"), "{why}");
let why = refused(&signed_data_with_encap(&id, &der::seq(&[]), &[content_type, message_digest]));
assert!(why.contains("eContentType"), "{why}");
}
+812
View File
@@ -0,0 +1,812 @@
//! The cryptographic core against files made by OpenSSL 3 (tests/data/README.md).
use craft_pkcs7::der::{Time, Tlv};
use craft_pkcs7::keys::DigestAlg;
use craft_pkcs7::{Certificate, Error, Name, PublicKey, cms, pkcs12};
fn data(name: &str) -> Vec<u8> {
std::fs::read(format!("{}/tests/data/{name}", env!("CARGO_MANIFEST_DIR"))).unwrap()
}
fn pem(name: &str) -> Vec<u8> {
let text = String::from_utf8(data(name)).unwrap();
let b64: String = text.lines().filter(|l| !l.starts_with("-----")).collect();
decode_base64(&b64)
}
fn decode_base64(s: &str) -> Vec<u8> {
let val = |c: u8| match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
_ => 63,
};
let bytes: Vec<u8> = s.bytes().filter(|c| *c != b'=').map(val).collect();
bytes
.chunks(4)
.flat_map(|c| {
let n = c.iter().enumerate().fold(0u32, |acc, (i, v)| acc | (*v as u32) << (18 - 6 * i));
let k = c.len() * 6 / 8;
(0..k).map(move |i| (n >> (16 - 8 * i)) as u8)
})
.collect()
}
#[test]
fn opens_every_openssl_flavour_of_pkcs12() {
for (file, cn, key) in [
("rsa-aes.p12", "Test Signer RSA", "RSA 2048-bit"),
("rsa-legacy.p12", "Test Signer RSA", "RSA 2048-bit"),
("ec-p256.p12", "Test Signer EC", "ECDSA P-256"),
("ec-p384.p12", "Test Signer P384", "ECDSA P-384"),
("chain.p12", "Ada Lovelace", "RSA 2048-bit"),
] {
let id = pkcs12::open(&data(file), "test").unwrap_or_else(|e| panic!("{file}: {e}"));
assert_eq!(id.certificate.subject.common_name(), Some(cn), "{file}");
assert_eq!(id.key.public_key().describe(), key, "{file}");
assert_eq!(&id.certificate.public_key, id.key.public_key());
assert!(matches!(pkcs12::open(&data(file), "nope"), Err(Error::WrongPassword)), "{file}");
}
assert_eq!(pkcs12::open(&data("rsa-aes.p12"), "test").unwrap().friendly_name.as_deref(), Some("Test Signer RSA"));
let chain = pkcs12::open(&data("chain.p12"), "test").unwrap();
assert_eq!(chain.chain.len(), 1);
assert_eq!(chain.certificate.subject.email(), Some("ada@example.com"));
assert!(chain.chain[0].is_ca && chain.chain[0].is_self_signed());
assert!(chain.certificate.signed_by(&chain.chain[0].public_key), "the leaf is issued by the root");
assert!(!chain.certificate.is_self_signed());
assert_eq!(chain.certificate.key_usage.map(|u| u & 0b11), Some(0b11), "digitalSignature + nonRepudiation");
}
/// One element with BER's indefinite length: contents, then the end-of-contents octets.
fn indefinite(tag: u8, body: &[u8]) -> Vec<u8> {
let mut v = vec![tag, 0x80];
v.extend_from_slice(body);
v.extend([0, 0]);
v
}
/// An OCTET STRING split into segments, BER's constructed form.
fn segmented(bytes: &[u8]) -> Vec<u8> {
let parts: Vec<u8> = bytes.chunks(64).flat_map(der::octets).collect();
indefinite(0x24, &parts)
}
/// A `.p12` in the shape Windows writes one: the PFX, its ContentInfo and the `[0]` holding the
/// AuthenticatedSafe all in the indefinite form, and the AuthenticatedSafe split into segments.
/// Nothing changes meaning, and the MAC still covers the same octets, so whatever a reader makes
/// of it, it is making it of the form and not of the contents.
///
/// With `deep`, every ContentInfo inside is rewritten the same way, as a real Windows file has it.
/// That re-encodes the AuthenticatedSafe, which is what the MAC covers, so the MacData is dropped
/// — RFC 7292 §4 makes it optional — and a wrong password is then caught by the decryption.
fn as_windows_writes_it(der: &[u8], deep: bool) -> Vec<u8> {
let pfx = Tlv::parse_all(der).unwrap().children().unwrap();
let auth = pfx[1].children().unwrap();
let mut content = auth[1].inner().unwrap().value.to_vec();
if deep {
let mut inner = Vec::new();
for ci in Tlv::parse_all(&content).unwrap().children().unwrap() {
let p = ci.children().unwrap();
let body = p[1].inner().unwrap();
// A Data content is an OCTET STRING, so it gets segmented; an EncryptedData is a
// SEQUENCE and stays as it is inside its indefinite wrappers.
let wrapped = if body.tag == 0x04 { segmented(body.value) } else { body.raw.to_vec() };
let zero = indefinite(0xA0, &wrapped);
inner.extend(indefinite(0x30, &[p[0].raw, zero.as_slice()].concat()));
}
content = indefinite(0x30, &inner);
}
let zero = indefinite(0xA0, &segmented(&content));
let mut body = pfx[0].raw.to_vec();
body.extend(indefinite(0x30, &[auth[0].raw, zero.as_slice()].concat()));
if !deep {
for extra in &pfx[2..] {
body.extend_from_slice(extra.raw);
}
}
indefinite(0x30, &body)
}
#[test]
fn opens_pkcs12_as_windows_writes_it() {
for file in ["rsa-aes.p12", "rsa-legacy.p12", "chain.p12"] {
let der = data(file);
let want = pkcs12::open(&der, "test").unwrap();
for deep in [false, true] {
let ber = as_windows_writes_it(&der, deep);
assert_ne!(ber, der, "{file} ({deep})");
let got = pkcs12::open(&ber, "test").unwrap_or_else(|e| panic!("{file} in BER (deep: {deep}): {e}"));
assert_eq!(got.certificate.raw, want.certificate.raw, "{file} ({deep})");
assert_eq!(got.key.public_key(), want.key.public_key(), "{file} ({deep})");
assert_eq!(got.chain.len(), want.chain.len(), "{file} ({deep})");
assert_eq!(got.friendly_name, want.friendly_name, "{file} ({deep})");
assert!(matches!(pkcs12::open(&ber, "nope"), Err(Error::WrongPassword)), "{file} ({deep})");
}
}
}
#[test]
fn certificates_parse_as_openssl_made_them() {
let c = Certificate::parse(&pem("rsa.crt.pem")).unwrap();
// (The organisation name is what the fixtures were made with.)
assert_eq!(c.subject.display(), "CN=Test Signer RSA, O=PrintCraft Tests, C=US");
assert_eq!(c.serial_hex(), "03E9");
assert!(c.is_self_signed());
assert!(c.not_after.year >= c.not_before.year + 9);
let ca = Certificate::parse(&pem("ca.crt.pem")).unwrap();
assert!(ca.is_ca && ca.is_self_signed());
assert!(matches!(ca.public_key, PublicKey::P256(_)));
}
#[test]
fn signatures_round_trip_for_every_key_type() {
for file in ["rsa-aes.p12", "ec-p256.p12", "ec-p384.p12", "chain.p12"] {
let id = pkcs12::open(&data(file), "test").unwrap();
let alg = id.key.preferred_digest();
let digest = alg.digest(&[b"the document bytes"]);
let sig = cms::sign_detached(&id.key, &id.certificate, &id.chain, alg, &digest).unwrap();
let mut padded = sig.clone();
padded.extend([0u8; 64]);
let sd = cms::SignedData::parse(&padded).unwrap();
let signer = sd.signer_certificate().expect("the signer's certificate is embedded");
assert_eq!(signer, &id.certificate);
assert_eq!(sd.signer.message_digest.as_deref(), Some(&digest[..]));
assert!(sd.signer.signing_certificate, "CAdES signing-certificate-v2");
assert!(sd.verify_signature(signer, &digest).unwrap(), "{file}");
assert_eq!(sd.certificates.len(), 1 + id.chain.len());
// Tampering with the signature breaks it.
let mut bad = sd.clone();
bad.signer.signature[5] ^= 1;
assert!(!bad.verify_signature(signer, &digest).unwrap(), "{file}");
}
}
#[test]
fn new_digital_ids_are_self_signed_and_survive_a_p12_round_trip() {
let name = Name::build("Grace Hopper", "Compilers", "Navy", "grace@example.com", "us");
let now = Time { year: 2026, month: 10, day: 2, hour: 12, minute: 0, second: 0 };
for key in [craft_pkcs7::PrivateKey::generate_rsa(2048).unwrap(), craft_pkcs7::PrivateKey::generate_p256().unwrap()] {
let cert = Certificate::self_signed(&name, &key, now, 5, &[0x42, 0x01]).unwrap();
assert!(cert.is_self_signed());
assert_eq!(cert.subject.display(), "C=US, O=Navy, OU=Compilers, CN=Grace Hopper, E=grace@example.com");
assert_eq!(cert.not_after.year, 2031);
assert_eq!(cert.key_usage.map(|u| u & 0b11), Some(0b11));
let id = pkcs12::DigitalId { key, certificate: cert, chain: Vec::new(), friendly_name: Some("Grace Hopper".into()) };
let p12 = pkcs12::write(&id, "s3cret").unwrap();
let back = pkcs12::open(&p12, "s3cret").unwrap();
assert_eq!(back.certificate, id.certificate);
assert_eq!(back.friendly_name.as_deref(), Some("Grace Hopper"));
assert!(matches!(pkcs12::open(&p12, "wrong"), Err(Error::WrongPassword)));
let d = DigestAlg::Sha256.digest(&[b"x"]);
let sig = cms::sign_detached(&back.key, &back.certificate, &[], DigestAlg::Sha256, &d).unwrap();
let sd = cms::SignedData::parse(&sig).unwrap();
assert!(sd.verify_signature(&back.certificate, &d).unwrap());
}
}
#[test]
fn new_digital_ids_use_the_stronger_round_count_for_the_key_and_the_mac() {
let name = Name::build("Grace Hopper", "", "", "", "");
let now = Time { year: 2026, month: 10, day: 14, hour: 12, minute: 0, second: 0 };
let key = craft_pkcs7::PrivateKey::generate_p256().unwrap();
let certificate = Certificate::self_signed(&name, &key, now, 5, &[0x42]).unwrap();
let id = pkcs12::DigitalId { key, certificate, chain: Vec::new(), friendly_name: None };
let p12 = pkcs12::write(&id, "s3cret12").unwrap();
// OWASP's figure for PBKDF2-HMAC-SHA-256, spelled out so lowering the constant fails here.
const ROUNDS: u64 = 600_000;
// MacData's iteration count, the last field of the file.
let pfx = Tlv::parse_all(&p12).unwrap().children().unwrap();
let mac = pfx[2].children().unwrap();
assert_eq!(mac[2].u64().unwrap(), ROUNDS);
// The same INTEGER three times: the MAC and the PBKDF2 parameters of both encrypted parts.
let rounds = craft_pkcs7::der::int(ROUNDS);
assert_eq!(p12.windows(rounds.len()).filter(|w| *w == rounds.as_slice()).count(), 3);
assert_eq!(pkcs12::open(&p12, "s3cret12").unwrap().certificate, id.certificate);
}
/// A `.p12` whose MAC asks for `rounds` iterations. Its content is empty: opening it must stop
/// at the MAC, before the content is read.
fn p12_with_mac_rounds(rounds: u64) -> Vec<u8> {
use craft_pkcs7::der;
let auth_safe = der::seq(&[]);
let digest = der::seq(&[&DigestAlg::Sha256.algorithm(), &der::octets(&[0; 32])]);
let mac_data = der::seq(&[&digest, &der::octets(&[1; 16]), &der::int(rounds)]);
der::seq(&[&der::int(3), &der::seq(&[&der::oid("1.2.840.113549.1.7.1"), &der::explicit(0, &der::octets(&auth_safe))]), &mac_data])
}
#[test]
fn a_p12_asking_for_too_many_rounds_is_refused_before_hashing() {
// 2^32 + 1 used to wrap to one round; 20 million took seconds and 4 billion about an hour,
// all spent on the password check.
for rounds in [(1u64 << 32) + 1, 20_000_000, 4_000_000_000] {
let err = pkcs12::open(&p12_with_mac_rounds(rounds), "password").expect_err("refused");
assert!(matches!(err, Error::Unsupported(_)), "{rounds}: {err}");
}
// An ordinary count still gets as far as the password.
assert!(matches!(pkcs12::open(&p12_with_mac_rounds(2048), "password"), Err(Error::WrongPassword)));
}
#[test]
fn certificates_load_from_pem_and_der_and_export_as_pem() {
let pem_text = data("rsa.crt.pem");
let certs = craft_pkcs7::x509::load_certificates(&pem_text).unwrap();
assert_eq!(certs.len(), 1);
let der = certs[0].raw.clone();
assert_eq!(craft_pkcs7::x509::load_certificates(&der).unwrap()[0], certs[0]);
let back = craft_pkcs7::x509::to_pem(&certs[0]);
assert_eq!(craft_pkcs7::x509::load_certificates(back.as_bytes()).unwrap()[0], certs[0]);
assert!(craft_pkcs7::x509::load_certificates(b"hello").is_err());
}
/// Base64 in 64-column lines, for PEM armour.
fn encode_base64(der: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut s = String::new();
for c in der.chunks(3) {
let n = u32::from(c[0]) << 16 | u32::from(*c.get(1).unwrap_or(&0)) << 8 | u32::from(*c.get(2).unwrap_or(&0));
for i in 0..4 {
s.push(if i <= c.len() { T[(n >> (18 - 6 * i) & 63) as usize] as char } else { '=' });
}
}
let lines: Vec<&str> = s.as_bytes().chunks(64).map(|l| std::str::from_utf8(l).unwrap()).collect();
lines.join("\n")
}
/// A PKCS #7 certificate bundle (degenerate SignedData, RFC 5652 §5.2) holding `certs`, laid out
/// as `openssl crl2pkcs7 -nocrl` writes it.
fn p7b(certs: &[&[u8]]) -> Vec<u8> {
use craft_pkcs7::der::{explicit, int, oid, seq, set_of, tlv};
let encap = seq(&[&oid(cms::DATA)]);
let mut sd = vec![int(1), set_of(&[]), encap];
if !certs.is_empty() {
sd.push(tlv(0xA0, &certs.concat()));
}
sd.push(set_of(&[]));
let refs: Vec<&[u8]> = sd.iter().map(Vec::as_slice).collect();
seq(&[&oid(cms::SIGNED_DATA), &explicit(0, &seq(&refs))])
}
#[test]
fn certificates_load_from_pkcs7_bundles() {
use craft_pkcs7::x509::{load_certificates, to_pem};
let leaf = load_certificates(&data("rsa.crt.pem")).unwrap().remove(0);
let root = load_certificates(&data("ca.crt.pem")).unwrap().remove(0);
let bundle = p7b(&[&leaf.raw, &root.raw]);
// .p7b / .p7c in DER, the way Windows and Acrobat export a chain.
assert_eq!(load_certificates(&bundle).unwrap(), vec![leaf.clone(), root.clone()]);
// The same bundle as PEM `PKCS7` and `CMS` blocks, after a key and a plain certificate.
let b64 = encode_base64(&bundle);
for label in ["PKCS7", "CMS"] {
let text = format!(
"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n{}-----BEGIN {label}-----\n{}\n-----END {label}-----\n",
to_pem(&leaf),
b64
);
assert_eq!(load_certificates(text.as_bytes()).unwrap(), vec![leaf.clone(), leaf.clone(), root.clone()], "{label}");
}
// An END that closes a different block ends nothing.
let mismatched = format!("-----BEGIN PKCS7-----\n{b64}\n-----END CERTIFICATE-----\n");
assert!(load_certificates(mismatched.as_bytes()).is_err());
// A block that never ends is a damaged file, not a shorter list.
let unclosed = format!("{}-----BEGIN PKCS7-----\n{b64}\n", to_pem(&leaf));
assert!(load_certificates(unclosed.as_bytes()).is_err());
// A bundle with no certificate, and a SignedData that isn't one, are errors, not empty lists.
assert!(load_certificates(&p7b(&[])).is_err());
let not_signed_data = craft_pkcs7::der::seq(&[&craft_pkcs7::der::oid(cms::DATA), &craft_pkcs7::der::explicit(0, &[])]);
assert!(load_certificates(&not_signed_data).is_err());
// A SignedData out of shape: no signerInfos SET, or a version that isn't an INTEGER.
{
use craft_pkcs7::der::{explicit, int, oid, seq, set_of, tlv};
let wrap = |sd: &[&[u8]]| seq(&[&oid(cms::SIGNED_DATA), &explicit(0, &seq(sd))]);
let (certs, encap) = (tlv(0xA0, &[leaf.raw.as_slice(), &root.raw].concat()), seq(&[&oid(cms::DATA)]));
assert_eq!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &encap, &certs, &set_of(&[])])).unwrap().len(), 2);
assert!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &encap, &certs])).is_err());
assert!(load_certificates(&wrap(&[&set_of(&[]), &set_of(&[]), &encap, &certs, &set_of(&[])])).is_err());
// An empty encapContentInfo, a repeated [0], and [1] CRLs before [0] certificates.
let crls = tlv(0xA1, &[]);
assert!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &seq(&[]), &certs, &set_of(&[])])).is_err());
assert!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &encap, &certs, &certs, &set_of(&[])])).is_err());
assert!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &encap, &crls, &certs, &set_of(&[])])).is_err());
assert_eq!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &encap, &certs, &crls, &set_of(&[])])).unwrap().len(), 2);
}
let mut trailing = bundle.clone();
trailing.extend_from_slice(&[0x05, 0x00]);
assert!(load_certificates(&trailing).is_err());
assert!(cms::certificate_bundle(&bundle).is_ok());
// Truncated anywhere: an error, never a panic.
for n in 0..bundle.len() {
let _ = load_certificates(&bundle[..n]);
}
}
// ── CRL and OCSP verification (RFC 5280, RFC 6960) ─────────────────────────────────────────
use craft_pkcs7::der::{self, tag};
use craft_pkcs7::revocation::{CertificateList, OcspResponse, RevocationStatus};
/// A self-issued CRL from `issuer`: optional revocation entry, signed with the issuer's key.
fn crl(issuer: &pkcs12::DigitalId, revoked_serial: Option<(&[u8], Time)>, this: Time, next: Time) -> Vec<u8> {
let alg = issuer.key.signature_algorithm(DigestAlg::Sha256);
let mut tbs = vec![der::int(1), alg.clone(), issuer.certificate.subject.raw.clone(), this.encode(), next.encode()];
if let Some((serial, at)) = revoked_serial {
tbs.push(der::seq(&[&der::seq(&[&der::uint(serial), &at.encode()])]));
}
let refs: Vec<&[u8]> = tbs.iter().map(Vec::as_slice).collect();
let tbs = der::seq(&refs);
let sig = issuer.key.sign(DigestAlg::Sha256, &tbs).unwrap();
der::seq(&[&tbs, &alg, &der::bit_string(&sig)])
}
/// A self-issued OCSP response from `issuer` answering for its own serial.
fn ocsp(issuer: &pkcs12::DigitalId, status: RevocationStatus, this: Time, next: Time) -> Vec<u8> {
let cert_id = der::seq(&[
&DigestAlg::Sha1.algorithm(),
&der::octets(&DigestAlg::Sha1.digest(&[&issuer.certificate.issuer.raw])),
&der::octets(&DigestAlg::Sha1.digest(&[&issuer.certificate.public_key.key_bits()])),
&der::uint(&issuer.certificate.serial),
]);
// RFC 6960's real encodings: good [0] IMPLICIT NULL, revoked [1] IMPLICIT RevokedInfo,
// unknown [2] IMPLICIT; nextUpdate [0] EXPLICIT GeneralizedTime.
let status = match status {
RevocationStatus::Good => der::tlv(tag::ctx_prim(0), &[]),
RevocationStatus::Revoked { at } => der::tlv(tag::ctx(1), &at.encode()),
RevocationStatus::Unknown => der::tlv(tag::ctx_prim(2), &[]),
};
let single = der::seq(&[&cert_id, &status, &this.encode(), &der::tlv(tag::ctx(0), &next.encode())]);
let responder_id = der::tlv(tag::ctx(1), &issuer.certificate.subject.raw);
signed_ocsp(&issuer.key, &responder_id, None, &single, this)
}
/// A successful OCSP response holding `single`, signed with `key` and naming its responder by
/// `responder_id`, with `certs` (the encoded `[0]` field) if given.
fn signed_ocsp(key: &craft_pkcs7::PrivateKey, responder_id: &[u8], certs: Option<&[u8]>, single: &[u8], at: Time) -> Vec<u8> {
let tbs = der::seq(&[responder_id, &at.encode(), &der::seq(&[single])]);
let alg = key.signature_algorithm(DigestAlg::Sha256);
let sig = key.sign(DigestAlg::Sha256, &tbs).unwrap();
let basic = match certs {
Some(c) => der::seq(&[&tbs, &alg, &der::bit_string(&sig), c]),
None => der::seq(&[&tbs, &alg, &der::bit_string(&sig)]),
};
let basic_type = der::oid("1.3.6.1.5.5.7.48.1.1");
der::seq(&[&der::tlv(tag::ENUMERATED, &[0]), &der::explicit(0, &der::seq(&[&basic_type, &der::octets(&basic)]))])
}
fn mid_june() -> Time {
Time { year: 2026, month: 6, day: 15, hour: 12, minute: 0, second: 0 }
}
#[test]
fn crls_verify_and_report_revocation() {
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
let (this, next) = (mid_june(), Time { year: 2026, month: 7, day: 15, hour: 12, minute: 0, second: 0 });
let issuer = &id.certificate;
let at = mid_june();
// Not on the list: good.
let clean = CertificateList::parse(&crl(&id, None, this, next)).unwrap();
assert_eq!(clean.check(issuer, issuer, at), RevocationStatus::Good);
// On the list: revoked with its date.
let revoked =
CertificateList::parse(&crl(&id, Some((&issuer.serial, Time { year: 2026, month: 6, day: 1, hour: 8, minute: 0, second: 0 })), this, next))
.unwrap();
assert_eq!(
revoked.check(issuer, issuer, at),
RevocationStatus::Revoked { at: Time { year: 2026, month: 6, day: 1, hour: 8, minute: 0, second: 0 } }
);
// Outside the validity window: unknown.
let later = Time { year: 2026, month: 8, day: 1, hour: 0, minute: 0, second: 0 };
assert_eq!(clean.check(issuer, issuer, later), RevocationStatus::Unknown);
// A tampered CRL does not verify: unknown, never a false "good".
let mut tampered = crl(&id, None, this, next);
let t = tampered.windows(4).position(|w| w == &[0x6F, 0x6B, 0x00, 0x00][..]).unwrap_or(20);
tampered[t] ^= 0xFF;
assert_ne!(CertificateList::parse(&tampered).unwrap().check(issuer, issuer, at), RevocationStatus::Good);
// A different issuer's key does not verify it.
let other = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
assert_eq!(
CertificateList::parse(&crl(&id, None, this, next)).unwrap().check(&other.certificate, &other.certificate, at),
RevocationStatus::Unknown
);
}
/// `.p12` files that aren't clean DER still open — trailing
/// whitespace, PEM armour or a bare base64 body — and real damage still fails,
/// with the reason in the message.
#[test]
fn opens_wrapped_pkcs12_files_and_still_rejects_broken_ones() {
use base64::Engine as _;
let der = data("rsa-aes.p12");
let signer = pkcs12::open(&der, "test").unwrap();
// An editor or a download appends whitespace.
let mut trailing = der.clone();
trailing.extend_from_slice(b"\r\n \n");
assert_eq!(pkcs12::open(&trailing, "test").unwrap().certificate, signer.certificate);
// PEM armour, the way OpenSSL and government portals present them.
let b64 = base64::engine::general_purpose::STANDARD.encode(&der);
let mut pem = String::from("-----BEGIN PKCS12-----\n");
for line in b64.as_bytes().chunks(64) {
pem.push_str(std::str::from_utf8(line).unwrap());
pem.push('\n');
}
pem.push_str("-----END PKCS12-----\n");
assert_eq!(pkcs12::open(pem.as_bytes(), "test").unwrap().certificate, signer.certificate);
// The base64 body alone, armour stripped.
assert_eq!(pkcs12::open(b64.as_bytes(), "test").unwrap().certificate, signer.certificate);
// A truncated file still fails, and says why.
let err = pkcs12::open(&der[..64], "test").expect_err("truncated file");
assert!(err.to_string().contains("runs past the end"), "{err}");
let err = pkcs12::open(b"", "test").expect_err("empty file");
assert!(err.to_string().contains("truncated DER"), "{err}");
}
#[test]
fn ocsp_responses_verify_and_match_the_certificate() {
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
let (this, next) = (mid_june(), Time { year: 2026, month: 7, day: 15, hour: 12, minute: 0, second: 0 });
let issuer = &id.certificate;
let at = mid_june();
let good = OcspResponse::parse(&ocsp(&id, RevocationStatus::Good, this, next)).unwrap();
assert_eq!(good.check(issuer, issuer, at), RevocationStatus::Good);
let revoked = OcspResponse::parse(&ocsp(
&id,
RevocationStatus::Revoked { at: Time { year: 2026, month: 6, day: 2, hour: 9, minute: 0, second: 0 } },
this,
next,
))
.unwrap();
assert_eq!(
revoked.check(issuer, issuer, at),
RevocationStatus::Revoked { at: Time { year: 2026, month: 6, day: 2, hour: 9, minute: 0, second: 0 } }
);
// Outside the window: unknown.
let later = Time { year: 2026, month: 8, day: 1, hour: 0, minute: 0, second: 0 };
assert_eq!(good.check(issuer, issuer, later), RevocationStatus::Unknown);
// Signed by a different key than the named responder: unknown.
let other = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
let wrong_key = OcspResponse::parse(&ocsp(&other, RevocationStatus::Good, this, next)).unwrap();
assert_eq!(wrong_key.check(issuer, issuer, at), RevocationStatus::Unknown);
// An error response is rejected at parse time.
let error = der::seq(&[&der::tlv(tag::ENUMERATED, &[6])]);
assert!(OcspResponse::parse(&error).is_err());
// responseStatus is an ENUMERATED (RFC 6960); an INTEGER is not one.
let error = der::seq(&[&der::int(6)]);
assert!(OcspResponse::parse(&error).is_err());
}
/// An embedded responder certificate answers for an issuer only when the issuer signed it for
/// OCSP signing. One that merely copies the issuer's name, found by its key hash, does not.
#[test]
fn a_responder_that_copies_the_issuers_name_does_not_answer_for_it() {
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
let issuer = &id.certificate;
let (this, next) = (mid_june(), Time { year: 2026, month: 7, day: 15, hour: 12, minute: 0, second: 0 });
let key = craft_pkcs7::PrivateKey::generate_p256().unwrap();
let impostor =
Certificate::self_signed(&issuer.subject, &key, Time { year: 2026, month: 1, day: 1, hour: 0, minute: 0, second: 0 }, 5, &[9]).unwrap();
assert_eq!(impostor.subject.raw, issuer.subject.raw);
let cert_id = der::seq(&[
&DigestAlg::Sha1.algorithm(),
&der::octets(&DigestAlg::Sha1.digest(&[&issuer.issuer.raw])),
&der::octets(&DigestAlg::Sha1.digest(&[&issuer.public_key.key_bits()])),
&der::uint(&issuer.serial),
]);
let revoked = der::tlv(tag::ctx(1), &Time { year: 2026, month: 6, day: 2, hour: 9, minute: 0, second: 0 }.encode());
let single = der::seq(&[&cert_id, &revoked, &this.encode(), &der::tlv(tag::ctx(0), &next.encode())]);
let by_key = der::tlv(tag::ctx(2), &der::octets(&DigestAlg::Sha1.digest(&[&impostor.public_key.key_bits()])));
// RFC 6960's `[0] EXPLICIT SEQUENCE OF Certificate`, and the certificate directly in `[0]`.
for certs in [der::explicit(0, &der::seq(&[&impostor.raw])), der::explicit(0, &impostor.raw)] {
let forged = OcspResponse::parse(&signed_ocsp(&key, &by_key, Some(&certs), &single, this)).unwrap();
assert_eq!(forged.check(issuer, issuer, mid_june()), RevocationStatus::Unknown);
}
}
// ── tolerance for the encodings found in old and unusual signers ───────────────────────────────
// RSA signatures over SHA-256("hello") by the key in rsa-aes.p12, made with OpenSSL 3 (see
// tests/data/README.md): the standard DigestInfo, DigestInfo without the NULL parameter, the bare
// digest (which is refused: it does not say which hash algorithm it uses), and RSASSA-PSS with a
// zero-length salt.
const RSA_STD: &str = "40896f76bc460e4ae9dc523dfe136159c7ccc20eff09bfeeafd570fd41a6c016d6b2a1d771ba81642dd5bf94d2bb9c4aeead237ce0e1662678efd5ff7b8590bf7d5042f6541b001a9f7d688e60a9fde5c9b3f295bd5373aea1059b53f1341f30a4db68302161e95c6a62847205782edcd1fa99b6720eaa60b59d3f54d4ec7be396c8f1146fe35f198bd12dca608b8080a446f89ba300fd74f8df4e7f177c59dc48cf878451423dd9674df768a12f85a1b5e81a5b8df43ac0fb03c4393cfed7dceeeddf1c8aef9a37e738ff4169a6881e783c2f841fb974eb0e6ecedf649bf64fbc51920b7824d86b26527acaec5d0ebb44454606441fea2b7d7b7a6014ab9d64";
const RSA_NO_NULL: &str = "a6589a056b04903cd77d5dae3b52f8c9905403ee2cef2e7343cfdd07746cbf1034c8c510a6f7258face9cfbcacb990e30f185dd091310d88efc3457e0fd34245d525e2cb5ceaab029e7b7fd25a581379c92e6abf90a36388cfc6afba0be47c99abc8b32e82a2c750ffe7c6d04de886cbfe63685cfc0f0204f641103e3c752496a0ae341fb503c4c2389824f7f20d07a03f0e3a95d5084fc1fb1b94c6bc607a14623b1bf7180b3d7d4592e217ba08698becb1994a6ef72ccab6e239418f11c57f184f59b0d1c7407216856cd4585da6a07354191ae85f8be02e670a10017f115ccb55d96f2ea2c1f095bbcf6b50ed8763945b78054989b016119c5371d918d682";
const RSA_BARE: &str = "a5812c29d3edf0b567636aaa501a960b977e774a0ea941e41401611d8b63464e99e2e908d8d8452b97112597f06f2adee029109bf6e421eb57add689b518532232199151ed822d1766013ea44ae5cb468b4a2288cf866f8c5954b729bfd34a8ca82d73bb397a75eb76687f7d9e506dab32e2da6231ece307fe8eb929c7648f73de6dd81c38aecb78a0e7ef0e11654fbadef88c7b206079afee70db8054ada079c6456826383dfd2504f05033b58cf1610a180953e501e137b47de5a77914cfaa33b8c588f24dcdb5789e7d1288ff009918f6c2808bfa1c57398d2e007a5e6d3004a3ca568a4ae2eb5cfa14083099e81cfcb53ead9ec6650a7c58e441d117d439";
const RSA_PSS_SALT0: &str = "2cdbb02d75a80df07dcca229363644caa4391cca30a3bc1281a3f4512dbe609727767c42e62cdad7d6bd38088b2c0b5dcb439fd2c337c31b47bbc39bda0e5c1486a773cde163fa2cad8047c06217541c993b21413e5cd341a4f66d2fb6e4755b52d25935dd90af667ce3d3e1825c8e6e40274dca7cf469ae17cbc3c1c1560d87a877a46353245128120beb1108cd0cafddae5b4ef9005601cc05f114da9de4cdaee08020d5b9be27e7b48f961e40664ef592990580bc1e1f2a8107c3d71d323df120720be6e862191a0aaa3b394b96efd2f45afa5f8211e2082865b8d3e3c28f99054976162ae8f42a903bfb82c17700db93fef603c4cd85c70a0be68fda9af8";
fn unhex(s: &str) -> Vec<u8> {
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
}
#[test]
fn rsa_signatures_are_read_with_every_digestinfo_variant() {
use craft_pkcs7::keys::{DigestAlg, PssParams, Scheme};
let id = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
let key = &id.certificate.public_key;
let digest = DigestAlg::Sha256.digest(&[b"hello"]);
for (name, sig) in [("standard", RSA_STD), ("no NULL", RSA_NO_NULL)] {
assert!(key.verify(Scheme::RsaPkcs1, DigestAlg::Sha256, &digest, &unhex(sig)).unwrap(), "{name}");
}
// Redundant leading zeros on the modulus change neither the result nor the arithmetic size.
if let craft_pkcs7::keys::PublicKey::Rsa { n, e } = key {
let pad = |v: &[u8]| [vec![0u8; 1 << 16], v.to_vec()].concat();
let padded = craft_pkcs7::keys::PublicKey::Rsa { n: pad(n), e: pad(e) };
assert!(padded.verify(Scheme::RsaPkcs1, DigestAlg::Sha256, &digest, &unhex(RSA_STD)).unwrap(), "padded key");
}
// The bare digest, with no DigestInfo, binds no hash algorithm: not a signature we accept.
assert!(!key.verify(Scheme::RsaPkcs1, DigestAlg::Sha256, &digest, &unhex(RSA_BARE)).unwrap(), "bare digest");
// PSS with the salt length it declares, or any when none is declared.
let pss = |salt_len| Scheme::RsaPss(PssParams { mgf: DigestAlg::Sha256, salt_len });
assert!(key.verify(pss(Some(0)), DigestAlg::Sha256, &digest, &unhex(RSA_PSS_SALT0)).unwrap());
assert!(key.verify(pss(None), DigestAlg::Sha256, &digest, &unhex(RSA_PSS_SALT0)).unwrap(), "salt length recovered");
assert!(!key.verify(pss(Some(32)), DigestAlg::Sha256, &digest, &unhex(RSA_PSS_SALT0)).unwrap(), "declared salt length is enforced");
// The check itself is not relaxed: another digest, a flipped bit, or the wrong scheme fail.
let other = DigestAlg::Sha256.digest(&[b"hellp"]);
let mut flipped = unhex(RSA_STD);
flipped[7] ^= 1;
for sig in [RSA_STD, RSA_NO_NULL, RSA_BARE] {
assert!(!key.verify(Scheme::RsaPkcs1, DigestAlg::Sha256, &other, &unhex(sig)).unwrap());
}
assert!(!key.verify(Scheme::RsaPkcs1, DigestAlg::Sha256, &digest, &flipped).unwrap());
assert!(!key.verify(pss(None), DigestAlg::Sha256, &digest, &unhex(RSA_STD)).unwrap());
}
#[test]
fn ecdsa_signatures_are_read_as_der_non_minimal_der_or_raw() {
use craft_pkcs7::der::{self, Tlv};
use craft_pkcs7::keys::{DigestAlg, Scheme};
for (file, field) in [("ec-p256.p12", 32usize), ("ec-p384.p12", 48)] {
let id = pkcs12::open(&data(file), "test").unwrap();
let alg = id.key.preferred_digest();
let msg = b"signed content";
let digest = alg.digest(&[msg]);
let sig = id.key.sign(alg, msg).unwrap();
let pk = id.key.public_key();
assert!(pk.verify(Scheme::Ecdsa, alg, &digest, &sig).unwrap(), "{file}: DER");
let ints = Tlv::parse_all(&sig).unwrap().children().unwrap();
let fixed = |i: &Tlv<'_>| {
let m = i.uint_bytes();
let mut v = vec![0u8; field - m.len()];
v.extend_from_slice(m);
v
};
let (r, s) = (fixed(&ints[0]), fixed(&ints[1]));
// Raw r ‖ s (PKCS #11 tokens).
let raw = [r.clone(), s.clone()].concat();
assert!(pk.verify(Scheme::Ecdsa, alg, &digest, &raw).unwrap(), "{file}: raw");
// Integers padded with redundant leading zeros.
let padded = |v: &[u8]| der::tlv(0x02, &[&[0u8, 0][..], v].concat());
let loose = der::seq(&[&padded(&r), &padded(&s)]);
assert!(pk.verify(Scheme::Ecdsa, alg, &digest, &loose).unwrap(), "{file}: non-minimal DER");
// Still a real check.
let mut bad = raw.clone();
bad[3] ^= 0x40;
assert!(!pk.verify(Scheme::Ecdsa, alg, &digest, &bad).unwrap());
assert!(!pk.verify(Scheme::Ecdsa, alg, &DigestAlg::Sha256.digest(&[b"other"]), &raw).unwrap());
assert!(!pk.verify(Scheme::Ecdsa, alg, &digest, &[1, 2, 3]).unwrap());
}
}
#[test]
fn tolerated_encodings_are_noted_and_standard_ones_are_not() {
use craft_pkcs7::der::{self, Tlv};
use craft_pkcs7::keys::{DigestAlg, Scheme};
let id = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
let key = &id.certificate.public_key;
let digest = DigestAlg::Sha256.digest(&[b"hello"]);
let rsa_notes = |sig: &[u8]| {
let mut notes = Vec::new();
assert!(key.verify_noting(Scheme::RsaPkcs1, DigestAlg::Sha256, &digest, sig, &mut notes).unwrap());
notes
};
assert!(rsa_notes(&unhex(RSA_STD)).is_empty(), "a standard signature needs no excuse");
let no_null = rsa_notes(&unhex(RSA_NO_NULL));
assert!(matches!(no_null.as_slice(), [n] if n.contains("no NULL")), "{no_null:?}");
// A zero byte in front of the signature value: still the same signature, but not modulus-length.
let padded: Vec<u8> = std::iter::once(0).chain(unhex(RSA_STD)).collect();
let long = rsa_notes(&padded);
assert!(matches!(long.as_slice(), [n] if n.contains("modulus")), "{long:?}");
for (file, field) in [("ec-p256.p12", 32usize), ("ec-p384.p12", 48)] {
let id = pkcs12::open(&data(file), "test").unwrap();
let alg = id.key.preferred_digest();
let digest = alg.digest(&[b"signed content"]);
let sig = id.key.sign(alg, b"signed content").unwrap();
let pk = id.key.public_key();
let ec_notes = |sig: &[u8]| {
let mut notes = Vec::new();
assert!(pk.verify_noting(Scheme::Ecdsa, alg, &digest, sig, &mut notes).unwrap(), "{file}");
notes
};
assert!(ec_notes(&sig).is_empty(), "{file}: DER as made");
let ints = Tlv::parse_all(&sig).unwrap().children().unwrap();
let fixed = |i: &Tlv<'_>| {
let m = i.uint_bytes();
let mut v = vec![0u8; field - m.len()];
v.extend_from_slice(m);
v
};
let (r, s) = (fixed(&ints[0]), fixed(&ints[1]));
let raw = ec_notes(&[r.clone(), s.clone()].concat());
assert!(matches!(raw.as_slice(), [n] if n.contains("raw")), "{file}: {raw:?}");
let padded = |v: &[u8]| der::tlv(0x02, &[&[0u8, 0][..], v].concat());
let loose = ec_notes(&der::seq(&[&padded(&r), &padded(&s)]));
assert!(matches!(loose.as_slice(), [n] if n.contains("canonical DER")), "{file}: {loose:?}");
}
}
/// `a + b` for big-endian numbers of equal width, or `None` if the sum does not fit.
fn add_be(a: &[u8], b: &[u8]) -> Option<Vec<u8>> {
let mut out = vec![0u8; a.len()];
let mut carry = 0u16;
for i in (0..a.len()).rev() {
let sum = u16::from(a[i]) + u16::from(b[i]) + carry;
out[i] = sum as u8;
carry = sum >> 8;
}
(carry == 0).then_some(out)
}
#[test]
fn weak_digests_say_so_in_words_that_are_pinned() {
use craft_pkcs7::keys::DigestAlg;
// What the signature panel shows for a signature that still validates but should not be
// relied on. Strong digests get no note at all.
for alg in DigestAlg::ALL {
let note = alg.weakness();
match alg {
DigestAlg::Sha1 => assert_eq!(
note,
Some(
"This signature uses SHA-1, a weak hash algorithm: collisions in it have been demonstrated. It still validates because many documents signed years ago use it, but it should not be relied on."
)
),
DigestAlg::Ripemd160 => assert_eq!(
note,
Some(
"This signature uses RIPEMD-160, a legacy 160-bit hash algorithm that is no longer recommended for signatures. It still validates, but it should not be relied on."
)
),
_ => assert_eq!(note, None, "{}", alg.name()),
}
}
}
#[test]
fn small_rsa_keys_say_so_and_others_do_not() {
let rsa = |n: Vec<u8>| PublicKey::Rsa { n, e: vec![1, 0, 1] };
let note = Some(
"This signature uses an RSA key shorter than 2048 bits, which is no longer considered strong enough for signatures. It still validates, but it should not be relied on.",
);
// 1024 bits, with and without the leading zero byte of a DER INTEGER.
assert_eq!(rsa(vec![0xC0; 128]).weakness(), note);
assert_eq!(rsa([vec![0], vec![0xC0; 128]].concat()).weakness(), note);
// 2047 bits is still short; 2048 bits and more are not.
assert_eq!(rsa([vec![0x7F], vec![0xFF; 255]].concat()).weakness(), note);
assert_eq!(rsa([vec![0, 0x80], vec![0; 255]].concat()).weakness(), None);
assert_eq!(rsa(vec![0xFF; 512]).weakness(), None);
assert_eq!(rsa(Vec::new()).weakness(), note, "an empty modulus is not a strong key");
// The key description counts the same bits, so it never says 2048 next to the warning.
assert_eq!(rsa([vec![0], vec![0xC0; 128]].concat()).describe(), "RSA 1024-bit");
assert_eq!(rsa([vec![0x7F], vec![0xFF; 255]].concat()).describe(), "RSA 2047-bit");
assert_eq!(rsa([vec![0, 0x80], vec![0; 255]].concat()).describe(), "RSA 2048-bit");
assert_eq!(PublicKey::P256(vec![4; 65]).weakness(), None);
let id = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
assert_eq!(id.certificate.public_key.weakness(), None, "the 2048-bit test identity");
}
/// A synthetic 1024-bit key made for this test (`openssl genrsa 1024`, e = 65537) and its
/// PKCS #1 v1.5 SHA-256 signature of `hello`.
const RSA_1024_N: &str = "ae561814194f0a06475c002fc7633b81e83a738ad3f35ba1ded959be6f6b62302028d58707403f44b7bda959e446f13849896711985ac4815e58d7d323aee664771c35bf2dc3db1bcdc49ac61ae7282a24ec7a5af9f6ebe98945e6a71c4998ed31c41bbb997c4873fb4b8271fd3cf76648efcbff76972ab7bcaf831b9e203d47";
const RSA_1024_SIG: &str = "0de5110137e768a6b00201cf8340688c81a901fe309489f2c8186b7dc56e67f7e64188970a9bea926b9bd99b19985adfb0489956ffd5e0264468b678c4fd49a155a895fcb9ab127d0c3742b7a07866b3deec20a5b2f61af4cbfd2901517dc268e4d6edf4e905e0f4e05cdc740c3180971c1ec1470c1476f90ea988e2ab4cbb18";
#[test]
fn a_signature_made_with_a_small_rsa_key_verifies_with_the_note() {
use craft_pkcs7::keys::Scheme;
let key = PublicKey::Rsa { n: unhex(RSA_1024_N), e: vec![1, 0, 1] };
let weak = key.weakness().expect("a 1024-bit key is short");
let sig = unhex(RSA_1024_SIG);
let mut notes = Vec::new();
let digest = DigestAlg::Sha256.digest(&[b"hello"]);
assert!(key.verify_noting(Scheme::RsaPkcs1, DigestAlg::Sha256, &digest, &sig, &mut notes).unwrap());
assert_eq!(notes, [weak]);
// A signature that doesn't verify says nothing about the key.
let mut notes = Vec::new();
let other = DigestAlg::Sha256.digest(&[b"hullo"]);
assert!(!key.verify_noting(Scheme::RsaPkcs1, DigestAlg::Sha256, &other, &sig, &mut notes).unwrap());
assert!(!notes.contains(&weak), "{notes:?}");
}
#[test]
fn an_rsa_signature_value_not_below_the_modulus_is_refused() {
use craft_pkcs7::keys::{DigestAlg, PssParams, Scheme};
// RFC 8017 §5.2.2 (RSAVP1): a signature representative s must be below n. Otherwise s and
// s + n are the same number modulo n and both would verify (malleability, not forgery).
let id = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
let key = &id.certificate.public_key;
let PublicKey::Rsa { n, .. } = key else { panic!("an RSA key") };
let digest = DigestAlg::Sha256.digest(&[b"hello"]);
let pss = Scheme::RsaPss(PssParams { mgf: DigestAlg::Sha256, salt_len: Some(0) });
let sig = unhex(RSA_PSS_SALT0);
assert!(key.verify(pss, DigestAlg::Sha256, &digest, &sig).unwrap(), "the signature as made");
let alias = add_be(&sig, n).expect("this signature plus the modulus still has the modulus' length");
assert_eq!(alias.len(), n.len());
assert!(!key.verify(pss, DigestAlg::Sha256, &digest, &alias).unwrap(), "s + n is not the signature");
}
#[test]
fn hostile_rsa_signature_values_are_wrong_not_crashes() {
use craft_pkcs7::keys::{DigestAlg, PssParams, Scheme};
let id = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
let key = &id.certificate.public_key;
let PublicKey::Rsa { n, .. } = key else { panic!("an RSA key") };
let digest = DigestAlg::Sha256.digest(&[b"hello"]);
let pss = Scheme::RsaPss(PssParams { mgf: DigestAlg::Sha256, salt_len: Some(32) });
let mut one_more = vec![1u8];
one_more.extend(vec![0u8; n.len()]);
for sig in [vec![], vec![0], vec![0; n.len()], vec![0xFF; n.len()], vec![0xFF; n.len() + 40], n.clone(), one_more] {
for scheme in [Scheme::RsaPkcs1, pss] {
assert!(!key.verify(scheme, DigestAlg::Sha256, &digest, &sig).unwrap(), "{} bytes", sig.len());
}
}
// A digest of the wrong length for PSS is a mismatch, not a crash either.
assert!(!key.verify(pss, DigestAlg::Sha256, &[1, 2, 3], &unhex(RSA_STD)).unwrap());
}
#[test]
fn keys_of_unknown_algorithms_are_unsupported_not_wrong() {
use craft_pkcs7::der::{self, Tlv};
use craft_pkcs7::keys::{DigestAlg, Scheme};
// An Ed448 key (1.3.101.113), which isn't supported: the key parses, checking says "can't".
let spki = der::seq(&[&der::seq(&[&der::oid("1.3.101.113")]), &der::bit_string(&[7u8; 57])]);
let key = PublicKey::from_spki(&Tlv::parse_all(&spki).unwrap()).unwrap();
assert_eq!(key.spki(), spki);
assert!(key.describe().contains("unsupported"));
let r = key.verify(Scheme::Ecdsa, DigestAlg::Sha256, &[0; 32], &[0; 64]);
assert!(matches!(r, Err(Error::Unsupported(_))), "{r:?}");
// An unknown curve likewise.
let spki = der::seq(&[&der::seq(&[&der::oid("1.2.840.10045.2.1"), &der::oid("1.3.132.0.10")]), &der::bit_string(&[4u8; 65])]);
let key = PublicKey::from_spki(&Tlv::parse_all(&spki).unwrap()).unwrap();
assert!(matches!(key.verify(Scheme::Ecdsa, DigestAlg::Sha256, &[0; 32], &[0; 64]), Err(Error::Unsupported(_))));
}
// ECDSA signatures by OpenSSL 3 over a digest of "hello": (SubjectPublicKeyInfo, digest, DER signature).
const P521: (&str, &str, &str) = (
"30819b301006072a8648ce3d020106052b8104002303818600040079586325f44d28973f270dcb9595a6309b93c3898eedb901b08873694c690265e61c1cbf5b1a339cacdcfe3ece7e00744bf6ed158d663209c8acf378ddda2e824100b1386a84a2efa8d4f78b6cf7018e58fac5bb9a57f33b3ef584649ba57910a163ba026245ad9e1b93597b5db1f05431267b3953c205312d0c8bf0d910eef237f438",
"9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043",
"30818802420156bae253f050fc9202661280cbb8b1eedb8d4767aee63afdd130d376f50a18097902d0c6bd17eb8e4b4383f355751f3578e06850d5f3dfd8a86f3085e7100535b4024201f92f6f722290652195edf7b3168a91425a4e8a70386e06af6bceafd8a5f26dc19e5c13e7566e789d9459433e911e439819fdc350ea8ab1df6421257eaa55e23cf8",
);
const BP256: (&str, &str, &str) = (
"305a301406072a8648ce3d020106092b2403030208010107034200049ba05e64295ad9b86295d9cfa717a2549474e83abb2825be32a0fae5297b07344f9e9018cf394001f98d634680b1e20be49f6118c726fdad4b11ccae99b95358",
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"304402203815740bcccfdec285f1e168f28b4529bb39eba2683085260653c7a53dd2e69e022048ba910ce1f9141d8213fa1804a3d548271e0b9c307f6b26c80951be7e7a59a2",
);
const BP384: (&str, &str, &str) = (
"307a301406072a8648ce3d020106092b240303020801010b03620004874bc125ea23dd5553b1e5440c5cb064032f9a6cbdea271745232ade525b082951d0b251a45b2af74d3325dbe7872f9635b6223c6b2cd4d7f24a50fd9058f946a154919adb24e268975df951e561884b14f899bc87af0820c50f3b706b7acb4c",
"59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f",
"30640230284776ae1458b137bfe0f1628193983ae8bd5d58d84957fb215da42c5ba0e48400aaba4960b6e0a23aa8d7c20084cbba02301000b700ae6d946ad032f7a08e64c565bb6227e722edd2813dcbc5dfa79d099cf93ea8d48b4293869942a4f68671a915",
);
#[test]
fn verifies_ecdsa_on_p521_and_the_brainpool_curves() {
use craft_pkcs7::der::Tlv;
use craft_pkcs7::keys::{DigestAlg, Scheme};
for (name, (spki, digest, sig), alg, describe) in [
("P-521", P521, DigestAlg::Sha512, "ECDSA P-521"),
("brainpoolP256r1", BP256, DigestAlg::Sha256, "ECDSA brainpoolP256r1"),
("brainpoolP384r1", BP384, DigestAlg::Sha384, "ECDSA brainpoolP384r1"),
] {
let spki = unhex(spki);
let key = PublicKey::from_spki(&Tlv::parse_all(&spki).unwrap()).unwrap();
assert_eq!(key.describe(), describe);
assert_eq!(key.spki(), spki, "{name}: re-encodes");
let (digest, sig) = (unhex(digest), unhex(sig));
assert!(key.verify(Scheme::Ecdsa, alg, &digest, &sig).unwrap(), "{name}");
let mut bad = digest.clone();
bad[0] ^= 1;
assert!(!key.verify(Scheme::Ecdsa, alg, &bad, &sig).unwrap(), "{name}: other digest");
// The digest named by the algorithm identifier may be shorter than the curve (SHA-256 on
// P-521 etc.): that is a different digest, so it must not verify this signature.
assert!(!key.verify(Scheme::Ecdsa, alg, &digest[..digest.len() / 2], &sig).unwrap(), "{name}: short digest");
}
}
+8
View File
@@ -0,0 +1,8 @@
//! The crate's integration tests, linked into one test binary.
mod algorithms;
mod ber;
mod chain;
mod cms;
mod crypto;
mod store_keys;
+89
View File
@@ -0,0 +1,89 @@
//! What the Windows certificate store (`src/windows.rs`, built only on Windows) may sign with,
//! checked on every platform: which certificate keys it lists, and how a CNG signature becomes
//! the one a CMS signature carries.
use craft_pkcs7::keys::{DigestAlg, Scheme, StoreKey};
use craft_pkcs7::{Error, PublicKey};
use p256::ecdsa::signature::hazmat::PrehashSigner;
/// One key of every kind, with what the store must hold to sign for it.
fn every_kind() -> Vec<(PublicKey, Option<StoreKey>)> {
let point = vec![4u8; 65];
let keys = vec![
(PublicKey::Rsa { n: vec![0xc5; 256], e: vec![1, 0, 1] }, Some(StoreKey::Rsa)),
(PublicKey::P256(point.clone()), Some(StoreKey::Ecdsa(256))),
(PublicKey::P384(point.clone()), Some(StoreKey::Ecdsa(384))),
(PublicKey::P521(point.clone()), None),
(PublicKey::BrainpoolP256(point.clone()), None),
(PublicKey::BrainpoolP384(point.clone()), None),
(PublicKey::BrainpoolP512(point.clone()), None),
(PublicKey::Ed25519(vec![7; 32]), None),
(PublicKey::Unsupported { what: "elliptic curve 1.3.132.0.10".into(), spki: Vec::new() }, None),
];
// Every variant is named, with no `_` arm: a new one stops this test compiling until it is
// listed above with the answer the store should give.
for (key, _) in &keys {
match key {
PublicKey::Rsa { .. }
| PublicKey::P256(_)
| PublicKey::P384(_)
| PublicKey::P521(_)
| PublicKey::BrainpoolP256(_)
| PublicKey::BrainpoolP384(_)
| PublicKey::BrainpoolP512(_)
| PublicKey::Ed25519(_)
| PublicKey::Unsupported { .. } => {}
}
}
keys
}
#[test]
fn the_store_signs_only_with_rsa_p256_and_p384() {
for (key, want) in every_kind() {
assert_eq!(key.store_signing_key(), want, "{}", key.describe());
if want.is_none() {
// Skipped when listing, and refused (not a panic) if one ever reached signing.
let refused = key.store_signature_der(&[0; 64]);
assert!(matches!(refused, Err(Error::Unsupported(_))), "{}: {refused:?}", key.describe());
}
}
}
#[test]
fn cng_signatures_become_cms_signatures_that_verify() {
let msg = b"hello";
let k = p256::ecdsa::SigningKey::from_slice(&[0x11; 32]).unwrap();
let public = PublicKey::P256(k.verifying_key().to_sec1_point(false).as_bytes().to_vec());
let digest = DigestAlg::Sha256.digest(&[msg]);
let s: p256::ecdsa::Signature = k.sign_prehash(&digest).unwrap();
// CNG returns r ‖ s, 32 bytes each for P-256.
let der = public.store_signature_der(&s.to_bytes()).unwrap();
assert_eq!(der.first(), Some(&0x30), "a DER SEQUENCE");
assert!(public.verify(Scheme::Ecdsa, DigestAlg::Sha256, &digest, &der).unwrap());
let k = p384::ecdsa::SigningKey::from_slice(&[0x22; 48]).unwrap();
let public = PublicKey::P384(k.verifying_key().to_sec1_point(false).as_bytes().to_vec());
let digest = DigestAlg::Sha384.digest(&[msg]);
let s: p384::ecdsa::Signature = k.sign_prehash(&digest).unwrap();
let der = public.store_signature_der(&s.to_bytes()).unwrap();
assert!(public.verify(Scheme::Ecdsa, DigestAlg::Sha384, &digest, &der).unwrap());
// RSA PKCS #1 v1.5 signatures are already what CMS carries.
let rsa = PublicKey::Rsa { n: vec![0xc5; 256], e: vec![1, 0, 1] };
assert_eq!(rsa.store_signature_der(&[9; 256]).unwrap(), vec![9; 256]);
}
#[test]
fn a_malformed_cng_signature_is_an_error() {
// Wrong lengths, and right lengths whose r and s are zero or not below the group order.
let p256: [&[u8]; 5] = [&[], &[1; 63], &[1; 65], &[0; 64], &[0xff; 64]];
let p384: [&[u8]; 5] = [&[], &[1; 95], &[1; 97], &[0; 96], &[0xff; 96]];
for (key, bad) in [(PublicKey::P256(Vec::new()), p256), (PublicKey::P384(Vec::new()), p384)] {
for raw in bad {
let got = key.store_signature_der(raw);
assert!(matches!(got, Err(Error::Crypto(_))), "{}, {} bytes: {got:?}", key.describe(), raw.len());
}
}
}