UNPKG

undersign

Version:

Create eIDAS compatible XAdES digital signatures with certificate OCSP responses and timestamps. Works with the Estonian Id-card and Mobile-Id to create BDOCs, but isn't limited to Estonia.

332 lines (288 loc) 10.9 kB
var XadesXml = require("./lib/xades_xml") var Crypto = require("./lib/crypto") var Certificate = require("./lib/certificate") var OcspResponse = require("./lib/ocsp").OcspResponse var TimestampResponse = require("./lib/timestamp").TimestampResponse // XML Security URLs: https://tools.ietf.org/html/rfc6931 var C14N_URL = "http://www.w3.org/2001/10/xml-exc-c14n#" var SHA256_URL = "http://www.w3.org/2001/04/xmlenc#sha256" var sha256 = Crypto.hash.bind(null, "sha256") var concat = Array.prototype.concat.bind(Array.prototype) module.exports = Xades // https://www.w3.org/TR/xmldsig-core1/#sec-AlgID var SIG_ALGORITHM_URLS = { rsa: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", ecdsa: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", dsa: "http://www.w3.org/2009/xmldsig11#dsa-sha256" } // curl https://www.sk.ee/repository/bdoc-spec21.pdf | sha256sum | // awk '{ printf("%s", $1) }' | xxd -r -p | base64 var BDOC_2_1_0_OID = "1.3.6.1.4.1.10015.1000.3.2.1" var BDOC_2_1_0_SHA256 = "3Tl1oILSvOAWomdI9VeWV6IA/32eSXRUri9kPEz1IVs=" //var BDOC_2_1_2_OID = "1.3.6.1.4.1.10015.1000.3.2.3" var BDOC_POLICY = { // The order of elements in <xades:SignaturePolicyIdentifier> is fixed // according to libdigidocpp's schemas. xades$SignaturePolicyId: { xades$SigPolicyId: { xades$Identifier: { // Digidoc4j v3.3.0 fails if not given BDOC v2.1.0's OID. That // is, it doesn't support BDOC v2.1.2. Qualifier: "OIDAsURN", $: `urn:oid:${BDOC_2_1_0_OID}` } }, // <xades:SigPolicyHash> is the hash of the <xades:SPURI> content, // although BDOC v2.1.2 says it's not to be really validated. xades$SigPolicyHash: { ds$DigestMethod: {Algorithm: SHA256_URL}, ds$DigestValue: {$: BDOC_2_1_0_SHA256} }, // Should move the specification to somewhere under our control? xades$SigPolicyQualifiers: { xades$SigPolicyQualifier: { xades$SPURI: {$: "https://www.sk.ee/repository/bdoc-spec21.pdf"} } } } } // Profile-wise BDOC v2.1.2 creates XAdES-LT signatures based on either // XAdES-EPES or XAdES-BES. "LT" stands for "long-term". "T" for time, which // can come from a time-mark (TM) or a time-stamp (TS). Time-mark is a method // of getting a timestamp from the OCSP response. Time-stamp gets it from // a dedicated server. // // The XAdES profiles are described in https://www.etsi.org/deliver/etsi_ts/103100_103199/103171/02.01.01_60/ts_103171v020101p.pdf. function Xades(cert, files, opts) { this.certificate = cert var signedProperties = { Id: "signed-properties", xades$SignedSignatureProperties: { // BDOC v2.1.2 says the time of the signature creation is the time of the // OCSP response. However it still requires a <xades:SigningTime> // element. // // TODO: Confirm whether this can include milliseconds or not. xades$SigningTime: {$: formatIsoDateTime(new Date)}, xades$SigningCertificate: { xades$Cert: { xades$CertDigest: { ds$DigestMethod: {Algorithm: SHA256_URL}, ds$DigestValue: {$: sha256(cert.toBuffer()).toString("base64")} }, xades$IssuerSerial: { // https://www.w3.org/TR/xmldsig-core1/ refers to RFC 4514 and its // distinguished name encoding rules. ds$X509IssuerName: {$: cert.issuerRfc4514Name}, ds$X509SerialNumber: {$: cert.serialNumber.toString()} } } }, }, xades$SignedDataObjectProperties: { xades$DataObjectFormat: files.map((file, i) => ({ ObjectReference: "#file-" + i, xades$MimeType: {$: String(file.type)} })) } } if (opts) switch (opts.policy) { case undefined: case null: break case "bdoc": // Only used with BDOC and its time-mark variant. // There's also <xades:SignaturePolicyImplied>. var props = signedProperties.xades$SignedSignatureProperties props.xades$SignaturePolicyIdentifier = BDOC_POLICY break default: throw new RangeError("Unknown policy: " + opts.policy) } var canonicalizedSignedProps = XadesXml.canonicalize({ asic$XAdESSignatures: { ds$Signature: { ds$Object: { xades$QualifyingProperties: { xades$SignedProperties: signedProperties } } } } }, [ "asic$XAdESSignatures", "ds$Signature", "ds$Object", "xades$QualifyingProperties", "xades$SignedProperties" ]) var sigAlgorithmUrl = SIG_ALGORITHM_URLS[cert.publicKeyAlgorithmName] if (sigAlgorithmUrl == null) throw new RangeError( "Unsupported signature algorithm: " + cert.publicKeyAlgorithmName ) var signedInfo = { // While BDOC v2.1.2 seems to hint the use of // http://www.w3.org/2006/12/xml-c14n11, libdigidocpp v3.14.1 supports // Exclusive XML Canonicalization just fine. So does Digidoc4j v3.3.0. As // the exclusive variant is a more robust canonicalization method for // extracted subtrees, sticking to that. Libdigidocpp's source code even // has comments and Digidoc4j has tests regarding explicit support for // exclusive canonicalization. In the worst case we're just EIDAS and // XADES LT-TM compatible. ds$CanonicalizationMethod: {Algorithm: C14N_URL}, ds$SignatureMethod: {Algorithm: sigAlgorithmUrl}, ds$Reference: concat({ Type: "http://uri.etsi.org/01903#SignedProperties", URI: "#signed-properties", ds$Transforms: {ds$Transform: {Algorithm: C14N_URL}}, ds$DigestMethod: {Algorithm: SHA256_URL}, ds$DigestValue: {$: sha256(canonicalizedSignedProps).toString("base64")} }, files.map((file, i) => ({ // An id is required to match the DataObjectFormat to this // reference. Id: "file-" + i, URI: file.path, ds$DigestMethod: {Algorithm: SHA256_URL}, ds$DigestValue: {$: file.hash.toString("base64")} }))) } this.obj = { // https://www.w3.org/TR/XAdES/ asic$XAdESSignatures: { ds$Signature: { Id: "signature", ds$SignedInfo: signedInfo, ds$SignatureValue: {}, ds$KeyInfo: { ds$X509Data: {ds$X509Certificate: {$: cert.toString("base64")}} }, ds$Object: { xades$QualifyingProperties: { Target: "#signature", xades$SignedProperties: signedProperties, xades$UnsignedProperties: { xades$UnsignedSignatureProperties: { // TODO: According to BDOC v2.1.2 Section 6, based on XAdES LT, // <xades:CertificateValues> needs to include the issuer chain // certificates and the OCSP response certificate if the latter // is not included in the OCSP response itself. That includes // the time stamp's certificate if not included in the time // stamp itself. // // libdigidocpp v3.14.1 doesn't seem to make use of any // included certificates though. // // Digidoc4j v3.3.0 on the other hand throws "OCSP Responder // does not meet TM requirements" if the OCSP response // certificate is missing. It doesn't seem to depend on the // issuer nor root CA certificate. //xades$CertificateValues: { // xades$EncapsulatedX509Certificate: [] //}, // Optionally insert <xades:RevocationValues> here later. // Optionally insert <xades:SignatureTimeStamp> here later. } } } } } } } } Xades.prototype.obj = null Xades.prototype.certificate = null Xades.prototype.signableHashAlgorithm = "sha256" Xades.prototype.__defineGetter__("signable", function() { return XadesXml.canonicalize(this.obj, [ "asic$XAdESSignatures", "ds$Signature", "ds$SignedInfo" ]) }) Xades.prototype.__defineGetter__("signableHash", function() { return sha256(this.signable) }) Xades.prototype.__defineGetter__("signature", function() { return Buffer.from( this.obj.asic$XAdESSignatures.ds$Signature.ds$SignatureValue.$, "base64" ) }) Xades.prototype.setSignature = function(signature) { var basedSig = signature.toString("base64") this.obj.asic$XAdESSignatures.ds$Signature.ds$SignatureValue.$ = basedSig } // The <ds:SignatureVale> element is hashed for time stamping as per XAdES // v1.4.1 Specification. Xades.prototype.__defineGetter__("signatureElement", function() { return XadesXml.canonicalize(this.obj, [ "asic$XAdESSignatures", "ds$Signature", "ds$SignatureValue" ]) }) // BDOC v2.1.0 required that the time stamp be queried before the OCSP response. // http://open-eid.github.io/libdigidocpp/manual.html#signature-notes: // // «An exception is thrown if the OCSP confirmation's time is earlier than // time-stamp's time. If the OCSP confirmation's time is later than // time-stamp's time by more than 15 minutes then a warning is returned. If the // difference is more than 24 hours then exception is thrown.» // // BDOC v2.1.2, however, removed that requirement as can be seen in the // specification and from the CHANGELOG: https://www.id.ee/?id=36110. // // BDOC v2.1.2 says only OCSP resposnes with status "good" are allowed. Xades.prototype.setOcspResponse = function(ocsp) { var obj = this.obj.asic$XAdESSignatures.ds$Signature.ds$Object var props = obj.xades$QualifyingProperties.xades$UnsignedProperties props = props.xades$UnsignedSignatureProperties props.xades$RevocationValues = { xades$OCSPValues: { xades$EncapsulatedOCSPValue: {$: serializeOcsp(ocsp).toString("base64")} } } } // http://open-eid.github.io/libdigidocpp/manual.html#signature-notes // «In case of BDOC-TS signature, the time-stamping authority's (TSA's) // certificate is not added to the <CertificateValues> element (differently // from the requirements of BDOC specification, chap 6) to avoid duplication of // the certificate in the signature. It is expected that the TSA certificate is // present in the time-stamp token itself.» Xades.prototype.setTimestamp = function(stamp) { var obj = this.obj.asic$XAdESSignatures.ds$Signature.ds$Object var props = obj.xades$QualifyingProperties.xades$UnsignedProperties props = props.xades$UnsignedSignatureProperties props.xades$SignatureTimeStamp = { xades$EncapsulatedTimeStamp: { $: serializeTimestamp(stamp).toString("base64") } } } Xades.prototype.toString = function() { return XadesXml.stringify(this.obj) } Xades.prototype.valueOf = function() { return this.obj } Xades.parse = function(xml) { var obj = XadesXml.parse(xml) var keyInfo = obj.asic$XAdESSignatures.ds$Signature.ds$KeyInfo var der = Buffer.from(keyInfo.ds$X509Data.ds$X509Certificate.$, "base64") var cert = new Certificate(der) var xades = Object.create(Xades.prototype) xades.obj = obj xades.certificate = cert return xades } function serializeOcsp(ocsp) { if (ocsp instanceof OcspResponse) return ocsp.toBuffer() if (ocsp instanceof Buffer) return ocsp throw new TypeError("Invalid OCSP response type: " + ocsp) } function serializeTimestamp(stamp) { if (stamp instanceof TimestampResponse) return stamp.token if (stamp instanceof Buffer) return stamp throw new TypeError("Invalid time stamp type: " + stamp) } function formatIsoDateTime(time) { return time.toISOString().replace(/\.\d\d\dZ$/, "Z") }