pyHanko is de Python-bibliotheek waar kleinere koppelingen meestal naar grijpen. Zijn workflow "interrupted signing" is gemaakt voor een sleutel op afstand: bereid het document en de signed attributes voor, geef de hash aan wie de sleutel heeft, en maak af met de waarde die terugkomt. De code hieronder volgt het patroon uit pyHanko's eigen library guide (sectie "Interrupted signing"), aangepast voor deze flow, met de klasse- en methodenamen zoals ze daar staan.
Behandel het als een uitgewerkte schets, niet als getoetste code. Anders dan het DSS-hoofdstuk, dat uit een service komt die wij draaien, is deze hier niet uitgevoerd. Leg het naast de documentatie van de pyHanko-versie die je vastzet (0.25 of nieuwer) voordat je erop bouwt.
pip install pyHanko pyhanko-certvalidator
Voorbereiden (stap 4 van de flow)
import base64, hashlib
from asn1crypto import x509
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import signers, fields
from pyhanko_certvalidator.registry import SimpleCertificateStore
# Certificates from credentials/info, base64 DER, signer first.
chain = [x509.Certificate.load(base64.b64decode(c)) for c in cert_b64_list]
signing_cert = chain[0]
registry = SimpleCertificateStore.from_certs(chain)
# A placeholder signer: same certificate, a signature value of the right LENGTH.
# 256 bytes for RSA-2048; the real value replaces it at completion.
placeholder = signers.ExternalSigner(
signing_cert=signing_cert,
cert_registry=registry,
signature_value=bytes(256),
)
meta = signers.PdfSignatureMetadata(
field_name="Signature1",
subfilter=fields.SigSeedSubFilter.PADES, # ETSI.CAdES.detached
md_algorithm="sha256",
)
with open("input.pdf", "rb") as f:
w = IncrementalPdfFileWriter(f)
pdf_signer = signers.PdfSigner(meta, signer=placeholder)
# Writes the field and the reserved /Contents, digests the byte ranges.
prep_digest, tbs_document, output = pdf_signer.digest_doc_for_signing(w)
# The CMS signed attributes around the document digest. PAdES flavour:
# no signingTime attribute, signing-certificate-v2 present.
signed_attrs = placeholder.signed_attrs(
prep_digest.document_digest, "sha256", use_pades=True
)
# The hash for Cleverbase: SHA-256 over the DER of the signed attributes,
# encoded exactly as pyHanko will sign them.
tbs = signed_attrs.dump()
digest = hashlib.sha256(tbs).digest()
hash_b64 = base64.b64encode(digest).decode() # signHash
hash_b64url = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() # oauth2/authorize
Bewaar output (de voorbereide PDF-bytes, output.getvalue() als het een BytesIO is), prep_digest en signed_attrs (pickle ze of houd ze in het proces). Alles tussen hier en het afmaken, de autorisatierondgang, de bevestiging van de ondertekenaar en signHash, mag het voorbereide document niet aanraken.
De lengte van de plaatshouder doet ertoe: de echte handtekening mag niet langer zijn dan de plaatshouder, anders mislukt het afmaken. Gebruik de sleutellengte uit credentials/info (key.len / 8 bij RSA; bij ECDSA over P-256 is een DER SEQUENCE { r, s } maximaal 72 bytes).
Afmaken (stap 7 van de flow)
import asyncio
from pyhanko.sign.signers.pdf_signer import PdfTBSDocument
signature_value = base64.b64decode(signatures_from_cleverbase[0])
# A signer carrying the REAL value; the attributes are the ones we hashed.
real = signers.ExternalSigner(
signing_cert=signing_cert,
cert_registry=registry,
signature_value=signature_value,
)
sig_cms = real.sign_prescribed_attributes("sha256", signed_attrs=signed_attrs)
asyncio.run(
PdfTBSDocument.async_finish_signing(
output, prepared_digest=prep_digest, signature_cms=sig_cms
)
)
with open("signed.pdf", "wb") as f:
f.write(output.getvalue())
sign_prescribed_attributes bouwt de CMS SignedData met jouw signed attributes, de certificaatketen uit de registry en de handtekeningwaarde; async_finish_signing schrijft die in de gereserveerde /Contents. Er verandert niets anders in het bestand, dus de handtekening dekt precies de bytes die bij het voorbereiden gedigest zijn.
Het algoritme, één keer
pyHanko leidt het handtekeningalgoritme af uit het sleuteltype van het ondertekencertificaat en de digest die je meegeeft (sha256), dus een RSA-certificaat geeft sha256WithRSAEncryption in de CMS. Dat is wat je wilt, en het is ook waarom het verzoek aan Cleverbase er anders uitziet: daar stuur je signAlgo 1.2.840.113549.1.1.1 (rsaEncryption) met hashAlgo SHA-256, dezelfde handtekening onder zijn andere naam. Geef prefer_pss=True niet mee tenzij Cleverbase je heeft gezegd dat de credential PSS doet; de CMS zou dan bytes beschrijven die je niet hebt gekregen.
Een zichtbare handtekening
digest_doc_for_signing maakt standaard een onzichtbaar veld. Voor een zichtbaar veld definieer je het eerst met fields.SigFieldSpec(sig_field_name="Signature1", box=(x1, y1, x2, y2), on_page=0) via fields.append_signature_field(w, spec), voordat je de PdfSigner maakt, en geef je PdfSignatureMetadata een stamp_style. Doe dit voor het voorbereiden; de appearance stream valt binnen de ondertekende byte range.
Verder gaan
- B-T: geef
PdfSignereentimestamper=timestamps.HTTPTimeStamper(url); de tijdstempel komt erbij bij het afmaken en de ondertekende hash verandert niet. Reserveer meer ruimte metbytes_reservedopdigest_doc_for_signing. - B-LT en B-LTA:
PdfTimeStamper.update_archival_timestamp_chainen een validation context die OCSP en CRL ophaalt. "De PAdES-niveaus" behandelt wanneer dat de moeite is.