💡 Volledig werkend voorbeeld beschikbaar op GitHub:
sign-docx-with-mldsa-certificates-python
Inleiding
Onderteken dit middag een contract met RSA‑2048 en je hebt een belofte gedaan die moet blijven gelden zolang het contract van belang is. Als dat twintig of dertig jaar is – en voor akten, toestemmingsformulieren en technische goedkeuringen is dat vaak het geval – moet de belofte langer meegaan dan het algoritme. De aanval hoeft vandaag niet te bestaan. Ze moet bestaan voordat het document niet meer relevant is, waarna iedereen die de openbare sleutel bezit de privésleutel kan afleiden en in jouw naam kan ondertekenen.
Post‑kwantum documentondertekening is de GroupDocs.Signature‑functie voor Python die die belofte vervangt door één die is gebaseerd op ML‑DSA, het handtekeningsalgoritme dat NIST in 2024 heeft gestandaardiseerd als FIPS 204. Ondersteuning voor Word‑formaten kwam in GroupDocs.Signature 26.9, en het hergebruikt de API die je al kent: een ML‑DSA‑sleutel zit in een PFX en wordt in DigitalSignOptions geplaatst precies zoals een RSA‑sleutel.
Deze gids ondertekent een DOCX in vier stappen, vergelijkt de drie beveiligingsniveaus op gemeten output, verifieert de handtekening uitsluitend met een openbaar certificaat, en eindigt met de twee beperkingen die je moet kennen voordat je verder gaat.
Waarom dit belangrijker is dan de gebruikelijke migratie
Handtekeningmigratie verschilt van encryptiemigratie op één punt dat het makkelijker maakt om uit te stellen en lastiger om te corrigeren.
Bij encryptie is het “nu oogsten‑later ontcijferen”‑probleem onmiddellijk: alles wat vandaag wordt onderschept, kan later worden opgeslagen en geopend. Bij handtekeningen wordt niets wat je al hebt ondertekend retroactief vervalsbaar – maar niets wat je hebt ondertekend blijft ook provabel jouw eigendom zodra de sleutel kan worden afgeleid uit het certificaat dat iedereen heeft. Het opnieuw ondertekenen van een decennium aan gearchiveerde documenten met nieuwe sleutels is mogelijk en niemand wil degene zijn die dat plant.
Daarom is het praktische advies smal in plaats van breed: migreer de documenten met een lange bewaartermijn, laat de rest staan. Sommige profielen hebben de lat al gelegd – CNSA 2.0 vereist ML‑DSA‑87 voor systemen met nationale veiligheid – en voor iedereen anders is de doorslaggevende factor hoe lang het bestand defensief moet blijven.
Voorvereisten
- Python 3.9 of hoger op een 64‑bit interpreter – het pakket levert een gebundelde .NET‑runtime en heeft geen 32‑bit wheel
- GroupDocs.Signature voor Python via .NET 26.10.0, met een gratis tijdelijk licentie om de evaluatielimieten te verwijderen
- Een ML‑DSA‑certificaat als een met wachtwoord beveiligde PFX, en een Word‑document om te ondertekenen
Installatie
pip install groupdocs-signature-net
Stap 1 – Onderteken met een ML‑DSA‑certificaat
Het certificaat doet het werk. De aanroep is dezelfde als je voor RSA zou schrijven:
with signature.Signature(source_path) as sign:
options = DigitalSignOptions(pfx_path)
options.password = CERTIFICATE_PASSWORD
result = sign.sign(output_path, options)
Dat is het volledige adoptieverhaal voor code die al ondertekent: wijs DigitalSignOptions naar een andere PFX. Geen nieuwe optie, geen apart algoritme‑parameter, geen tak voor post‑kwantum.
Het uitlezen van de ondertekenaar vereist één extra stap, en bevat de enige Python‑specifieke valkuil in deze hele oefening:
for created in result.succeeded:
certificate = getattr(created, "certificate", None)
subject = getattr(certificate, "subject", None)
if subject:
return str(subject)
Het certificaat op een DigitalSignature is een brugobject dat attributen dynamisch oplost. certificate.subject geeft CN=GroupDocs.Signature MLDSA65 test terug, terwijl dir() op datzelfde object helemaal niets oplevert. Ik inspecteerde het eerst met dir(), concludeerde dat het onderwerp niet werd blootgesteld, en had simpelweg ongelijk – dus als je introspecteert vóór het lezen, sla je een waarde over die er wel is.
Stap 2 – Vergelijk de drie beveiligingsniveaus
ML‑DSA bestaat uit drie parametersets, en ze worden geselecteerd door een ander certificaat te gebruiken:
levels = (
("ML-DSA-44", MLDSA44_PFX),
("ML-DSA-65", MLDSA65_PFX),
("ML-DSA-87", MLDSA87_PFX),
)
for level, pfx_path in levels:
with signature.Signature(source_path) as sign:
options = DigitalSignOptions(pfx_path)
options.password = CERTIFICATE_PASSWORD
sign.sign(output_path, options)
sizes[f"{level} -> {file_name}"] = os.path.getsize(output_path)
Dit is de stap die je echt moet uitvoeren, want de afweging wordt meestal beschreven maar zelden gemeten. Van een broncontract van 132 KB:
| Niveau | NIST‑beveiligingscategorie | Ondertekend bestand | Ten opzichte van kleinste |
|---|---|---|---|
| ML-DSA-44 | 2 | 138.202 bytes | - |
| ML-DSA-65 | 3 | 140.650 bytes | +2.448 bytes |
| ML-DSA-87 | 5 | 143.971 bytes | +5.769 bytes |
Minder dan 6 KB scheidt het zwakste niveau van het sterkste. Bij een contract is dat verwaarloosbaar, waardoor de beslissing simpel wordt: gebruik ML‑DSA‑65 als standaard, ML‑DSA‑87 waar een profiel categorie 5 eist of waar grootte geen rol speelt, en ML‑DSA‑44 alleen wanneer je zoveel bestanden ondertekent dat kilobytes optellen tot iets reëels.
Stap 3 – Verifieer met een openbaar certificaat
Een ontvanger heeft alleen het openbare certificaat van de ondertekenaar nodig en niets geheims:
with signature.Signature(signed_path) as sign:
options = DigitalVerifyOptions(certificate_path)
if password is not None:
options.password = password
return sign.verify(options).is_valid
Het voorbeeld roept dit twee keer aan op hetzelfde bestand: eenmaal met mldsa65.cer, de publieke helft van de ondertekeningssleutel, en eenmaal met een ander ondertekenaar‑PFX. De eerste geeft True terug, de tweede False. Merk op dat het verkeerde certificaat een False oplevert in plaats van een uitzondering – “ondertekend door iemand anders” is een antwoord dat je code moet afhandelen, geen fout. De controle omvat de documentinhoud samen met het serienummer en de vingerafdruk van het certificaat, dus een bestand dat na ondertekening wordt bewerkt, faalt ook.
Stap 4 – Lees de handtekeningen uit een document
Wanneer een ondertekend document arriveert en je niet weet welk certificaat je kunt verwachten:
with signature.Signature(signed_path) as sign:
found = sign.search(SignatureType.DIGITAL)
for item in found:
print(item.sign_time, item.is_valid)
search met SignatureType.DIGITAL retourneert DigitalSignature‑objecten die het certificaat, de ondertekenings‑tijd en een geldigheidsvlag dragen. Een Word‑document kan meerdere handtekeningen bevatten, inclusief een mix van RSA‑ en ML‑DSA‑handtekeningen, en elke wordt gerapporteerd met zijn eigen certificaat en eigen geldigheid.
Verandert dit hoe ontvangers verifiëren?
In geen geval merkbaar. Een ontvanger heeft nog steeds alleen het openbare certificaat van de ondertekenaar nodig, geeft het nog steeds door aan dezelfde DigitalVerifyOptions, en krijgt nog steeds een booleaanse waarde terug. Niets aan het verificatiepad is specifiek voor ML‑DSA. De enige plaats waar het algoritme zichtbaar wordt, is de handtekeningindicator van Microsoft Word, die ML‑DSA mogelijk nog niet herkent omdat het formaat geen standaard‑identificator daarvoor heeft.
Praktische toepassingen
Contracten met lange bewaartermijn
Het duidelijkste geval. Een document dat decennialang verifieerbaar moet blijven, wordt één keer ondertekend, nu, met ML‑DSA‑65 of ML‑DSA‑87, en hoeft nooit opnieuw te worden ondertekend omdat het algoritme verouderd is.
Gereguleerde omgevingen met een benoemd profiel
Waar CNSA 2.0 of een vergelijkbaar profiel van toepassing is, is het niveau geen beoordelingsvraag – ML‑DSA‑87 is de eis, en de enige technische vraag is of het formaat wordt ondersteund.
Gemengde pipelines tijdens migratie
Nieuwe documenten post‑kwantum ondertekenen terwijl het archief onaangeroerd blijft, is een volkomen redelijke tussentoestand, en search dat elke handtekening afzonderlijk rapporteert, maakt het beheersbaar.
Best practices en tips
- Migreer op basis van bewaartermijn, niet op volume. De documenten die dit nodig hebben, zijn de langdurige; een bonnetje dat 90 dagen relevant is, niet.
- Standaard naar ML‑DSA‑65 tenzij een profiel een niveau specificeert, en maak je geen zorgen over het grootteverschil – het is minder dan 6 KB per handtekening.
- Behoud RSA waar de ontvanger valideert in Word. Correcte handtekeningen die een lezer markeert, zijn erger dan een langzamere migratie.
- Vervang de testcertificaten. De PFX‑bestanden in het voorbeeld zijn zelf‑ondertekend met een openbaar wachtwoord, dus alles wat ermee wordt ondertekend bewijst niets.
- Verifieer na ondertekening in elke pipeline, met het openbare certificaat dat een ontvanger zou hebben.
Veelvoorkomende problemen oplossen
Microsoft Word toont de handtekening niet als geldig. Verwacht voor nu: er is geen standaard XML‑DSig‑identificator voor ML‑DSA, dus Word herkent het mogelijk niet, hoewel de handtekening correct is en GroupDocs.Signature deze verifieert. Verifieer in je eigen pipeline en behoud RSA voor documenten waarvan de ontvangers afhankelijk zijn van de Word‑indicator.
De ondertekeningsaanroep weigert een PDF of spreadsheet. ML‑DSA‑ondertekening dekt Word‑formaten – DOCX, DOC, ODT en de rest. PDF, spreadsheets en presentaties worden nog niet ondersteund, en moeten nog steeds met RSA of ECDSA worden ondertekend zoals voorheen.
Het certificaatonderwerp komt leeg terug. Bijna altijd de dir()‑valkuil uit Stap 1: het attribuut wordt dynamisch opgelost, dus lees het in plaats van eerst te testen of het bestaat.
Conclusie
De code‑wijziging is een certificaat‑wijziging, en dat is het deel dat het de moeite waard maakt om dit nu te doen voordat het urgent wordt. Onderteken de langdurige Word‑documenten met ML‑DSA‑65, gebruik ML‑DSA‑87 waar een profiel dat vereist, verifieer met het openbare certificaat, en behoud RSA waar het formaat of de lezer het vereist.
Voer het voorbeeld uit tegen een van je eigen contracten en de drie groottes vertellen je, in bytes, precies wat het sterkste beschikbare niveau je kost. Op het bestand dat ik testte was dat 5.769 bytes.