.NET can’t sign a PDF with Ed25519: it has no Ed25519, and OnePDF’s own signer takes RSA and ECDSA keys. RecursiveAI.OneCrypto.Pdf signs with any OneCrypto key, including Ed25519, Ed448 and, as a preview, the post-quantum ML-DSA. The program below signs the IRS Form W-9 once with an Ed25519 key and once with an ML-DSA-65 key, both made by OpenSSL, and verifies each.
dotnet new console -o PdfEd25519 && cd PdfEd25519 dotnet add package RecursiveAI.OneCrypto.Pdf --version 0.2.0-preview.1 curl -LO https://www.irs.gov/pub/irs-pdf/fw9.pdf for alg in ed25519 ML-DSA-65; do openssl req -x509 -newkey $alg -nodes -days 365 -subj "/CN=Ada Lovelace" \ -addext "basicConstraints=critical,CA:FALSE" \ -addext "keyUsage=critical,digitalSignature,nonRepudiation" \ -keyout $alg-key.pem -out $alg.pem done
OneCrypto is a preview, so dotnet add package needs --version or --prerelease. ML-DSA keys need OpenSSL 3.5 or later, for the openssl command and for .NET on Linux. Put this in Program.cs and run dotnet run:
using OnePdf; using RecursiveAI.OneCrypto; using RecursiveAI.OneCrypto.Pdf; using RecursiveAI.OneCrypto.Validation; foreach (string alg in new[] { "ed25519", "ML-DSA-65" }) { using LoadedItems certFile = Crypto.Load($"{alg}.pem"); using LoadedItems keyFile = Crypto.Load($"{alg}-key.pem"); Certificate cert = certFile.Certificates[0]; using (PdfDocument doc = PdfDocument.Open("fw9.pdf", new PdfOpenOptions { AllowEditing = true })) using (FileStream output = File.Create($"fw9-{alg}.pdf")) doc.SignPades(output, keyFile.PrivateKeys[0].Signing!, cert); // Verify: the test certificate is its own trust anchor using PdfDocument signed = PdfDocument.Open($"fw9-{alg}.pdf"); PadesReport report = signed.VerifyPades(new PadesVerifyOptions { TrustAnchors = [TrustAnchor.FromCertificate(cert)], ReferenceTime = DateTimeOffset.UtcNow, }); PadesSignatureReport s = report.Signatures[0]; Console.WriteLine($"{alg}: valid={s.IsValid}, level={s.Level}, " + $"OnePDF's own check: {s.OnePdf.SignatureCheck}"); // For openssl cms -verify: the CMS signature and the bytes it covers PdfSignature sig = signed.GetSignatures().Single(x => x.FieldName == s.FieldName); File.WriteAllBytes($"{alg}.p7s", SignedData.Parse(sig.Contents, null, true) .Encoded.ToArray()); using Stream signedBytes = sig.OpenSignedData()!; using FileStream copy = File.Create($"{alg}.bin"); signedBytes.CopyTo(copy); }
ed25519: valid=True, level=Basic, OnePDF's own check: NotChecked ML-DSA-65: valid=True, level=Basic, OnePDF's own check: NotChecked
OnePDF reports EdDSA and ML-DSA signatures as NotChecked, so for these keys the verdict is the bridge’s: it checks the CMS signature, the byte ranges and the certificate itself, and its onepdf check is a warning that says so. The signature uses SHA-512 for Ed25519 and ML-DSA, SHAKE256 for Ed448, and the sub-filter ETSI.CAdES.detached.
Check it with OpenSSL and pyHanko
Both verify the Ed25519 and the ML-DSA signature. The CMS here has signed attributes, which OpenSSL needs for EdDSA:
for alg in ed25519 ML-DSA-65; do openssl cms -verify -binary -inform DER -in $alg.p7s -content $alg.bin \ -CAfile $alg.pem -purpose any -out /dev/null 2>&1 done python3 -m venv venv && venv/bin/pip install -q pyhanko pyhanko-cli for alg in ed25519 ML-DSA-65; do venv/bin/pyhanko sign validate --trust-replace --trust $alg.pem \ --executive-summary fw9-$alg.pdf 2>/dev/null done
CMS Verification successful CMS Verification successful Signature1:32484f370d674cacf027977b5dba0d116e22ef61f37f184febd0d4aeb7516661:VALID Signature1:59b94b334dee1df4f1269ac698d7a3ef02f069d4a51698b90f3294132d18d411:VALID
Poppler’s pdfsig 26.01 reports both files as “Signature is Invalid”, although OpenSSL and pyHanko verify the same signatures. It says the same of an Ed25519 signature made by pyHanko, so read that line as pdfsig not verifying EdDSA or ML-DSA signatures, not as a fault in the file.
Which readers accept them
- Ed25519 and Ed448 are part of PDF 2.0 through ISO/TS 32002. A reader that implements it can verify them; older readers report the signature as unverifiable. The project records Acrobat’s behavior in its own sample run; its conformance table lists “Adobe Acrobat Reader: pending”, so there is no claim yet that Acrobat accepts them.
- ML-DSA in PDF isn’t standardized yet. It is a preview in OneCrypto, and the project says to expect no PDF reader to verify it yet.
- RSA and ECDSA are what every reader checks. Use them when the file goes to people whose software you don’t know.
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.
- The bridge alone. In the project’s conformance table, EdDSA and ML-DSA signatures “are judged by the bridge alone (OnePDF does not check them); ML-DSA is a preview”.
- Platforms. Ed25519 and Ed448 are OneCrypto’s own code and work everywhere .NET 10 runs. ML-DSA comes from the platform: Linux with OpenSSL 3.5 and recent Windows builds have it, macOS and the browser don’t, and there OneCrypto raises
AlgorithmNotSupportedException(Platform availability). - Trust. A self-signed test certificate is its own trust anchor here. A valid report says the signature and certificate check out against the anchors you gave, nothing more.
How signing and verification work: PDF signatures. Ed25519 outside PDF: Sign and verify with Ed25519.