Sign PDFs with CSC v2 — PAdES-B-B through B-LTA.
Integrate CodeB's Remote Signature Service into your product. Every authenticated user gets an EC P-256 signing credential whose Subject DN is enriched from the OIDC profile. Sign a hash via signHash, wrap it into a PAdES envelope client-side (sign.html) or let the server do it via signDoc, add an RFC 3161 timestamp, embed RFC 6960 OCSP revocation data, append a document timestamp — PAdES all the way up to B-LTA. Ready for European Digital Identity Wallet integrations.
ICryptoModule boundary is HSM-ready; Azure Key Vault and PKCS#11 back-ends are stubbed and return HTTP 501 until wired.Try it live → Full API reference
The four PAdES conformance levels
Pick the lowest level that satisfies your evidentiary need. Higher levels add durability (survive certificate expiry) at the cost of larger envelopes and extra network round-trips.
PAdES-B-B
Basic — signed attributes only. Verifiable while the signer's certificate is still valid.
- CMS SignerInfo with signingCertificateV2 (RFC 5035)
- id-aa-CMSAlgorithmProtection (RFC 6211)
PAdES-B-T
Time — adds an RFC 3161 TSA token proving the signature existed at a moment in time.
- B-B + id-aa-signatureTimeStampToken
- ~5 KiB extra per signature
PAdES-B-LT
Long-Term — embeds OCSP responses + cert chain so verification works offline after the cert expires.
- B-T + id-aa-ets-revocationValues + id-aa-ets-certValues
- ~10-15 KiB extra per signature
PAdES-B-LTA
Long-Term with Archive — appends a /DocTimeStamp over the whole B-LT PDF. Repeat before the archive TSA expires to extend validity indefinitely.
- B-LT + incremental
/DocTimeStampsignature - ISO 32000-2 §12.8.5
1 OIDC sign-in — get an access token
CSC v2 rides on top of the tenant's OIDC provider. Use standard OAuth 2.0 Authorization Code + PKCE to obtain a Bearer token. See the OIDC sign-in cookbook for the flow. Once you have the token, everything below is Authorization: Bearer <token>.
// After OIDC round-trip:
const accessToken = tokenResp.access_token;
const authHeaders = {
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json'
};
2 List the user's credentials
Under Phase 1b every user has exactly one per-user EC P-256 credential. Set Csc:PerUserCerts=false if you want all users to share a tenant-wide fallback.
const listResp = await fetch('/csc/v2/credentials/list', {
method: 'POST', headers: authHeaders, body: '{}'
}).then(r => r.json());
const credentialID = listResp.credentialIDs[0]; // e.g. "codeb-user-a1b2c3d4e5f60718"
const info = await fetch('/csc/v2/credentials/info', {
method: 'POST', headers: authHeaders,
body: JSON.stringify({ credentialID })
}).then(r => r.json());
console.log(info.cert.subjectDN); // "CN=Ada Lovelace, GN=Ada, SN=Lovelace, E=..."
console.log(info.cert.certificates[0]); // base64 DER leaf
3 Compute the signed-attributes hash, then request a SAD
The Signature Activation Data (SAD) is a short-lived JWT that binds the signer, the credential and the exact hash list. Compute the SHA-256 of the PDF's CMS signedAttributes DER first (see step 5 for how signedAttrs are assembled), then:
const authorize = await fetch('/csc/v2/credentials/authorize', {
method: 'POST', headers: authHeaders,
body: JSON.stringify({
credentialID, numSignatures: 1,
hash: [ hashB64 ],
hashAlgo: '2.16.840.1.101.3.4.2.1' // SHA-256
})
}).then(r => r.json());
// SCAL1 -> immediate SAD in authorize.SAD
// SCAL2 -> authorize.pending=true; prompt user for PIN + confirm:
// POST /csc/v2/credentials/authorize/confirm { authorization_id, pin }
SAD.hash === request.hash on signHash. Never reuse a SAD for a different hash — that would let a compromised hash swap the signed content.4 Sign the hash
const sig = await fetch('/csc/v2/signatures/signHash', {
method: 'POST', headers: authHeaders,
body: JSON.stringify({
credentialID, SAD: authorize.SAD,
hash: [ hashB64 ], hashAlgo: '2.16.840.1.101.3.4.2.1',
signAlgo: '1.2.840.10045.4.3.2' // ecdsa-with-SHA256
})
}).then(r => r.json());
const rawSig = base64Decode(sig.signatures[0]); // 64-byte r||s
r||s concatenation (64 bytes for P-256). Wrap it in an ASN.1 SEQUENCE { INTEGER r, INTEGER s } before splicing into the CMS SignerInfo.signature OCTET STRING.5 Assemble the PAdES envelope
This is the fiddly part. If you don't want to touch bytes, either use the browser signer (which does all of this client-side using node-forge and PDF.js) or the server-side signDoc. If you want to build it yourself:
5.1 · Reserve the /Contents placeholder in the PDF
Assemble the PDF incremental-update trailer with the signature dictionary. The /Contents<...> hex string is placeholder-padded to your target size (64 KiB is a comfortable ceiling for B-LTA). Record the byte offsets of the < and > brackets — they define the ByteRange hole.
<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached /ByteRange [ 0 <sigDictStart> <sigDictEnd> <afterSig> ] /Contents <0000000000...0000> /* placeholder */ /M (D:YYYYMMDDHHMMSSZ) /Reason (...) /Location (...) /ContactInfo (...) >>
Bracket rule (ISO 32000-1 §12.8.1.1): the < and > brackets sit inside the ByteRange hole, not the signed range.
5.2 · Compute the signedAttributes DER
Five mandatory signed attributes, DER-sorted:
contentType(OID1.2.840.113549.1.9.3) =id-datamessageDigest(OID1.2.840.113549.1.9.4) = SHA-256 of the PDF bytes covered byByteRangesigningTime(OID1.2.840.113549.1.9.5) = current UTCsigningCertificateV2(OID1.2.840.113549.1.9.16.2.47, RFC 5035) — ESSCertIDv2 with issuerSerialid-aa-CMSAlgorithmProtection(OID1.2.840.113549.1.9.52, RFC 6211) — digestAlgorithm + signatureAlgorithm
SHA-256 the DER-encoded SET. That's the hash you send to signHash in step 4.
5.3 · Splice the CMS into the placeholder
Build the CMS SignedData with the signer's certificate included, then hex-encode. Left-align inside the /Contents<...> window; pad the remainder with 0. Fix up the ByteRange integers so the four numbers all fit inside the space you reserved for them.
6 B-T — add an RFC 3161 timestamp
SHA-256 the CMS SignerInfo.signature OCTET STRING contents, then post to the timestamp endpoint:
const tsaHash = sha256(rawSigDerBytes);
const tsResp = await fetch('/csc/v2/signatures/timestamp', {
method: 'POST', headers: authHeaders,
body: JSON.stringify({
hash: base64(tsaHash),
hashAlgo: '2.16.840.1.101.3.4.2.1'
})
}).then(r => r.json());
// tsResp.token is a base64 DER RFC 3161 TimeStampToken.
// Splice it into CMS SignerInfo.unsignedAttrs as
// id-aa-signatureTimeStampToken (OID 1.2.840.113549.1.9.16.2.14)
signatures/timestamp proxies to whatever URL is set in HKLM\SOFTWARE\CodeB\TSAURL (defaulting to Sectigo). signatures/tsa is an in-tenant issuer with an auto-generated responder cert — use it for air-gapped or sovereign deployments (see the TSA server note).7 B-LT — embed OCSP responses + cert chain
Long-term validation means a relying party can verify the signature after the signer's certificate has expired. You bundle the revocation evidence and the cert chain inside the signature so a future verifier does not need to reach out to any online service.
Fetch a BasicOCSPResponse for each cert whose revocation status you want to freeze:
// Build RFC 6960 OCSPRequest DER for the signer cert (self-issued
// -> issuer == subject; SHA-1 CertID per RFC 5019).
const ocspReq = buildOcspRequestDer(signerCertDer);
const ocspBytes = await fetch('/csc/v2/ocsp', {
method: 'POST',
headers: { 'Content-Type': 'application/ocsp-request',
'Accept': 'application/ocsp-response' },
body: ocspReq
}).then(r => r.arrayBuffer());
// Extract the BasicOCSPResponse (unwrap OCSPResponse.responseBytes.response).
// Repeat for the TSA cert. Bundle both into CMS unsignedAttrs as:
// id-aa-ets-revocationValues (OID 1.2.840.113549.1.9.16.2.24)
// id-aa-ets-certValues (OID 1.2.840.113549.1.9.16.2.23)
Fall-back policy the browser signer uses (recommended pattern):
- Some OCSPs fail: log it, continue as B-LT with whatever revocationValues you got.
- All OCSPs fail: log it, degrade to B-T.
See the OCSP responder page for the full request / response wire format and error taxonomy.
8 B-LTA — append a DocTimeStamp
PDF-level /DocTimeStamp is a signature dict variant with /SubFilter /ETSI.RFC3161 and /Contents = raw RFC 3161 TimeStampToken DER (not CMS SignedData). ByteRange covers the entire prior PDF minus the new /Contents<...> gap.
// 1) Compute SHA-256 of the ENTIRE B-LT PDF as-is (all prior bytes). // 2) Post to /csc/v2/signatures/timestamp OR /csc/v2/signatures/tsa // (in-tenant issuer) to get a TimeStampToken. // 3) Assemble a NEW incremental update: // - New /Sig dict with /SubFilter /ETSI.RFC3161 // - Splice raw token DER (hex) into /Contents // - Fix up the widget /Annots on the first page // - Extend AcroForm.Fields // - Append a fresh xref subsection + trailer with /Prev pointing at the // B-LT trailer's startxref // 4) Fall-back: if the archive-TSA fetch fails, deliver B-LT instead // (log clearly, do not error out).
Re-run this step every few years before the archive TSA cert expires. Each fresh /DocTimeStamp extends the verifiability window.
Server-side alternative — signDoc
If you'd rather ship your PDF, a page-hint and a level flag and let the server assemble everything (including the incremental save), use POST /csc/v2/signatures/signDoc:
const signed = await fetch('/csc/v2/signatures/signDoc', {
method: 'POST', headers: authHeaders,
body: JSON.stringify({
credentialID, SAD,
documents: [{
document: base64OfPdfBytes,
signature_format: 'P', /* PAdES */
conformance_level: 'AdES-B-LT', /* or -B-B / -B-T / -B-LTA */
signed_envelope_property:'ENVELOPED',
parameters: {
signing_reason: 'Contract acceptance',
signing_location: 'Valletta',
contact_info: 'legal@example.com',
signing_time: '20260727120000Z' /* optional; else DateTime.UtcNow */
}
}]
})
}).then(r => r.json());
const signedPdf = base64Decode(signed.documentWithSignature[0]);
signDoc is 10 MiB (vs. 32 KiB elsewhere). The server does the SAD binding + hash computation + PAdES assembly for you and returns the finished PDF ready to download.Troubleshooting
- Adobe Reader shows "signature signature not applied" — usually a ByteRange bug (brackets outside the hole) or a missing widget
/P. Attach the widget to the page/AnnotsAND set its/Pto the page indirect ref. - Adobe shows an empty signature panel — the AcroForm dictionary is missing or does not list the widget. Update
/AcroForm << /Fields [ ... ] /SigFlags 3 >>. - CMS parser rejects the envelope — signedAttrs must be DER-sorted by tag (SET OF requires canonical ordering).
- PAdES-B-T verifies but LT/LTA doesn't — check that OCSP responses are BasicOCSPResponse structures (unwrapped from the outer
OCSPResponse). - Timestamp fetch fails — the client should degrade to PAdES-B-B and log clearly. Do not fail the whole signature.
Standards + wire references
- Cloud Signature Consortium API v2 §§ 11.1–11.10
- ETSI EN 319 142-1 — PAdES baseline profile (B-B / B-T / B-LT / B-LTA)
- ETSI EN 319 122-1 — CAdES baseline (CMS SignerInfo layout)
- RFC 5652 — Cryptographic Message Syntax
- RFC 5035 — ESSCertIDv2 with issuerSerial
- RFC 6211 — id-aa-CMSAlgorithmProtection
- RFC 3161 + RFC 5816 — RFC 3161 TSP with ESSCertIDv2 update
- RFC 6960 + RFC 5019 — OCSP + lightweight profile
- ISO 32000-1 §12.8 + ISO 32000-2 §12.8.5 — PDF signature dictionaries and DocTimeStamp
- Regulation (EU) 910/2014 Article 3(11), 26, 32 — AdES definition and validation requirements
Questions? Ask us.