---
title: "Verify a PDF signature in C#"
url: https://recursiveai.org/onecrypto/cookbook/verify-pdf-signature/
description: "Verify a PDF signature in C#, including what OnePDF reports as NotChecked: Ed25519, Ed448 and ML-DSA signatures, and revocation through CRLs and OCSP. A per-rule report and the PAdES level. Runnable code with OneCrypto.Pdf 0.2.0-preview.1."
version: "0.2.0-preview.1"
---

# Verify a PDF signature in C#

`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.

```shell
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`:

```csharp
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,
    });
}
```

```text
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):

1. **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.
2. **OneCrypto** verifies the signature itself and keeps OnePDF’s other checks; the `onepdf` warning says that, for this algorithm, the verdict is OneCrypto’s. With no time-stamp it judges the signature at `ReferenceTime` (the `proof-of-existence` warning), and with no revocation data the `chain` check is a warning: revocation is unknown, never assumed good.
3. **`RevocationRequired`** turns unknown revocation into an undecided verdict: the `chain` check reports that no revocation data was supplied, and the chain is `Indeterminate`. 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`, `LongTerm` or `LongTermArchive` ([Sign a PDF with long-term validation](https://recursiveai.org/onecrypto/cookbook/pades-ltv-pdf-signature/)).
- **`s.ProofOfExistence`** and **`s.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.Chain`** and **`s.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 `CertificateSource` and `RevocationSource` to the sources in `RecursiveAI.OneCrypto.Online`, and call `VerifyPadesAsync`.

## Check it with pyHanko

pyHanko verifies the same Ed25519 signature against the test root:

```shell
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
```

```text
Signature1:e4539dcbc970eefeacc9d1e580cdb7b4110f9314d8b173dae912ebc3f7a421a0:VALID
```

Poppler’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](https://recursiveai.org/onecrypto/docs/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, `VerifyPades` throws `PadesException` with `PadesError.SourceChanged`: open it again.

Every rule and the time model: [PDF signatures](https://recursiveai.org/onecrypto/docs/pdf/). To sign: [Ed25519 PDF signature in C#](https://recursiveai.org/onecrypto/cookbook/ed25519-pdf-signature/).
