Package: pyasice
pyasice - ASiC-E (BDOC) and XAdES Manipulation LibraryThepyasicelibrary is designed to:create, read, and verify XAdES/XMLDsig/eIDAS electronic signatures,validate signers' certificates with OCSP,confirm these signatures with TimeStamping,create and manipulateASiC-Eor BDoc 2.1 containers,
which are based on the XAdES/eIDAS stack.ContentsQuickstartASiC-E/BDOC Container File ManipulationSigning Flow UtilitiesNormative ReferencesModule LayoutTechnology StackBuild the XAdES XML Signature meta-fileSignedInfoSignatureValueKeyInfoSignedPropertiesSecondary ServicesOCSPTimestamping ServiceQuickstartASiC-E/BDOC Container File ManipulationCreate a new container:frompyasiceimportContainer,XmlSignaturexmlsig=XmlSignature.create().add_document('test.txt',b'Test data','application/pdf')# ... here goes the signing, confirming and timestamping part ...container=Container()container\.add_file('test.txt',b'Test data','application/pdf')\.add_signature(xmlsig)\.save('test.asice')# container is a context manager:withContainer()ascontainer:container.add_file('a',b'b','c').save('path/to')# Open an existing container:container=Container.open('test.asice')# Verify container. Raises pyasice.SignatureVerificationError on failurecontainer.verify_signatures()# Read files in the containerwithcontainer.open_file('test.txt')asf:assertf.read()==b'Test data'# Iterate over signaturesforxmlsigincontainer.iter_signatures():xmlsig.get_signing_time()Signing Flow UtilitiesfrompyasiceimportContainer,finalize_signature# get this from an external service, ID card, or elsewhereuser_certificate=b'user certificate in DER/PEM format'container=Container()container.add_file("test.txt",b'Test',"text/plain")xml_sig=container.prepare_signature(user_certificate)# Use an external service, or ID card, or a private key from elsewhere# to sign the XML signature structuresignature_value=externally.sign(xml_sig.signed_data())xml_sig.set_signature_value(signature_value)# Get issuer certificate from the ID service provider, e.g. sk.ee.# Here we use the user certificate's `issuer.common_name` field to identify the issuer cert,# and find the cert in the `esteid-certificates` PyPI package.issuer_cert_name=xml_sig.get_certificate_issuer_common_name()importesteid_certificatesissuer_certificate=esteid_certificates.get_certificate(issuer_cert_name)# Complete the XML signature with OCSP and optionally Timestampingfinalize_signature(xml_sig,ocsp_url="https://ocsp.server.url",tsa_url="https://tsa.server.url")container.add_signature(xml_sig)container.save("path/to/file.asice")Normative ReferencesThe main document this library is based on:
theBDOC 2.1.2 spec.The specific standards outlined in that document:ETSI TS 101 903 v1.4.2– XML Advanced Electronic Signatures (XAdES) and its Baseline Profile ETSI TS 103 171;ITU-T Recommendation X.509;RFC 3161– PKIX Time-Stamp protocol;RFC 6960– Online Certificate Status Protocol;ETSI TS 102 918 v1.2.1 - Associated Signature Containers (ASiC) and its
Baseline Profile ETSI TS 103 174.The difference between ASiC-E and BDOC is almost exclusively in terminology.TheBDOC 2.1.2 specstates:The BDOC file format is based on ASiC standard which is in turn profiled by ASiC BP.
BDOC packaging is a ASiC-E XAdES type ZIP container ...So with a moderate risk of confusion, we can accept that ASiC-E and BDOC refer to the same thing.Module Layoutcontainer.py-- theContainerclass, that deals with ASiC-E (BDOC v.2.1) container formatxmlsig.py-- theXmlSignatureclass, that deals with XAdES/XMLDSig XML structuresocsp.py-- theOCSPclass that deals with OCSP requests and responsestsa.py-- theTSAclass that deals with TimeStamping service requests and responsessignature_verifier.py-- theverifyfunction, to verify signatures against a certificate.Technology StackDealing with the subject involves, at least:public key cryptography (RSA, ECDSA);ASN.1 encoding;XML processing;Zip archives;and also requests to various services (obtaining signer's certificate and the signature,
validating the certificate through OCSP, time-stamping the signature).Theasn1cryptolibrary and its higher-level complementoscryptoallow handling certificates and ASN.1 structures quite easily.Thecryptographylibrary is by far the most powerful python library
for dealing with public key cryptography algorithms.Build the XAdES XML Signature meta-fileThe structure of the XAdES XML signature file looks like this:<asic:XAdESSignaturesxmlns:asic="http://uri.etsi.org/02918/v1.2.1#"xmlns:ds="http://www.w3.org/2000/09/xmldsig#"xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"><ds:SignatureId="S0"><ds:SignedInfoId="S0-SignedInfo">...</ds:SignedInfo><ds:SignatureValueId="S0-SIG">...</ds:SignatureValue><ds:KeyInfoId="S0-KeyInfo">...</ds:KeyInfo><ds:ObjectId="S0-object-xades"><xades:QualifyingPropertiesId="S0-QualifyingProperties"Target="#S0"><xades:SignedPropertiesId="S0-SignedProperties"><xades:SignedSignaturePropertiesId="S0-SignedSignatureProperties"><xades:SigningTime>2019-06-07T14:03:50Z</xades:SigningTime><xades:SigningCertificate>...</xades:SigningCertificate><xades:SignaturePolicyIdentifer>...</xades:SignaturePolicyIdentifer></xades:SignedSignatureProperties></xades:SignedProperties></xades:QualifyingProperties></ds:Object></ds:Signature></asic:XAdESSignatures>We'll go over each section below.SignedInfoSignatureValueKeyInfoSignedPropertiesSignedInfoTheSignedInfonode is the source of the data being signed. The XML content of the node, canonicalized
using theCanonicalizationMethodas per the respective child node, is hashed using an algorithm defined in
theSignatureMethodchild node, and this hash is fed to a signing service (ID card, SmartID etc.)<ds:SignedInfoId="S0-SignedInfo"><ds:CanonicalizationMethodAlgorithm="http://www.w3.org/2006/12/xml-c14n11"></ds:CanonicalizationMethod><ds:SignatureMethodAlgorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"></ds:SignatureMethod><ds:ReferenceId="S0-ref-0"URI="test.pdf"><ds:DigestMethodAlgorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>...</ds:DigestValue></ds:Reference><ds:ReferenceId="S0-ref-sp"Type="http://uri.etsi.org/01903#SignedProperties"URI="#S0-SignedProperties"><ds:Transforms><ds:TransformAlgorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethodAlgorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>...</ds:DigestValue></ds:Reference></ds:SignedInfo>TheReferencefields are different in purpose and formation.The firstReferencefield is about the signed document and as such, has anURIattribute of the document's file name.
Its childDigestValueelement is the SHA256 hash of the document, it is, incidentally, the very hash that is sent to the SmartID API for signing.The secondReferenceis built on the basis of some fields defined later in theSignedPropertiessection.
Its childDigestValueis calculated as a SHA256 hash of the canonicalized XML output of theSignedPropertiestag, after that one is formed:
TheURIattribute of thisReferencetag is the#-prefixedIdattribute of theSignedPropertiestag.importbase64importhashlibfromlxmlimportetreebuf=etree.tostring(el,method='c14n',exclusive=TrueorFalse)# NOTE belowdigest_value=base64.b64encode(hashlib.sha256(buf).digest())(Assuming theelhere to be the XML<SignedProperties>element)CanonicalizationTheexclusivekwarg controls whether the namespace declarations of ancestor tags should be included in the resulting canonical representation, orexcluded.
Whether to useexclusive=Truedepends on the canonicalization tag'sAlgorithmattribute:http://www.w3.org/2001/10/xml-exc-c14n#, usesexclusive=True,the two others, the requiredhttp://www.w3.org/TR/2001/REC-xml-c14n-20010315, orhttp://www.w3.org/2006/12/xml-c14n11, are not exclusive.The aforementioned<ds:CanonicalizationMethod>tag controls the c14n of theSignedInfonode before feeding its digest to the signature service.
The c14n ofSignedPropertiesprior to getting its digest is determined by theds:Transformtag within thisds:Referencenode.
If it's not present, then the default, ie. not exclusive, c14n is used.KeyInfoThis section contains the base64-encoded user certificate value, e.g. the SmartID API response'scert.value,
or the certificate obtained from an ID card:<ds:KeyInfoId="S0-KeyInfo"><ds:X509Data><ds:X509Certificate>MIIGJDCCBAygAwIBAgIQBNsLtTIpnmNbbE4+laSLaTANBgkqhkiG9w0BAQsFADBr...</ds:X509Certificate></ds:X509Data></ds:KeyInfo>More details about the certificate in theSigningCertificatesubsection.SignedPropertiesThe XML section ofSignedPropertiesconsists of,at least,
theSigningTime,SigningCertificateandSignaturePolicyIdentiferelements.:question: The signatures returned by e.g.Dokobit,
do not contain theSignaturePolicyIdentifernode.SigningTimeA timestamp in ISO 8601 format.SignaturePolicyIdentifierThis appears to be a static^1 XML chunk referencing the BDOC 2.1 Specifications document:<xades:SignaturePolicyIdentifier><xades:SignaturePolicyId><xades:SigPolicyId><xades:IdentifierQualifier="OIDAsURN">urn:oid:1.3.6.1.4.1.10015.1000.3.2.1</xades:Identifier></xades:SigPolicyId><xades:SigPolicyHash><ds:DigestMethodAlgorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>3Tl1oILSvOAWomdI9VeWV6IA/32eSXRUri9kPEz1IVs=</ds:DigestValue></xades:SigPolicyHash><xades:SigPolicyQualifiers><xades:SigPolicyQualifier><xades:SPURI>https://www.sk.ee/repository/bdoc-spec21.pdf</xades:SPURI></xades:SigPolicyQualifier></xades:SigPolicyQualifiers></xades:SignaturePolicyId></xades:SignaturePolicyIdentifier>[1] The DigestValue is the hash value of the document referenced bySPURI, encoded in base64.
Refer toBDOC 2.1:2014 Specificationfor more information.SigningCertificateThe user certificate is a base64-encoded DER certificate which can be loaded as follows:importbase64fromcryptographyimportx509fromcryptography.hazmat.backendsimportdefault_backendcert_asn1=base64.b64decode(cert_value)cert=x509.load_der_x509_certificate(base64.b64decode(cert_asn1),default_backend())or withpyopenssl:importbase64fromOpenSSL.cryptoimportload_certificate,FILETYPE_ASN1cert_asn1=base64.b64decode(cert_value)openssl_cert=load_certificate(FILETYPE_ASN1,base64.b64decode(cert_asn1))These objects expose a slightly different but similar API.What we need is the issuer name and certificate serial number:assertopenssl_cert.get_serial_number()==cert.sertial_number=='6454262457486410408874311107672836969'assertcert.issuer.rfc4514_string()=='C=EE,O=AS Sertifitseerimiskeskus,2.5.4.97=NTREE-10747013,CN=TEST of ESTEID-SK 2015'assertopenssl_cert.issuer.get_components()==[(b'C',b'EE'),(b'O',b'AS Sertifitseerimiskeskus'),(b'organizationIdentifier',b'NTREE-10747013'),(b'CN',b'ESTEID-SK 2015')]Also we need a SHA256 digest value of the certificate:cert_digest=base64.b64encode(hashlib.sha256(cert_asn1).digest())With these values we can build the certificate information entry of the SignedProperties:<xades:SigningCertificate><xades:Cert><xades:CertDigest><ds:DigestMethodAlgorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>hdsLTm4aaFKaGMwF6fvH5vWmiMBBnTCH3kba+TjY+pE=</ds:DigestValue></xades:CertDigest><xades:IssuerSerial><ds:X509IssuerName>C=EE,O=ASSertifitseerimiskeskus,2.5.4.97=NTREE-10747013,CN=TESTofEID-SK2016</ds:X509IssuerName><ds:X509SerialNumber>98652662091042892833248946646759285960</ds:X509SerialNumber></xades:IssuerSerial></xades:Cert></xades:SigningCertificate>:question: DoesX509IssuerNamecontent need to be acert.issuer.rfc4514_string()or can it be anything else?So, in the end, we get a<xades:SignedProperties>element which we then canonicalize and calculate a sha256 hash of this string,
to place it in the appropriate<ds:Reference>element.SignatureValue<ds:SignatureValueId="SIG-{SIGNATURE_ID}"><!-- Base64-encoded SIGNATURE_VALUE, gotten externally --></ds:SignatureValue>A base64-encoded value of the signature calculated over the signed data.
The signed data is theds:SignedInfosection, asdescribed above.When using SmartID/MobileID, this is taken from thesignature.valuefield of the response.KeyInfoContains the base64-encoded certificate, as gotten from the SmartID response.<ds:KeyInfoId="S0-KeyInfo"><ds:X509Data><ds:X509Certificate>...</ds:X509Certificate></ds:X509Data></ds:KeyInfo>Secondary ServicesOCSPOCSP (Online Certificate Status Protocol)
is designed to check that the signing certificate is valid at the point of signing. It is a binary protocol, and uses ASN.1 encoding in both request and response payload.
To deal with it, we're using theasn1cryptolibrary.The OCSP request should be made immediately after signing, and the base64-encoded response is embedded in the XAdES signature as axades:UnsignedSignaturePropertiesdescendant node,
namelyxades:EncapsulatedOCSPValue.Estonian eIDURLs for OCSP services:Demo:http://demo.sk.ee/ocspProduction:http://ocsp.sk.ee/More detail on thesk.ee OCSP pageTimestamping serviceTheTimeStamp protocolis also a binary protocol, for getting a Long-Term Validity Timestamp for a signature.
Also handled with the help of theasn1cryptolibrary.The TSA request should be made immediately after OCSP validity confirmation, and the base64-encoded response is embedded in the XAdES signature as axades:UnsignedSignaturePropertiesdescendant node,
namelyxades:EncapsulatedTimeStamp.Estonian eIDURLs for timestamping services:Demo:http://demo.sk.ee/tsa/Production:http://tsa.sk.eeMore detail on thesk.ee TSA page