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.
257 lines (212 loc) • 8.65 kB
JavaScript
var _ = require("./underscore")
var Bn = require("bn.js")
var Pem = require("./pem")
var X509Asn = require("./x509_asn")
var Crypto = require("crypto")
var LdapAttributes = require("./ldap_attributes")
var flatten = Function.apply.bind(Array.prototype.concat, Array.prototype)
var OCSP_URL_OID = require("./x509_asn").OCSP_URL
// https://tools.ietf.org/html/rfc4517#appendix-A
var EMPTY_ARR = Array.prototype
var LDAP_ATTRS = require("./ldap_attributes.json")
exports = module.exports = Certificate
var rfc4514Attrs = new LdapAttributes(_.pick(LDAP_ATTRS, [
"0.9.2342.19200300.100.1.1", // userId
"0.9.2342.19200300.100.1.25", // domainComponent
"2.5.4.3", // commonName
"2.5.4.6", // countryName
"2.5.4.7", // localityName
"2.5.4.8", // stateOrProvinceName
"2.5.4.9", // streetAddress
"2.5.4.10", // organizationName
"2.5.4.11", // organizationalUnitName
]))
var SIGNATURE_HASH_ALGORITHMS = {
"rsa-sha1": "sha1",
"rsa-sha224": "sha224",
"rsa-sha256": "sha256",
"rsa-sha384": "sha384",
"rsa-sha512": "sha512",
"ecdsa-sha1": "sha1",
"ecdsa-sha224": "sha224",
"ecdsa-sha256": "sha256",
"ecdsa-sha384": "sha384",
"ecdsa-sha512": "sha512"
}
function Certificate(der) {
this.asn = X509Asn.Certificate.decode(der)
}
Certificate.prototype.__defineGetter__("serialNumber", function() {
return this.asn.tbsCertificate.serialNumber
})
Certificate.prototype.__defineGetter__("subject", function() {
return serializeSubject(this.asn.tbsCertificate.subject)
})
Certificate.prototype.__defineGetter__("subjectDer", function() {
return X509Asn.Name.encode(this.asn.tbsCertificate.subject)
})
Certificate.prototype.__defineGetter__("validFrom", function() {
return parseTime(this.asn.tbsCertificate.validity.notBefore)
})
Certificate.prototype.__defineGetter__("validUntil", function() {
return parseTime(this.asn.tbsCertificate.validity.notAfter)
})
Certificate.prototype.__defineGetter__("subjectDistinguishedName", function() {
return serializeDistinguishedName(this.asn.tbsCertificate.subject)
})
Certificate.prototype.__defineGetter__("subjectRfc4514Name", function() {
return serializeRfc4514Name(this.asn.tbsCertificate.subject)
})
Certificate.prototype.__defineGetter__("publicKey", function() {
return this.asn.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey.data
})
Certificate.prototype.__defineGetter__("publicKeyAlgorithm", function() {
var oid = this.asn.tbsCertificate.subjectPublicKeyInfo.algorithm.algorithm
return oid.join(".")
})
Certificate.prototype.__defineGetter__("publicKeyAlgorithmName", function() {
return X509Asn.CRYPTO_NAMES[this.publicKeyAlgorithm]
})
Certificate.prototype.__defineGetter__("publicKeyParameters", function() {
var algorithm = this.asn.tbsCertificate.subjectPublicKeyInfo.algorithm
switch (this.publicKeyAlgorithmName) {
case "ecdsa": return X509Asn.EcParameters.decode(algorithm.parameters)
default: return null
}
})
Certificate.prototype.__defineGetter__("issuer", function() {
return serializeSubject(this.asn.tbsCertificate.issuer)
})
Certificate.prototype.__defineGetter__("issuerDer", function() {
return X509Asn.Name.encode(this.asn.tbsCertificate.issuer)
})
Certificate.prototype.__defineGetter__("issuerDistinguishedName", function() {
return serializeDistinguishedName(this.asn.tbsCertificate.issuer)
})
Certificate.prototype.__defineGetter__("issuerRfc4514Name", function() {
return serializeRfc4514Name(this.asn.tbsCertificate.issuer)
})
Certificate.prototype.__defineGetter__("issuerSignatureAlgorithm", function() {
return this.asn.signatureAlgorithm.algorithm.join(".")
})
Certificate.prototype.__defineGetter__("issuerSignatureAlgorithmName",
function() {
return X509Asn.SIGNATURE_NAMES[this.issuerSignatureAlgorithm]
})
Certificate.prototype.__defineGetter__("ocspUrl", function() {
// Authority Information Access is defined in RFC 5280.
// https://tools.ietf.org/html/rfc5280#section-4.2.2.1
var extensions = this.asn.tbsCertificate.extensions || EMPTY_ARR
var info = extensions.find((e) => e.extnID == "authorityInformationAccess")
if (info == null) return null
var ocspInfo = info.extnValue.find((info) => (
_.deepEquals(info.accessMethod, OCSP_URL_OID) &&
info.accessLocation.type == "uniformResourceIdentifier"
))
return ocspInfo && ocspInfo.accessLocation.value
})
Certificate.prototype.hasSigned = function(signable, signature) {
var algorithm = this.publicKeyAlgorithmName
var verifier = Crypto.createVerify("sha256").update(signable)
// "ESTEID-SK 2007" issued certificates that sign with RSA.
// "EID-SK 2011" issued certificates that sign with RSA.
// "ESTEID-SK 2011" issued certificates for Id-card that sign with RSA.
// "ESTEID-SK 2011" issued certificates for Mobile-Id that sign with ECDSA.
// "ESTEID-SK 2015" issued certificates that sign with both RSA and ECDSA.
// "ESTEID2018" issued certificates that sign with ECDSA.
switch (algorithm) {
case "rsa": break
case "ecdsa":
// https://www.openssl.org/docs/man1.1.0/man3/ECDSA_sign.html
// We're presuming the ECDSA signature is in X9.62 aka CVC format, i.e
// without the ASN wrapper.
var r = new Bn(signature.slice(0, signature.length / 2))
var s = new Bn(signature.slice(signature.length / 2))
signature = X509Asn.EcSignature.encode({r: r, s: s})
break
default: throw new Error("Unsupported signature algorithm: " + algorithm)
}
// OpenSSL v1.0's PEM_read_bio_PUBKEY() fails if a PEM line happens to be
// a multiple of 253 — e.g. 1012 characters or 2024 characters in base64.
// https://github.com/openssl/openssl/issues/9187
return verifier.verify(breakLines(80, this.toString("pem")), signature)
}
Certificate.prototype.hasIssued = function(cert) {
// "ESTEID-SK 2007" issued with RSA-SHA1.
// "ESTEID-SK 2011" issued with RSA-SHA1.
// "ESTEID-SK 2011" from ~2015 issued with RSA-SHA256.
// "EID-SK 2011" issued with RSA-SHA1.
// "EID-SK 2011" from ~2015 issued with RSA-SHA256.
// "ESTEID-SK 2015" issued with RSA-SHA256
// "ESTEID2018" issued with ECDSA-SHA256
var algoName = cert.issuerSignatureAlgorithmName
var hashName = SIGNATURE_HASH_ALGORITHMS[algoName]
if (!hashName) throw new Error("Unsupported signature algorithm: " + algoName)
// tbsCertificate.signature MUST contain the same algorithm identifier as the
// signatureAlgorithm field in the sequence Certificate.
// https://tools.ietf.org/html/rfc5280#section-4.1.2.3
if (!_.deepEquals(
cert.asn.tbsCertificate.signature,
cert.asn.signatureAlgorithm
)) return false
// DER support in verification arrived in Node v11.6. Before that PEM only.
var der = X509Asn.TBSCertificate.encode(cert.asn.tbsCertificate)
var verifier = Crypto.createVerify(hashName).update(der)
var pem = breakLines(80, this.toString("pem"))
return verifier.verify(pem, cert.asn.signature.data)
}
Certificate.prototype.toBuffer = function() {
return X509Asn.Certificate.encode(this.asn)
}
Certificate.prototype.toString = function(fmt) {
switch (fmt) {
case "hex":
case "base64": return this.toBuffer().toString(fmt)
case "pem": return Pem.serialize("CERTIFICATE", this.toBuffer())
default: throw new RangeError("Unsupported format: " + fmt)
}
}
exports.parse = function(derOrPem) {
var der = Pem.isPem(derOrPem) ? Pem.parse(derOrPem) : derOrPem
return new Certificate(der)
}
function serializeSubject(seq) {
return seq.value.map((names) => names.reduce(function(obj, name) {
var oid = name.type, value = name.value
if (LdapAttributes.has(oid))
obj[LdapAttributes.get(oid).name] = LdapAttributes.parse(oid, value)
else
obj[oid.join(".")] = value
return obj
}, {})
)
}
function serializeDistinguishedName(seq) {
return seq.value.map((names) => names.map(function(name) {
var oid = name.type, value = name.value
return LdapAttributes.has(oid)
? LdapAttributes.serializeKv(oid, LdapAttributes.parse(oid, value))
: LdapAttributes.serializeUnknownKv(oid, value)
}).join("+")
)
}
function serializeRfc4514Name(seq) {
return seq.value.map((names) => names.map(function(name) {
var oid = name.type, value = name.value
return rfc4514Attrs.has(oid)
? rfc4514Attrs.serializeKv(oid, LdapAttributes.parse(oid, value))
: LdapAttributes.serializeUnknownKv(oid, value)
}).join("+")).join(",")
}
function parseTime(obj) {
switch (obj.type) {
case "utcTime":
case "genTime": return new Date(obj.value)
default: throw new RangeError("Invalid time type: " + obj.type)
}
}
function breakLines(length, text) {
var wrapped = new RegExp(`(.{1,${length}})`, "g")
var lines = flatten(text.split("\n").map((line) => line.match(wrapped) || ""))
return lines.join("\n")
}