Kochbuch · digitale Signaturen

PDFs mit CSC v2 signieren — PAdES-B-B bis B-LTA.

Integrieren Sie den CodeB-Fern-Signaturdienst in Ihr Produkt. Jeder authentifizierte Nutzer erhält ein EC-P-256-Signaturzertifikat, dessen Subject DN aus dem OIDC-Profil angereichert wird. Signieren Sie einen Hash mit signHash, verpacken Sie ihn client-seitig zu einer PAdES-Hülle (sign.html) oder lassen Sie es den Server per signDoc erledigen, fügen Sie einen RFC-3161-Zeitstempel hinzu, betten Sie RFC-6960-OCSP-Widerrufsdaten ein, hängen Sie einen Dokument-Zeitstempel an — PAdES bis B-LTA. Bereit für Integrationen mit der European Digital Identity Wallet.

Umfangserklärung — fortgeschrittene elektronische Signatur. Erzeugt AdES gemäß Verordnung (EU) 910/2014 Art. 3(11). Der Signaturschlüssel ist software-basiert und das Zertifikat selbstsigniert durch eine tenant-interne CA — keine QSCD, keine QES. Die ICryptoModule-Abstraktion ist HSM-vorbereitet; Azure Key Vault und PKCS#11 sind gestubbt und liefern HTTP 501, bis verdrahtet.

Live testen → Vollständige API-Referenz

Die vier PAdES-Konformitätsstufen

Wählen Sie die niedrigste Stufe, die Ihren Beweisbedarf deckt. Höhere Stufen ergänzen Langzeit-Gültigkeit (überleben Zertifikatsablauf), zum Preis größerer Hüllen und zusätzlicher Netzwerk-Roundtrips.

PAdES-B-B

Basic — nur signierte Attribute. Prüfbar, solange das Signer-Zertifikat gültig ist.

  • CMS-SignerInfo mit signingCertificateV2 (RFC 5035)
  • id-aa-CMSAlgorithmProtection (RFC 6211)

PAdES-B-T

Time — ergänzt einen RFC-3161-TSA-Token, der belegt, dass die Signatur zu einem bestimmten Zeitpunkt existierte.

  • B-B + id-aa-signatureTimeStampToken
  • ~5 KiB Overhead pro Signatur

PAdES-B-LT

Long-Term — bettet OCSP-Antworten + Zertifikatskette ein, damit die Prüfung offline nach Zertifikatsablauf funktioniert.

  • B-T + id-aa-ets-revocationValues + id-aa-ets-certValues
  • ~10-15 KiB Overhead

PAdES-B-LTA

Long-Term with Archive — hängt einen /DocTimeStamp über die gesamte B-LT-PDF an. Erneuern Sie ihn vor Ablauf des Archiv-TSA, um die Gültigkeit unbegrenzt zu verlängern.

  • B-LT + inkrementelle /DocTimeStamp-Signatur
  • ISO 32000-2 §12.8.5

1 OIDC-Anmeldung — Access Token holen

CSC v2 reitet auf dem Tenant-OIDC-Provider. Standard OAuth 2.0 Authorization Code + PKCE. Siehe das OIDC-Anmeldungs-Kochbuch. Danach ist alles Authorization: Bearer <token>.

const accessToken = tokenResp.access_token;
const authHeaders = {
  'Authorization': 'Bearer ' + accessToken,
  'Content-Type':  'application/json'
};

2 Credentials des Nutzers listen

In Phase 1b hat jeder Nutzer genau ein Pro-Nutzer-EC-P-256-Credential.

const listResp = await fetch('/csc/v2/credentials/list', {
  method: 'POST', headers: authHeaders, body: '{}'
}).then(r => r.json());
const credentialID = listResp.credentialIDs[0];

const info = await fetch('/csc/v2/credentials/info', {
  method: 'POST', headers: authHeaders,
  body: JSON.stringify({ credentialID })
}).then(r => r.json());

3 Signierte-Attribute-Hash berechnen, dann SAD anfordern

Die Signature Activation Data (SAD) ist ein kurzlebiger JWT, der Signer, Credential und exakte Hash-Liste bindet. Berechnen Sie SHA-256 der CMS-signedAttributes-DER (siehe Schritt 5), dann:

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'
  })
}).then(r => r.json());
// SCAL1 -> sofortiger SAD in authorize.SAD
// SCAL2 -> authorize.pending=true; Nutzer PIN-bestätigung anfordern
Binden Sie den SAD an den exakten Hash. Der Server prüft SAD.hash === request.hash bei signHash. Niemals einen SAD für einen anderen Hash wiederverwenden.

4 Hash signieren

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'
  })
}).then(r => r.json());

const rawSig = base64Decode(sig.signatures[0]);   // 64-Byte r||s
Die Antwort ist die rohe ECDSA-r||s-Konkatenation. Verpacken Sie sie in SEQUENCE { INTEGER r, INTEGER s }, bevor Sie sie in die CMS-SignerInfo.signature-OCTET-STRING einfügen.

5 PAdES-Hülle zusammenbauen

Wenn Sie keine Bytes anfassen wollen: nutzen Sie den Browser-Signer oder das serverseitige signDoc. Wenn Sie selbst bauen wollen:

5.1 · /Contents-Platzhalter reservieren

Bauen Sie den PDF-inkrementellen Update-Trailer mit dem Signatur-Dictionary. Die /Contents<...>-Hex-Zeichenkette wird auf Ihre Zielgröße platzhalter-aufgefüllt (64 KiB komfortabel für B-LTA). Merken Sie sich die Byte-Offsets der <- und >-Klammern.

Klammer-Regel (ISO 32000-1 §12.8.1.1): Die <- und >-Klammern sitzen innerhalb des ByteRange-Lochs, nicht im signierten Bereich.

5.2 · signedAttributes-DER berechnen

Fünf verpflichtende Signed-Attribute, DER-sortiert: contentType, messageDigest, signingTime, signingCertificateV2 (mit issuerSerial), id-aa-CMSAlgorithmProtection. SHA-256 des DER-kodierten SET. Das ist der Hash, den Sie an signHash senden.

5.3 · CMS in den Platzhalter spleißen

CMS-SignedData mit eingebettetem Signer-Zertifikat bauen, dann hex-kodieren. Linksbündig in das /Contents<...>-Fenster; Rest mit 0 auffüllen. ByteRange-Ganzzahlen so anpassen, dass alle vier in den reservierten Platz passen.

6 B-T — RFC-3161-Zeitstempel hinzufügen

SHA-256 der CMS-SignerInfo.signature-OCTET-STRING-Bytes, dann an den Zeitstempel-Endpunkt senden:

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 ist ein base64 DER RFC-3161-TimeStampToken.
// In CMS SignerInfo.unsignedAttrs als id-aa-signatureTimeStampToken spleißen.
Zwei TSA-Endpunkte stehen bereit. signatures/timestamp leitet an HKLM\SOFTWARE\CodeB\TSAURL weiter (Standard Sectigo). signatures/tsa ist ein Tenant-interner Aussteller (siehe TSA-Server-Notiz).

7 B-LT — OCSP-Antworten + Zertifikatskette einbetten

Langzeit-Validierung bedeutet, dass eine verlassende Partei die Signatur nach Ablauf des Signer-Zertifikats prüfen kann. Die Widerrufs-Evidenz und die Kette werden innerhalb der Signatur gebündelt.

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());
// BasicOCSPResponse aus dem OCSPResponse.responseBytes.response extrahieren.
// Wiederholen für TSA. Beide als:
//   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)

Fallback-Regeln (Muster des Browser-Signers):

  • Einige OCSPs schlagen fehl: loggen, als B-LT mit Teildaten fortfahren.
  • Alle OCSPs schlagen fehl: loggen, auf B-T degradieren.

Wire-Format und Fehler-Taxonomie: siehe OCSP-Responder-Seite.

8 B-LTA — DocTimeStamp anhängen

PDF-Ebene /DocTimeStamp ist eine Signatur-Dict-Variante mit /SubFilter /ETSI.RFC3161 und /Contents = rohes RFC-3161-TimeStampToken-DER (kein CMS-SignedData). ByteRange überdeckt die gesamte vorherige PDF außer dem neuen /Contents<...>-Loch.

Wiederholen Sie diesen Schritt alle paar Jahre vor Ablauf des Archiv-TSA-Zertifikats. Jeder frische /DocTimeStamp verlängert das Prüffenster.

Serverseitige Alternative — signDoc

Wenn Sie stattdessen die PDF senden und der Server alles zusammenbauen soll (inkl. inkrementellem Speichern), nutzen Sie 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',
      conformance_level: 'AdES-B-LT',
      signed_envelope_property: 'ENVELOPED',
      parameters: {
        signing_reason: 'Vertragsannahme',
        signing_location: 'Valletta',
        contact_info: 'legal@example.com'
      }
    }]
  })
}).then(r => r.json());
const signedPdf = base64Decode(signed.documentWithSignature[0]);
Body-Cap für signDoc: 10 MiB (sonst 32 KiB). Der Server macht SAD-Bindung, Hash-Berechnung und PAdES-Zusammenbau für Sie und liefert die fertige PDF zurück.

Fehlerbehebung

  • Adobe Reader: "Signatur nicht angewendet" — meist ein ByteRange-Bug oder fehlendes Widget-/P.
  • Leerer Signaturbereich in Adobe — AcroForm-Dictionary fehlt oder listet das Widget nicht.
  • CMS-Parser lehnt Hülle ab — signedAttrs müssen DER-nach-Tag sortiert sein.
  • PAdES-B-T prüft, LT/LTA nicht — OCSP-Antworten müssen BasicOCSPResponse-Strukturen sein (aus OCSPResponse ausgepackt).
  • Zeitstempel-Fetch schlägt fehl — Client sollte auf PAdES-B-B degradieren und klar loggen. Nicht die ganze Signatur scheitern lassen.

Standards & Wire-Referenzen