mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
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:
1 parent
493476eb8d
commit
1c5c8a4e14
33 files changed
+6554
-88
No files matched your search
@@ -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"
|
||||
@@ -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`.
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
@@ -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-----
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
@@ -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, &[¬_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(&[¬_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}");
|
||||
}
|
||||
@@ -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(¬_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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user