doc.VerifyPades from RecursiveAI.OneCrypto.Pdf verifies every signature in a PDF: it keeps OnePDF’s own verdict and adds OneCrypto’s CMS verifier, time-stamp verifier and certificate path validator. It covers what OnePDF on its own reports as NotChecked: EdDSA and ML-DSA signatures, and revocation from CRLs and OCSP responses. The program below makes an Ed25519-signed copy of the IRS Form W-9, then verifies it three ways.
dotnet new console -o VerifyPdf && cd VerifyPdf dotnet add package RecursiveAI.OneCrypto.Pdf --version 0.2.0-preview.1 curl -LO https://www.irs.gov/pub/irs-pdf/fw9.pdf openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 3650 \ -subj "/CN=Test Root CA" -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" -keyout ca-key.pem -out ca.pem openssl req -x509 -CA ca.pem -CAkey ca-key.pem -newkey ed25519 -nodes -days 365 \ -subj "/CN=Ada Lovelace" -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature,nonRepudiation" \ -keyout signer-key.pem -out signer.pem
OneCrypto is a preview, so dotnet add package needs --version or --prerelease. The OpenSSL lines make a test root CA and an Ed25519 signing certificate. Put this in Program.cs and run dotnet run:
using System.Security.Cryptography.X509Certificates; using OnePdf; using RecursiveAI.OneCrypto; using RecursiveAI.OneCrypto.Issuance; using RecursiveAI.OneCrypto.Keys; using RecursiveAI.OneCrypto.Pdf; using RecursiveAI.OneCrypto.Validation; DateTimeOffset now = DateTimeOffset.UtcNow; using LoadedItems rootFile = Crypto.Load("ca.pem"); Certificate root = rootFile.Certificates[0]; // A signed file to check. Skip this block and open your own signed PDF instead. using (LoadedItems signerFile = Crypto.Load("signer.pem")) using (LoadedItems keyFile = Crypto.Load("signer-key.pem")) using (PdfDocument doc = PdfDocument.Open("fw9.pdf", new PdfOpenOptions { AllowEditing = true })) using (FileStream output = File.Create("fw9-signed.pdf")) doc.SignPades(output, keyFile.PrivateKeys[0].Signing!, signerFile.Certificates[0], [root]); using PdfDocument signed = PdfDocument.Open("fw9-signed.pdf"); // 1. OnePDF on its own using X509Certificate2 anchor = root.ToX509Certificate2(); var onePdf = signed.VerifySignatures(new PdfSignatureVerifyOptions { TrustAnchors = [anchor], }); foreach (PdfSignatureVerification v in onePdf.Signatures) Console.WriteLine($"OnePDF only: valid={v.IsValid}, " + $"signature={v.SignatureCheck}, revocation={v.Revocation}"); // 2. OnePDF plus OneCrypto, with the same trust anchor Print("OneCrypto", new PadesVerifyOptions { TrustAnchors = [TrustAnchor.FromCertificate(root)], ReferenceTime = now, }); // 3. Unknown revocation made fatal, then revocation data supplied: your CA's // CRL. Here the test CA issues one; Crypto.Load reads a downloaded .crl file. Print("Revocation required", new PadesVerifyOptions { TrustAnchors = [TrustAnchor.FromCertificate(root)], ReferenceTime = now, RevocationRequired = RevocationScope.EndEntity, }); Print("With the CA's CRL", new PadesVerifyOptions { TrustAnchors = [TrustAnchor.FromCertificate(root)], ReferenceTime = now, RevocationRequired = RevocationScope.EndEntity, Revocation = new RevocationData { Crls = [TestCrl()] }, }); void Print(string label, PadesVerifyOptions options) { foreach (PadesSignatureReport s in signed.VerifyPades(options).Signatures) { Console.WriteLine($"{label}: valid={s.IsValid}, level={s.Level}, " + $"chain={s.Chain?.Verdict}, revocation={s.Chain?.RevocationSummary}"); foreach (CmsCheck c in s.Checks.Where(c => c.Status != CmsCheckStatus.Passed)) Console.WriteLine($" {c.Rule}: {c.Status}"); } } CertificateRevocationList TestCrl() { using LoadedItems caKeyFile = Crypto.Load("ca-key.pem"); ISigningKey caKey = caKeyFile.PrivateKeys[0].Signing!; var ca = new CertificateAuthority(root, X509Signer.FromKey(caKey, caKey.DefaultSpec), new()); return ca.IssueCrl(new CrlIssueOptions { ThisUpdate = now, NextUpdate = now.AddDays(7), Number = 1, }); }
OnePDF only: valid=False, signature=NotChecked, revocation=NotChecked OneCrypto: valid=True, level=Basic, chain=Valid, revocation=Unknown proof-of-existence: Warning chain: Warning onepdf: Warning Revocation required: valid=False, level=None, chain=Indeterminate, revocation=Unknown proof-of-existence: Warning chain: Unsupported onepdf: Warning With the CA's CRL: valid=True, level=Basic, chain=Valid, revocation=Good proof-of-existence: Warning onepdf: Warning
What each run says (c.Reason holds the reason behind each status):
- OnePDF only checks the byte ranges, the document’s changes and the certificate chain, but not an Ed25519 signature value, so its verdict is not valid.
- OneCrypto verifies the signature itself and keeps OnePDF’s other checks; the
onepdfwarning says that, for this algorithm, the verdict is OneCrypto’s. With no time-stamp it judges the signature atReferenceTime(theproof-of-existencewarning), and with no revocation data thechaincheck is a warning: revocation is unknown, never assumed good. RevocationRequiredturns unknown revocation into an undecided verdict: thechaincheck reports that no revocation data was supplied, and the chain isIndeterminate. Given the CA’s CRL, the signer’s status is good and the signature is valid again.
For RSA and ECDSA signatures, which OnePDF does check, IsValid is true only when OnePDF’s verdict and every OneCrypto check agree: the bridge adds rules and never removes one. In the project’s test corpus of PDF signature attacks, 117 comparisons, it never disagreed with OnePDF’s verdict.
What else the report holds
s.Level: the PAdES level the file proves:Basic,Timestamped,LongTermorLongTermArchive(Sign a PDF with long-term validation).s.ProofOfExistenceands.ValidationTime: the time a trusted time-stamp proves, and the time the signature was judged at. The signing time written in the PDF is a claim and is never used.s.Timestamps,s.Cms,s.Chainands.OnePdf: the time-stamp reports, the CMS report, the path validator’s report with a result per rule, and OnePDF’s own verification, untouched.- Fetching. To fetch missing certificates and the OCSP responses or CRLs named in them, set
CertificateSourceandRevocationSourceto the sources inRecursiveAI.OneCrypto.Online, and callVerifyPadesAsync.
Check it with pyHanko
pyHanko verifies the same Ed25519 signature against the test root:
python3 -m venv venv && venv/bin/pip install -q pyhanko pyhanko-cli venv/bin/pyhanko sign validate --trust-replace --trust ca.pem \ --executive-summary fw9-signed.pdf 2>/dev/null
Signature1:e4539dcbc970eefeacc9d1e580cdb7b4110f9314d8b173dae912ebc3f7a421a0:VALIDPoppler’s pdfsig 26.01 reports this file as “Signature is Invalid”, although pyHanko and openssl cms verify the signature. It says the same of an Ed25519 signature made by pyHanko, so read that line as pdfsig not verifying EdDSA, not as a fault in the file.
Limits
- Not audited. OneCrypto (cryptography, not cryptocurrency) has not been independently audited. It is a preview: 0.2.0-preview.1, and the API can change.
- What “valid” means. The report says what was checked, against the trust anchors you gave, at the time it states. It doesn’t say a signature is fit for any legal use. The chain check comes from the RFC 5280 path validator, which the project calls “still a preview, so not yet recommended for trust decisions” (Path validation).
- The bridge alone for EdDSA and ML-DSA. OnePDF doesn’t check them, so for these the verdict is OneCrypto’s alone. ML-DSA is a preview.
- Nothing is fetched unless you ask. Without revocation data or a source, revocation is unknown. A source fetches only after a certificate path has been chosen.
- The file as opened. Verification reads the file’s original bytes. If it changes on disk after you open it,
VerifyPadesthrowsPadesExceptionwithPadesError.SourceChanged: open it again.
Every rule and the time model: PDF signatures. To sign: Ed25519 PDF signature in C#.