M9: more signature validation algorithms (SHA-3, RIPEMD-160, Ed25519, P-521, brainpool, PSS, rsa_sha1) (#386)

* M9: more signature validation algorithms

Validation now reads what real signers write, and says when it is weak:

- Digests SHA-224, SHA-512/224, SHA-512/256, SHA3-224/256/384/512 and
  RIPEMD-160, beside SHA-1 and SHA-2.
- RSA PKCS#1 v1.5 for every digest, in `rsa_pad`: the one expected encoding
  is built and compared byte for byte, with or without the DigestInfo's NULL
  (RFC 8017 App. B.1). A signature over the bare digest, which doesn't say
  which hash it uses, is refused. The signature value must be below the
  modulus (RFC 8017 §5.2.2); s and s + n both verified before.
- RSA-PSS with the hash, MGF1 hash and salt length the signature declares.
- ECDSA as DER, non-minimal DER or raw r||s on P-256, P-384, P-521 and
  brainpoolP256r1/P384r1/P512r1 (BSI "plain" ECDSA OIDs too); brainpoolP512r1
  is hand-written on crypto-bigint because no RustCrypto crate has it.
- Ed25519 (RFC 8419), and the legacy adbe.x509.rsa_sha1 format.
- An algorithm PdfCraft can't check is Unknown, never Invalid.
- SHA-1 and RIPEMD-160 signatures validate, but the signature's details say
  the algorithm is weak (wording pinned by tests).
- Encoding irregularities that were tolerated are listed in the details
  (`verify_noting`; `verify` keeps its signature).

Found while testing, each with a regression test that failed before the fix:
a brainpoolP512r1 public key with a coordinate >= p panicked inside
crypto-bigint's `sub_mod` (the certificate comes from the PDF); it is refused
now, and the point at infinity is an invalid signature, not an error.

Vectors were made with OpenSSL (tests/data/README.md has the commands).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Update comment to clarify MAC digest restrictions

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* M9: a legacy x509.rsa_sha1 signature whose key the verifier rejects is Invalid; only unsupported keys are "can't check yet"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Thorsten Hirsch 2026-10-10 00:44:38 +02:00 committed by GitHub
parent c957288d3b
commit 56f85f76df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1508 additions and 106 deletions

View File

@ -4,7 +4,7 @@
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
## In this repository (257)
## In this repository (258)
| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
@ -265,6 +265,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `assets/fonts/DancingScript.ttf` | Dancing Script (variable) | The Dancing Script Project Authors (Pablo Impallari) | OFL-1.1 | https://github.com/google/fonts/blob/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/dancingscript/DancingScript%5Bwght%5D.ttf | Fill & Sign typed signatures and initials: the typed name's glyph outlines, drawn as filled paths (the font file itself is not embedded in PDFs) |
| `packaging/macos/dmg/background.svg` | PdfCraft macOS DMG window background | @XusBadia | MIT OR Apache-2.0 | Original work for this repository, derived from the PdfCraft app icon (assets/app-icon/pdfcraft.svg, referenced via an SVG image element, not copied) | Finder window of the macOS DMG; rendered to background.tiff by packaging/macos/dmg/generate.py |
| `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile |
| `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): the legacy adbe.x509.rsa_sha1 format |
## Compiled in through dependencies (6)

View File

@ -3208,3 +3208,14 @@ licence = "OFL-1.1"
attribution = "https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md"
kind = "font"
usage = "Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); the web build embeds only BIZ UDPGothic Regular"
[[asset]]
path = "crates/sign/tests/data/x509-rsa-sha1.pdf"
title = "One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key"
author = "PdfCraft contributors"
source = "Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md)"
licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "document"
usage = "Signature validation tests (crates/sign/tests/pdf.rs): the legacy adbe.x509.rsa_sha1 format"
sha256 = "b77bfb21166f4bd043597e45a1ea28b9d9f4ba6df0142b3a0fc8b7f8e0fc111b"

131
Cargo.lock generated
View File

@ -730,6 +730,30 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "bp256"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f98c2d9e8e0796b24f84b7ae14159b91ae6ae4b85f74f7655a678a56ad5e1e"
dependencies = [
"ecdsa",
"elliptic-curve",
"primefield",
"primeorder",
]
[[package]]
name = "bp384"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc61f3a96b44a33fbb971e9917b81f4207d6dae9a6b0715845c785a48e73d9c2"
dependencies = [
"ecdsa",
"elliptic-curve",
"primefield",
"primeorder",
]
[[package]]
name = "brotli"
version = "8.0.4"
@ -1160,6 +1184,33 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "curve25519-dalek"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "dashmap"
version = "6.2.1"
@ -1338,6 +1389,28 @@ dependencies = [
"serde",
]
[[package]]
name = "ed25519"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"
dependencies = [
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
dependencies = [
"curve25519-dalek",
"ed25519",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "eframe"
version = "0.36.2"
@ -2614,6 +2687,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keccak"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598"
dependencies = [
"cfg-if",
"cpufeatures",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@ -3400,6 +3483,20 @@ dependencies = [
"sha2",
]
[[package]]
name = "p521"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449"
dependencies = [
"base16ct",
"ecdsa",
"elliptic-curve",
"primefield",
"primeorder",
"sha2",
]
[[package]]
name = "parking"
version = "2.2.1"
@ -3770,22 +3867,30 @@ dependencies = [
"aes",
"aws-lc-rs",
"base64 0.22.1",
"bp256",
"bp384",
"cbc",
"crypto-bigint",
"des",
"ecdsa",
"ed25519-dalek",
"getrandom 0.4.3",
"hmac",
"p256",
"p384",
"p521",
"pbkdf2",
"pdfcraft-annot",
"pdfcraft-cos",
"pdfcraft-fonts",
"rc2",
"ripemd",
"rsa",
"rustls-cng",
"security-framework",
"sha1",
"sha2",
"sha3",
"thiserror 2.0.21",
]
@ -4556,6 +4661,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "ripemd"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dd4211456b4172d7e44261920c25acf07367c4f04bb5f5d54fc21b090d9b159"
dependencies = [
"digest",
]
[[package]]
name = "ron"
version = "0.12.2"
@ -5006,6 +5120,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha3"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759"
dependencies = [
"digest",
"keccak",
"sponge-cursor",
]
[[package]]
name = "shlex"
version = "2.0.1"
@ -5223,6 +5348,12 @@ dependencies = [
"der",
]
[[package]]
name = "sponge-cursor"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"

View File

@ -174,6 +174,8 @@ Newest first. One line per session: the date, what moved, and the new overall pe
- **2026-10-09 (M9, signatures as DocuSign and Documenso write them, #144):** A real DocuSign + Telekom qualified-signature PDF showed "DER signature wrong" although Acrobat validates it: both CMS blobs use BER indefinite lengths. The CMS is read with the BER reader that #453 added (`Tlv::parse_ber`: indefinite lengths, segmented OCTET STRINGs, depth-bounded) while certificates and signed attributes are still verified over their own exact bytes, and the details say the signature was BER; tests build the nesting bombs a crafted /Contents could hold. Behind that, later revisions that add DSS data (indirect `/Certs`, `/VRI`) or rewrite the catalog's XMP were classified as disallowed changes; they are permitted now, narrowly (review by echelon on #144): only objects reached through `/DSS` `/Certs|CRLs|OCSPs|VRI` with the shape of their role, and new or only grown since signing, count as the store, and only the catalog's `/Metadata` is the document's XMP. A `/DSS` entry naming a page's contents, or a `/Type /DSS` or `/Type /Metadata` label on an existing dictionary or stream, makes nothing permitted. Six of the new tests fail on the previous code, among them a Form XObject labelled `/Type /Metadata` before signing that could be rewritten under a Valid signature. Negative tests at every DocMDP level. Cross-checked with pdfsig (both signatures valid). Still missing: AATL/EUTL trust lists (a separate change: they need CA-constraint checks in chain building first). Overall estimate unchanged (about 30–35%).
- **2026-10-09 (M9, signature algorithms, #386):** Validation reads what real signers write. Digests SHA-224, SHA-512/224, SHA-512/256, SHA3-224/256/384/512 and RIPEMD-160 (beside SHA-1/2); RSA PKCS#1 with or without the DigestInfo's NULL (checked by building the one expected encoding and comparing it byte for byte, in `rsa_pad`, for every digest; a bare digest without any DigestInfo is refused, it doesn't say which hash it uses); RSA-PSS with the hash, MGF1 hash and salt length the signature declares; ECDSA as DER, non-minimal DER or raw r‖s on P-256, P-384, P-521 and brainpoolP256r1/P384r1/P512r1 (the last hand-written on `crypto-bigint`, no RustCrypto crate exists); Ed25519 (RFC 8419); the legacy `adbe.x509.rsa_sha1` format. Algorithms PdfCraft can't check give Unknown instead of Invalid. Review of #386 (echelon): an RSA signature value must be below the modulus (RFC 8017 §5.2.2; `s` and `s + n` both verified before), and SHA-1 and RIPEMD-160 signatures still validate but the signature's details now say the algorithm is weak (wording pinned by tests). Found on the way, each with a regression test that failed first: a brainpoolP512r1 key with a coordinate not reduced modulo p panicked inside the bigint arithmetic (the certificate comes from the PDF), now refused, and the point at infinity is an invalid signature rather than an error. Tolerated encodings are listed in the signature's details. Vectors made with OpenSSL; cross-checked with pdfsig. Still missing: Ed448/DSA, signing with the new digests. Overall estimate unchanged (about 30–35%).
- **2026-10-09 (M14, German interface follow-up):** Follow the merged German catalog with shorter search/action labels, corrected callout/accessibility wording and a distinct certificate Subject context. Localized tool names retain their capitalization and comment composer titles go through the catalog. Search hints reserve shortcut space and Home cards bound text with full hover text; layout, context and German Preferences/search control regression tests cover these changes. Overall effort estimate remains about 30–35%.
- **2026-10-09 (community, destinations):** Bookmarks and links go to their destination's position, not just its page (ISO 32000-2 §12.3.2.2): `/XYZ` puts (left, top) at the window's top-left and applies a non-zero zoom; `/Fit`, `/FitH` and `/FitV` switch to fit page, width and height with `top` or `left` at the window's edge; `/FitR` zooms to fit the rectangle, centred. Named destinations resolve the same way; crop-box origin, `/Rotate` and view rotation are accounted for; malformed or non-finite operands fall back to the top of the page. Not yet: the `/FitB*` content box (read as the page). Overall estimate unchanged (about 30–35%).

View File

@ -20,14 +20,23 @@ sha1 = "0.11"
sha2 = "0.11"
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"] }
# Verification only on native targets (RSA private-key operations use aws-lc-rs, ADR-0009).
rsa = { version = "=0.10.0-rc.18", default-features = false, features = ["std"] }
crypto-bigint = { version = "0.7", 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"
sha3 = "0.12.0"
ripemd = "0.2.0"
ed25519-dalek = "3.0.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
aws-lc-rs = { version = "1.18", default-features = false, features = ["aws-lc-sys", "prebuilt-nasm"] }

View File

@ -72,6 +72,26 @@ ECDSA P-256/P-384; the store integration is tested with software-backed keys.
document's XMP, and only objects reached through the `/DSS` keys above are the security store. A
`/Type /Metadata` or `/Type /DSS` *label* on any other dictionary or stream (a page's Form
XObject, say) makes nothing permitted, at every DocMDP level.
- **Strict in what it checks, tolerant where signers differ.** RSA PKCS #1 signatures are checked
against the one encoding the digest should have, built here and compared byte for byte; the
`DigestInfo` may omit its NULL parameter, as older signers write it (RFC 8017 App. B.1), but a
signature over the bare digest, which doesn't say which hash it uses, is refused. A signature
value must be below the modulus (RFC 8017 §5.2.2). RSA-PSS uses the salt length it declares.
ECDSA is read as DER, non-minimal DER or raw `r ‖ s`. `adbe.x509.rsa_sha1` is validated. What the
check tolerated is listed in the signature's details: a DigestInfo without its NULL, an RSA value
that isn't modulus-length, ECDSA that isn't canonical DER, a SignerInfo whose digest algorithm
differs from the one the signature algorithm names. What PdfCraft can't check (an unknown
algorithm or curve) is *unknown*, never *invalid*.
- **Weak algorithms still validate, and say so.** SHA-1 and RIPEMD-160 signatures are common in
documents signed years ago, so they validate (new signatures never use SHA-1), but the signature's
details say that the algorithm is weak and shouldn't be relied on.
- **Validation algorithms:** digests SHA-1, SHA-224/256/384/512, SHA-512/224, SHA-512/256,
SHA3-224/256/384/512 and RIPEMD-160. RSA PKCS #1 v1.5 and RSASSA-PSS (the hash, MGF1 hash and
salt length the signature declares), ECDSA on P-256, P-384, P-521, brainpoolP256r1, P384r1 and
P512r1 (also BSI "plain" ECDSA), and Ed25519 (RFC 8419, over the signed attributes).
Signing: RSA, P-256, P-384 with SHA-256/384/512. The RSA padding checks and brainpoolP512r1 are
written here (`rsa_pad`, `ec512`) on `crypto-bigint`, because the typed APIs of the `rsa` crate
and RustCrypto don't cover them.
Oracles: poppler's `pdfsig` reports our signatures valid; OpenSSL reads our `.p12` files and
verifies our CMS; `tests/data/openssl-signed.pdf` is a signature OpenSSL made, which we validate.

View File

@ -124,14 +124,51 @@ impl SignedData {
/// 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.
pub fn verify_signature(&self, cert: &Certificate, content_digest: &[u8]) -> bool {
/// `Err(Unsupported)`: the signer's key algorithm can't be checked here.
pub fn verify_signature(&self, cert: &Certificate, content_digest: &[u8]) -> Result<bool, SignError> {
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, SignError> {
let s = &self.signer;
let alg = s.scheme_digest.unwrap_or(s.digest);
let digest = match &s.signed_attrs {
Some(attrs) => alg.digest(&[attrs]),
None => content_digest.to_vec(),
};
cert.public_key.verify(s.scheme, alg, &digest, &s.signature).unwrap_or(false)
// 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(SignError::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)
}
}

180
crates/sign/src/ec512.rs Normal file
View File

@ -0,0 +1,180 @@
//! ECDSA verification on brainpoolP512r1 (RFC 5639), for which no RustCrypto curve crate exists.
//!
//! Verification only, on public data, so plain affine arithmetic over `crypto-bigint` is enough:
//! no secret is involved, and a verification takes a few milliseconds. Checked against
//! signatures made by OpenSSL (`tests/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.bits_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));
}
}
}

View File

@ -9,66 +9,112 @@ 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_SHA1: &str = "1.2.840.113549.1.1.5";
pub const RSA_PSS: &str = "1.2.840.113549.1.1.10";
pub const RSA_SHA256: &str = "1.2.840.113549.1.1.11";
pub const RSA_SHA384: &str = "1.2.840.113549.1.1.12";
pub const RSA_SHA512: &str = "1.2.840.113549.1.1.13";
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 ECDSA_SHA1: &str = "1.2.840.10045.4.1";
pub const ECDSA_SHA256: &str = "1.2.840.10045.4.3.2";
pub const ECDSA_SHA384: &str = "1.2.840.10045.4.3.3";
pub const ECDSA_SHA512: &str = "1.2.840.10045.4.3.4";
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> {
Some(match o {
oid::SHA1 => DigestAlg::Sha1,
oid::SHA256 => DigestAlg::Sha256,
oid::SHA384 => DigestAlg::Sha384,
oid::SHA512 => DigestAlg::Sha512,
_ => return None,
})
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 run<D: sha2::Digest>(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);
@ -76,10 +122,51 @@ impl DigestAlg {
h.finalize().to_vec()
}
match self {
DigestAlg::Sha1 => run::<sha1::Sha1>(parts),
DigestAlg::Sha256 => run::<sha2::Sha256>(parts),
DigestAlg::Sha384 => run::<sha2::Sha384>(parts),
DigestAlg::Sha512 => run::<sha2::Sha512>(parts),
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();
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,
}
}
@ -89,52 +176,130 @@ impl DigestAlg {
}
}
/// 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,
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>), SignError> {
let parts = alg.children()?;
let o = parts.first().ok_or_else(|| SignError::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::RSA_SHA1 => (Scheme::RsaPkcs1, Some(DigestAlg::Sha1)),
oid::RSA_SHA256 => (Scheme::RsaPkcs1, Some(DigestAlg::Sha256)),
oid::RSA_SHA384 => (Scheme::RsaPkcs1, Some(DigestAlg::Sha384)),
oid::RSA_SHA512 => (Scheme::RsaPkcs1, Some(DigestAlg::Sha512)),
oid::RSA_PSS => {
// RSASSA-PSS-params: [0] hashAlgorithm (default SHA-1).
let hash = parts
.get(1)
.and_then(|p| p.children().ok())
.and_then(|c| c.into_iter().find(|t| t.tag == tag::ctx(0)))
.and_then(|h| h.inner().ok())
.and_then(|a| a.children().ok())
.and_then(|a| a.first().and_then(|o| o.oid().ok()))
.and_then(|o| DigestAlg::from_oid(&o))
.unwrap_or(DigestAlg::Sha1);
(Scheme::RsaPss, Some(hash))
}
oid::ECDSA_SHA1 => (Scheme::Ecdsa, Some(DigestAlg::Sha1)),
oid::ECDSA_SHA256 => (Scheme::Ecdsa, Some(DigestAlg::Sha256)),
oid::ECDSA_SHA384 => (Scheme::Ecdsa, Some(DigestAlg::Sha384)),
oid::ECDSA_SHA512 => (Scheme::Ecdsa, Some(DigestAlg::Sha512)),
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(SignError::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), SignError> {
let hash_of = |alg: &Tlv<'_>| -> Result<DigestAlg, SignError> {
let o = alg.children()?.first().ok_or_else(|| SignError::Malformed("PSS hash".into()))?.oid()?;
DigestAlg::from_oid(&o).ok_or_else(|| SignError::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(SignError::Unsupported("PSS mask generation function other than MGF1".into()));
}
mgf = hash_of(m.get(1).ok_or_else(|| SignError::Malformed("MGF1 hash".into()))?)?;
}
t if t == tag::ctx(2) => {
salt_len = usize::try_from(field.inner()?.u64()?).map_err(|_| SignError::Malformed("PSS salt length".into()))?
}
_ => {}
}
}
}
Ok((hash, PssParams { mgf, salt_len: Some(salt_len) }))
}
/// A public key from a `SubjectPublicKeyInfo`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PublicKey {
Rsa { n: Vec<u8>, e: Vec<u8> },
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 PdfCraft can't check signatures with. The certificate still
/// parses (so its name and dates show); verifying with it gives [`SignError::Unsupported`].
Unsupported {
what: String,
spki: Vec<u8>,
},
}
impl PublicKey {
@ -143,7 +308,19 @@ impl PublicKey {
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) => bits.clone(),
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(),
}
}
@ -154,6 +331,7 @@ impl PublicKey {
let o = alg.first().ok_or_else(|| SignError::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(SignError::Malformed("RSAPublicKey".into())) };
@ -162,9 +340,13 @@ impl PublicKey {
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())),
other => Err(SignError::Unsupported(format!("elliptic curve {}", other.unwrap_or("?")))),
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 => Err(SignError::Unsupported(format!("public key algorithm {other}"))),
other => Ok(PublicKey::Unsupported { what: format!("public key algorithm {other}"), spki: spki.raw.to_vec() }),
}
}
@ -177,6 +359,12 @@ impl PublicKey {
}
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(),
}
}
@ -186,43 +374,118 @@ impl PublicKey {
PublicKey::Rsa { n, .. } => format!("RSA {}-bit", n.len() * 8),
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})"),
}
}
/// Check `sig` over a message whose digest (with `alg`) is `digest`.
/// 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, SignError> {
self.verify_noting(scheme, alg, digest, sig, &mut Vec::new())
}
/// [`PublicKey::verify`], noting in `notes` each encoding irregularity it tolerated. 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, SignError> {
match (self, scheme) {
(PublicKey::Rsa { n, e }, Scheme::RsaPkcs1 | Scheme::RsaPss) => {
use rsa::{BoxedUint, Pkcs1v15Sign, RsaPublicKey, pss::Pss};
let key = RsaPublicKey::new(BoxedUint::from_be_slice_vartime(n), BoxedUint::from_be_slice_vartime(e))
.map_err(|e| SignError::Malformed(format!("RSA key: {e}")))?;
let ok = match (scheme, alg) {
(Scheme::RsaPkcs1, DigestAlg::Sha1) => key.verify(Pkcs1v15Sign::new::<sha1::Sha1>(), digest, sig),
(Scheme::RsaPkcs1, DigestAlg::Sha256) => key.verify(Pkcs1v15Sign::new::<sha2::Sha256>(), digest, sig),
(Scheme::RsaPkcs1, DigestAlg::Sha384) => key.verify(Pkcs1v15Sign::new::<sha2::Sha384>(), digest, sig),
(Scheme::RsaPkcs1, DigestAlg::Sha512) => key.verify(Pkcs1v15Sign::new::<sha2::Sha512>(), digest, sig),
(_, DigestAlg::Sha1) => key.verify(Pss::<sha1::Sha1>::new(), digest, sig),
(_, DigestAlg::Sha256) => key.verify(Pss::<sha2::Sha256>::new(), digest, sig),
(_, DigestAlg::Sha384) => key.verify(Pss::<sha2::Sha384>::new(), digest, sig),
(_, DigestAlg::Sha512) => key.verify(Pss::<sha2::Sha512>::new(), digest, sig),
};
Ok(ok.is_ok())
}
(PublicKey::Unsupported { what, .. }, _) => Err(SignError::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(SignError::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(|_| SignError::Malformed("P-256 key".into()))?;
let Ok(s) = p256::ecdsa::Signature::from_der(sig) else { return Ok(false) };
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(|_| SignError::Malformed("P-384 key".into()))?;
let Ok(s) = p384::ecdsa::Signature::from_der(sig) else { return Ok(false) };
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(|_| SignError::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(|_| SignError::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(|_| SignError::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(|| SignError::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, SignError> {
match (self, scheme) {
(PublicKey::Unsupported { what, .. }, _) => Err(SignError::Unsupported(what.clone())),
(PublicKey::Ed25519(k), Scheme::Ed25519) => {
let bytes: [u8; 32] = k.as_slice().try_into().map_err(|_| SignError::Malformed("Ed25519 key".into()))?;
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(|_| SignError::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 {
@ -370,16 +633,14 @@ impl PrivateKey {
/// The signature `AlgorithmIdentifier` this key writes with `alg`.
pub fn signature_algorithm(&self, alg: DigestAlg) -> Vec<u8> {
match (&self.public, alg) {
(PublicKey::Rsa { .. }, DigestAlg::Sha1) => der::algorithm(oid::RSA_SHA1, Some(&der::null())),
(PublicKey::Rsa { .. }, DigestAlg::Sha256) => der::algorithm(oid::RSA_SHA256, Some(&der::null())),
(PublicKey::Rsa { .. }, DigestAlg::Sha384) => der::algorithm(oid::RSA_SHA384, Some(&der::null())),
(PublicKey::Rsa { .. }, DigestAlg::Sha512) => der::algorithm(oid::RSA_SHA512, Some(&der::null())),
(_, DigestAlg::Sha1) => der::algorithm(oid::ECDSA_SHA1, None),
(_, DigestAlg::Sha256) => der::algorithm(oid::ECDSA_SHA256, None),
(_, DigestAlg::Sha384) => der::algorithm(oid::ECDSA_SHA384, None),
(_, DigestAlg::Sha512) => der::algorithm(oid::ECDSA_SHA512, None),
}
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.
@ -392,8 +653,8 @@ impl PrivateKey {
/// Sign `msg` (hashed with `alg`). New signatures never use SHA-1.
pub fn sign(&self, alg: DigestAlg, msg: &[u8]) -> Result<Vec<u8>, SignError> {
if alg == DigestAlg::Sha1 {
return Err(SignError::Unsupported("SHA-1 for new signatures".into()));
if !matches!(alg, DigestAlg::Sha256 | DigestAlg::Sha384 | DigestAlg::Sha512) {
return Err(SignError::Unsupported(format!("{} for new signatures", alg.name())));
}
match &self.inner {
#[cfg(not(target_arch = "wasm32"))]

View File

@ -10,12 +10,14 @@
pub mod cms;
pub mod der;
pub mod dss;
mod ec512;
#[cfg(target_os = "macos")]
pub mod keychain;
pub mod keys;
pub mod pdf;
pub mod pkcs12;
pub mod revocation;
mod rsa_pad;
pub mod timestamp;
#[cfg(target_os = "windows")]
pub mod windows;

View File

@ -528,12 +528,18 @@ fn validate_into(doc: &Document, bytes: &[u8], trust: &TrustStore, v: &Dict, inf
info.signed_len = covered;
// The signed revision's number: its cross-reference sections (1 for a reconstructed file).
info.revision = cache.revision(bytes, covered).map_or(1, |d| d.revisions().len().max(1));
// What can't be checked is unknown, not invalid: only data that is wrong or fails its check
// is "invalid".
let unsupported = |info: &mut SignatureInfo, why: &str| {
info.status = Status::Unknown;
info.details.push(format!("PdfCraft can't check this signature yet: {why}."));
};
if info.sub_filter.as_deref() == Some("adbe.x509.rsa_sha1") {
info.details.push("This signature uses the legacy adbe.x509.rsa_sha1 format, which PdfCraft does not validate yet.".into());
return;
return validate_x509_rsa_sha1(doc, bytes, trust, v, info, cache, (l0, o1, covered), &contents);
}
let sd = match SignedData::parse(&contents) {
Ok(sd) => sd,
Err(SignError::Unsupported(e)) => return unsupported(info, &e),
Err(e) => return invalid(info, &format!("The signature could not be read ({e}).")),
};
let s = &sd.signer;
@ -587,12 +593,42 @@ fn validate_into(doc: &Document, bytes: &[u8], trust: &TrustStore, v: &Dict, inf
};
info.algorithm = Some(format!("{} with {}", cert.public_key.describe(), s.scheme_digest.unwrap_or(s.digest).name()));
info.signer = Some(cert.display_name());
if !sd.verify_signature(&cert, &content_digest) {
info.certificate = Some(cert);
return invalid(info, "The signature value does not match the signer's certificate: the signature is corrupt.");
let mut notes = Vec::new();
match sd.verify_signature_noting(&cert, &content_digest, &mut notes) {
Ok(true) => info.details.extend(notes.into_iter().map(String::from)),
Ok(false) => {
info.certificate = Some(cert);
return invalid(info, "The signature value does not match the signer's certificate: the signature is corrupt.");
}
Err(SignError::Unsupported(e)) => {
info.certificate = Some(cert);
return unsupported(info, &e);
}
Err(e) => {
info.certificate = Some(cert);
return invalid(info, &format!("The signature could not be checked ({e})."));
}
}
info.signing_time = s.signing_time.or_else(|| info.date.as_deref().and_then(Time::from_pdf));
let mut pool = sd.certificates.clone();
finish_validation(doc, bytes, trust, info, cache, covered, cert, &sd.certificates, s.signing_time, unverified_time);
}
/// The verdict once the signature value has been checked: chain and trust, the signing time
/// against the certificate's validity, and what later revisions changed.
#[allow(clippy::too_many_arguments)]
fn finish_validation(
doc: &Document,
bytes: &[u8],
trust: &TrustStore,
info: &mut SignatureInfo,
cache: &DigestCache,
covered: usize,
cert: Certificate,
embedded: &[Certificate],
cms_time: Option<Time>,
unverified_time: Option<Time>,
) {
info.signing_time = cms_time.or_else(|| info.date.as_deref().and_then(Time::from_pdf));
let mut pool = embedded.to_vec();
pool.extend(trust.certs.iter().cloned());
// The time the chain is judged at: trusted timestamp, else the signer's claimed time.
let at = info.timestamp_time.or(info.signing_time);
@ -850,6 +886,66 @@ pub(crate) fn signature_contents(doc: &Document) -> Vec<Vec<u8>> {
out
}
/// `adbe.x509.rsa_sha1` (ISO 32000-2 §12.8.3.2, deprecated): `/Contents` is the PKCS #1
/// signature as a DER OCTET STRING over the SHA-1 digest of the byte ranges, and `/Cert` holds the
/// signer's certificate (then any others) as DER byte strings.
#[allow(clippy::too_many_arguments)]
fn validate_x509_rsa_sha1(
doc: &Document,
bytes: &[u8],
trust: &TrustStore,
v: &Dict,
info: &mut SignatureInfo,
cache: &DigestCache,
(l0, o1, covered): (usize, usize, usize),
contents: &[u8],
) {
let fail = |info: &mut SignatureInfo, why: &str| {
info.status = Status::Invalid;
info.details.push(why.to_string());
};
let certs: Vec<Certificate> = match v.get(b"Cert").map(|c| doc.resolve(c)).as_deref() {
Some(Object::Array(a)) => a.iter().filter_map(|c| doc.resolve(c).as_string().map(|s| s.bytes.clone())).collect::<Vec<_>>(),
Some(Object::String(s)) => vec![s.bytes.clone()],
_ => Vec::new(),
}
.iter()
.filter_map(|raw| Certificate::parse(raw).ok())
.collect();
let Some(cert) = certs.first().cloned() else { return fail(info, "The signer's certificate is not in the signature.") };
let signature = match crate::der::Tlv::parse_ber(contents).and_then(|(t, _)| t.octets("signature").map(|o| o.into_owned())) {
Ok(sig) => sig,
Err(e) => return fail(info, &format!("The signature could not be read ({e}).")),
};
info.digest = Some(DigestAlg::Sha1);
info.algorithm = Some(format!("{} with SHA-1", cert.public_key.describe()));
info.signer = Some(cert.display_name());
let digest = cache.digest(DigestAlg::Sha1, bytes, l0, o1, covered);
let mut notes = Vec::new();
match cert.public_key.verify_noting(crate::keys::Scheme::RsaPkcs1, DigestAlg::Sha1, &digest, &signature, &mut notes) {
Ok(true) => info.details.extend(notes.into_iter().map(String::from)),
Ok(false) => {
info.certificate = Some(cert);
return fail(info, "The document has been altered or corrupted since the signature was applied.");
}
// Only what PdfCraft doesn't support yet is "can't check"; a key or value that is
// malformed is a signature that doesn't verify (as on the CMS path).
Err(SignError::Unsupported(e)) => {
info.certificate = Some(cert);
info.status = Status::Unknown;
info.details.push(format!("PdfCraft can't check this signature yet: {e}."));
return;
}
Err(e) => {
info.certificate = Some(cert);
return fail(info, &format!("The signature could not be verified ({e})."));
}
}
info.details.push("The signature uses the legacy adbe.x509.rsa_sha1 format.".into());
info.details.extend(DigestAlg::Sha1.weakness().map(String::from));
finish_validation(doc, bytes, trust, info, cache, covered, cert, &certs, None, None);
}
/// What later revisions changed, classified as Acrobat reports it, under DocMDP `p` (or none:
/// an approval signature permits form fill, comments and further signatures).
fn classify_changes(doc: &Document, old: Option<Document>, p: Option<u8>) -> Modification {

View File

@ -71,11 +71,14 @@ fn bmp(password: &str) -> Vec<u8> {
/// 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) -> 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 Vec::new(),
};
let _ = u;
let fill = |s: &[u8]| -> Vec<u8> {
@ -157,21 +160,25 @@ fn hmac(alg: DigestAlg, key: &[u8], data: &[u8]) -> Result<Vec<u8>, SignError> {
}
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(SignError::Unsupported(format!("{} in a PKCS #12 MAC", other.name()))),
}
}
fn pbkdf2(prf: DigestAlg, password: &[u8], salt: &[u8], rounds: u32, len: usize) -> Vec<u8> {
fn pbkdf2(prf: DigestAlg, password: &[u8], salt: &[u8], rounds: u32, len: usize) -> Result<Vec<u8>, SignError> {
let mut out = 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(SignError::Unsupported(format!("PBKDF2 with {}", other.name()))),
}
out
Ok(out)
}
/// Decrypt `ct` with the password-based scheme `alg` (an AlgorithmIdentifier).
@ -215,7 +222,7 @@ fn pbe_decrypt(alg: &Tlv<'_>, password: &str, ct: &[u8]) -> Result<Vec<u8>, Sign
other => return Err(SignError::Unsupported(format!("PBES2 cipher {}", other.unwrap_or("?")))),
};
let iv = sc.get(1).ok_or_else(|| bad("IV"))?.octets("IV")?;
let key = pbkdf2(prf, password.as_bytes(), &salt, 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 => {
@ -355,7 +362,9 @@ pub fn open(bytes: &[u8], password: &str) -> Result<DigitalId, SignError> {
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).ok_or_else(|| SignError::Unsupported(format!("MAC digest {alg_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(|| SignError::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) as u32;
@ -435,7 +444,7 @@ pub fn write(id: &DigitalId, password: &str) -> Result<Vec<u8>, SignError> {
const ITER: u32 = 2048;
let encrypt = |plain: &[u8]| -> Result<Vec<u8>, SignError> {
let (salt, iv) = (random(16)?, random(16)?);
let key = pbkdf2(DigestAlg::Sha256, password.as_bytes(), &salt, ITER, 32);
let key = pbkdf2(DigestAlg::Sha256, password.as_bytes(), &salt, ITER, 32)?;
let ct = cbc::Encryptor::<aes::Aes256>::new_from_slices(&key, &iv)
.map_err(|_| SignError::Crypto("AES".into()))?
.encrypt_padded_vec::<Pkcs7>(plain);

139
crates/sign/src/rsa_pad.rs Normal file
View File

@ -0,0 +1,139 @@
//! 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::SignError;
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>>, SignError> {
let k = n.len();
let key = RsaPublicKey::new(BoxedUint::from_be_slice_vartime(n), BoxedUint::from_be_slice_vartime(e))
.map_err(|e| SignError::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))
}
/// `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, SignError> {
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, SignError> {
let Some(raw) = encoded_message(n, e, sig, notes)? else { return Ok(false) };
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)
}

View File

@ -147,7 +147,8 @@ pub fn parse_token(raw: &[u8]) -> Result<TimestampToken, SignError> {
return Err(bad("token signed message digest does not match TSTInfo"));
}
let cert = token.signer_certificate().ok_or_else(|| bad("TSA certificate is missing"))?;
if !token.verify_signature(cert, &content_digest) {
// 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 })

View File

@ -321,8 +321,11 @@ impl Certificate {
/// Whether `key` verifies this certificate's signature.
pub fn signed_by(&self, key: &PublicKey) -> bool {
let Ok(alg) = Tlv::parse_all(&self.sig_alg) else { return false };
let Ok((scheme, Some(digest))) = keys::signature_algorithm(&alg) else { return false };
key.verify(scheme, digest, &digest.digest(&[&self.tbs]), &self.signature).unwrap_or(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(digest))) => key.verify(scheme, digest, &digest.digest(&[&self.tbs]), &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

View File

@ -0,0 +1,140 @@
//! Signatures by OpenSSL 3.6 with the algorithms beyond SHA-2/RSA/NIST-curve ECDSA: SHA-3,
//! SHA-512/256, RIPEMD-160, RSA-PSS with other MGF1 hashes and salt lengths, Ed25519 and
//! brainpoolP512r1. Every vector signs the message "hello" with a throw-away key (see
//! tests/data/README.md for the commands). The CMS files are detached, `openssl cms -sign -binary`.
use pdfcraft_sign::cms::SignedData;
use pdfcraft_sign::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 = pdfcraft_sign::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(pdfcraft_sign::SignError::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 = pdfcraft_sign::PublicKey::from_spki(&pdfcraft_sign::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 pdfcraft_sign::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(pdfcraft_sign::SignError::Malformed(_))), "{what}: {r:?}");
}
}

View File

@ -153,12 +153,12 @@ fn signatures_round_trip_for_every_key_type() {
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), "{file}");
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), "{file}");
assert!(!bad.verify_signature(signer, &digest).unwrap(), "{file}");
}
}
@ -181,7 +181,7 @@ fn new_digital_ids_are_self_signed_and_survive_a_p12_round_trip() {
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));
assert!(sd.verify_signature(&back.certificate, &d).unwrap());
}
}
@ -341,3 +341,259 @@ fn ocsp_responses_verify_and_match_the_certificate() {
let error = der::seq(&[&der::int(6)]);
assert!(OcspResponse::parse(&error).is_err());
}
// ── 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 pdfcraft_sign::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}");
}
// 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 pdfcraft_sign::der::{self, Tlv};
use pdfcraft_sign::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 pdfcraft_sign::der::{self, Tlv};
use pdfcraft_sign::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 pdfcraft_sign::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 an_rsa_signature_value_not_below_the_modulus_is_refused() {
use pdfcraft_sign::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 pdfcraft_sign::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 pdfcraft_sign::der::{self, Tlv};
use pdfcraft_sign::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(SignError::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(SignError::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 pdfcraft_sign::der::Tlv;
use pdfcraft_sign::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");
}
}

View File

@ -35,3 +35,38 @@ openssl pkcs12 -export -inkey leaf.key -in leaf.crt -certfile ca.crt -out chain.
`openssl cms -sign -binary -md sha256 -outform DER -signer rsa.crt -inkey rsa.key` over its
byte ranges (`adbe.pkcs7.detached`, with OpenSSL's signing-time attribute). It checks the
validator against a signature PdfCraft did not make; poppler's `pdfsig` reports it valid.
### `x509-rsa-sha1.pdf`
A synthetic one-page PDF signed the legacy way (`/SubFilter /adbe.x509.rsa_sha1`, `/Cert` = `rsa.crt.pem`,
`/Contents` = a DER OCTET STRING with the PKCS #1 signature of the SHA-1 digest of the byte ranges), with
the key of `rsa-aes.p12`. Built by writing the objects with a fixed-width `/ByteRange` and zero-filled
`/Contents`, patching the byte range, then `openssl dgst -sha1 -sign rsa.key` over the two ranges.
### RSA signature variants (hex constants in `tests/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/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
```

View File

@ -0,0 +1,29 @@
%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R] /SigFlags 3 >> >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [4 0 R] >>
endobj
4 0 obj
<< /Type /Annot /Subtype /Widget /FT /Sig /T (Legacy) /Rect [0 0 0 0] /P 3 0 R /F 132 /V 5 0 R >>
endobj
5 0 obj
<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.x509.rsa_sha1 /ByteRange [0 0000002212 0000003414 0000000218] /Cert <308203533082023ba003020102020203e9300d06092a864886f70d01010b050030423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b3009060355040613025553301e170d3236313030323036313733345a170d3336303932393036313733345a30423118301606035504030c0f54657374205369676e65722052534131193017060355040a0c105072696e744372616674205465737473310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100cbe14960a30ad858bd7301dacbd61437f2815dfdf9506277000efe402b8e617a5421bfdcf019321bb80b8624fb72773794bca818dd176558b24525e6ae768375889ea2951da2d202c11181764210aa79b80790d139ba9d7008a995dca64c4d6259d1e2a52fe69e91238e566028c171d7e625a8f1fbf611d20dd9562aac97b4501bb81e4445c9bc5eb10adcc7b77a090af39724b45999ec2c6009011c8e1a87f3b48eb3165cbb27075ea81b59d0f233fd778bc0823ac1fd927762303b62ac2795f71daacdf3433d07101291a4839143282ec07567a7caacfc06617e4150a54c5e3e5be9be1cab909dd1aee44fc60cb82409acb2224a8ad88bba6f2c095f882da10203010001a3533051301d0603551d0e0416041459098dd173d7761ef139cc9504b7dca2d7d6f99f301f0603551d2304183016801459098dd173d7761ef139cc9504b7dca2d7d6f99f300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101009b1ff9466acd740894f2217bb203e9b5da792a229555bddf16b3b330fb06b7277b0670150abcc75e325ef9dd8725ca8c3ac3714ef1aee8d916b5a86d18cd4e2697b58a3a508c1c58259fd21dc23f31b2a6cd229919ea034296e00163f35cc5912fe97c5fc42d1bd5c29b6c85f26e7620bb5c62a206783b2f44d32493723486168d7b4c1914c2c043cf67ab4de33386453450a071b7a10659d47ab8524ec3a48a7f51e3886c56d632437e2ea30b70a60a19d5eb13ae856bbaeb54038580512d5dabd840e63622b0683ab410b4b591220c130313a2b1c7191d17d5dfacc164b0a360cf001917875589e78bac0a9812a836276c97f9725ce1cfb6538613cefe4242> /Contents <04820100960d572d3b941e684ec8bcfe65cd26fc7feed8f01256e1334477c218de9d7cf534edb60e66aa8a2d8569f6c5ddd84e1ab92df5080b74aa6679fa65228f268e8d8004a44aa674a086820882f382124a57d51cad65bc2c1a5f7d2655c2013c7f1a210aac4960961dd11c30aa14ee52c665bc5ea1a8d19e898c305f10c6bd1a5bc53b77f628a01bcbaa94783f3927eb5fc4a2aa0d5c57c1ec4de6fa0d0a511d3d50360cf3553fcd7f25f0131e556bf4a035be248459788f044b8f9825c9c1195ee1eb42d4b7ee9e973a917bfa2e7f105818f8587712a9c2eafc4aad6f54237ee6ed1dc758e215a158758c3cb77178cd848d2dca3705708bbe6a824a268177a8178f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000> /M (D:20261002120000Z) >>
endobj
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000102 00000 n
0000000159 00000 n
0000000246 00000 n
0000000359 00000 n
trailer
<< /Size 6 /Root 1 0 R >>
startxref
3448
%%EOF

View File

@ -1259,3 +1259,43 @@ fn a_standalone_timestamp_typed_sig_by_an_older_writer_is_still_a_timestamp() {
let s = signatures(&open(&old), &old, &TrustStore::default()).into_iter().find(|s| s.doc_timestamp).expect("found as a timestamp");
assert_eq!(s.status, Status::Invalid);
}
/// A legacy adbe.x509.rsa_sha1 signature whose certificate carries an RSA key the verifier
/// rejects (public exponent 1) is Invalid, not "can't check yet".
#[test]
fn a_legacy_x509_signature_with_a_malformed_rsa_key_is_invalid() {
let pdf = data("x509-rsa-sha1.pdf");
let at = pdf.windows(10).position(|w| w == b"0203010001").expect("the certificate's exponent 65537");
let mut bad = pdf.clone();
bad[at..at + 10].copy_from_slice(b"0203000001");
let s = signatures(&open(&bad), &bad, &TrustStore::default()).into_iter().find(|s| s.signed).unwrap();
assert_eq!(s.sub_filter.as_deref(), Some("adbe.x509.rsa_sha1"));
assert_eq!(s.status, Status::Invalid, "{:?}", s.details);
assert!(!s.details.iter().any(|d| d.contains("can't check this signature yet")), "{:?}", s.details);
}
#[test]
fn validates_the_legacy_adbe_x509_rsa_sha1_format() {
let id = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
// tests/data/x509-rsa-sha1.pdf: made with OpenSSL (README.md) as Acrobat 4-era signers wrote it.
let pdf = data("x509-rsa-sha1.pdf");
let s = signatures(&open(&pdf), &pdf, &TrustStore::default()).into_iter().find(|s| s.signed).unwrap();
assert_eq!(s.sub_filter.as_deref(), Some("adbe.x509.rsa_sha1"));
assert_eq!(s.status, Status::Unknown, "intact, identity not trusted: {:?}", s.details);
assert_eq!(s.signer.as_deref(), Some("Test Signer RSA"));
assert_eq!(s.algorithm.as_deref(), Some("RSA 2048-bit with SHA-1"));
assert_eq!(s.modification, Modification::None);
// SHA-1 still validates, but the panel says it is weak.
let weak = "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.";
assert_eq!(s.details.iter().filter(|d| *d == weak).count(), 1, "{:?}", s.details);
assert!(s.details.iter().any(|d| d.contains("legacy adbe.x509.rsa_sha1 format")), "{:?}", s.details);
let trusted = TrustStore { certs: vec![id.certificate.clone()] };
let s = signatures(&open(&pdf), &pdf, &trusted).into_iter().find(|s| s.signed).unwrap();
assert_eq!(s.status, Status::Valid, "{:?}", s.details);
// One changed byte inside the signed range.
let mut bad = pdf.clone();
let i = bad.windows(8).position(|w| w == b"(Legacy)").unwrap() + 1;
bad[i] = b'l';
let s = signatures(&open(&bad), &bad, &trusted).into_iter().find(|s| s.signed).unwrap();
assert_eq!(s.status, Status::Invalid, "{:?}", s.details);
}

View File

@ -5966,8 +5966,8 @@ milestone = "M9"
status = "shipped"
commands = ["sign.digital"]
tools = ["sign_document"]
evidence = ["crates/sign/tests/crypto.rs::signatures_round_trip_for_every_key_type"]
notes = "SHA-256, SHA-384 for P-384; SHA-1 refused for new signatures, validated in old ones"
evidence = ["crates/sign/tests/crypto.rs::signatures_round_trip_for_every_key_type", "crates/sign/tests/crypto.rs::rsa_signatures_are_read_with_every_digestinfo_variant", "crates/sign/tests/crypto.rs::ecdsa_signatures_are_read_as_der_non_minimal_der_or_raw", "crates/sign/tests/crypto.rs::verifies_ecdsa_on_p521_and_the_brainpool_curves", "crates/sign/tests/crypto.rs::keys_of_unknown_algorithms_are_unsupported_not_wrong", "crates/sign/tests/crypto.rs::tolerated_encodings_are_noted_and_standard_ones_are_not", "crates/sign/tests/crypto.rs::weak_digests_say_so_in_words_that_are_pinned", "crates/sign/tests/crypto.rs::an_rsa_signature_value_not_below_the_modulus_is_refused", "crates/sign/tests/crypto.rs::hostile_rsa_signature_values_are_wrong_not_crashes", "crates/sign/tests/algorithms.rs::rsa_with_sha3_sha512_256_and_ripemd160", "crates/sign/tests/algorithms.rs::rsa_pss_with_its_declared_parameters", "crates/sign/tests/algorithms.rs::ed25519_signs_the_signed_attributes_themselves", "crates/sign/tests/algorithms.rs::ecdsa_on_brainpool_p512r1", "crates/sign/tests/algorithms.rs::brainpool_p512r1_keys_must_have_coordinates_reduced_modulo_p", "crates/sign/tests/algorithms.rs::only_weak_digests_leave_a_note_when_a_signature_validates", "crates/sign/tests/pdf.rs::validates_the_legacy_adbe_x509_rsa_sha1_format"]
notes = "SHA-256, SHA-384 for P-384; SHA-1 refused for new signatures, validated in old ones. Validation also reads SHA-224, SHA-512/224, SHA-512/256, SHA3-224/256/384/512, RIPEMD-160, RSA PKCS#1 with or without the DigestInfo NULL (a bare digest without any DigestInfo is refused), RSA-PSS with its declared MGF1 hash and salt length, ECDSA as DER/non-minimal DER/raw r||s on P-256, P-384, P-521 and brainpoolP256r1/P384r1/P512r1 (the last hand-written on crypto-bigint, checked against OpenSSL), Ed25519 (RFC 8419), and the legacy adbe.x509.rsa_sha1 format. An RSA value not below the modulus is refused (RFC 8017 5.2.2). SHA-1 and RIPEMD-160 validate but the signature details say the algorithm is weak (wording pinned by tests); tolerated encodings are listed in the details. An algorithm PdfCraft cannot check gives Unknown, not Invalid. Still missing: Ed448/SHAKE, DSA, brainpool curves other than P256/P384/P512, signing with the new digests"
[[feature]]
id = "sign.certify"