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.
626 lines (519 loc) • 18.3 kB
JavaScript
var _ = require("../../lib/underscore")
var Fs = require("fs")
var Asn = require("../../lib/asn")
var X509Asn = require("../../lib/x509_asn")
var Crypto = require("crypto")
var Certificate = require("../../lib/certificate")
var LdapAttributes = require("../../lib/ldap_attributes")
var Pem = require("../../lib/pem")
var demand = require("must")
var OCSP_URL_OID = require("../../lib/x509_asn").OCSP_URL
var EMPTY_ARR = Array.prototype
var EMPTY_BUFFER = Buffer.alloc(0)
var GENERATE_KEYS = !!Crypto.generateKeyPairSync
var HASH_ALGORITHMS = ["sha1", "sha224", "sha256", "sha384", "sha512"]
var NO_PARAMS = Buffer.from("0500", "hex")
var nextSerialNumber = Math.floor(10000 * Math.random())
describe("Certificate", function() {
describe(".prototype.serialNumber", function() {
it("must return the serial number", function() {
var cert = new Certificate(newCertificate({serialNumber: 42}))
cert.serialNumber.toNumber().must.eql(42)
})
})
function mustHaveSubject(prop) {
it("must serialize name with multiple relative parts", function() {
var cert = new Certificate(newCertificate({
[prop]: [
{countryName: "EE"},
{organizationName: "AS Foo"},
{organizationalUnitName: "Bars", localityName: "Tallinn"}
]
}))
cert[prop].must.eql([
{countryName: "EE"},
{organizationName: "AS Foo"},
{organizationalUnitName: "Bars", localityName: "Tallinn"}
])
})
it("must serialize unknown attribute as OID", function() {
var cert = new Certificate(newCertificate({
[prop]: [
{countryName: "EE"},
{"2.5.4.1337": "Hello, world"}
]
}))
cert[prop].must.eql([
{countryName: "EE"},
{"2.5.4.1337": encodeUtf8("Hello, world")}
])
})
}
function mustHaveSubjectRfc4514Name(prop) {
it("must serialize name with multiple relative parts", function() {
var cert = new Certificate(newCertificate({
[prop]: [
{countryName: "EE"},
{organizationName: "AS Foo"},
{organizationalUnitName: "Bars", localityName: "Tallinn"}
]
}))
cert[prop + "Rfc4514Name"].must.equal("C=EE,O=AS Foo,OU=Bars+L=Tallinn")
})
_.each({
userId: "UID",
domainComponent: "DC",
commonName: "CN",
countryName: "C",
localityName: "L",
stateOrProvinceName: "ST",
streetAddress: "STREET",
organizationName: "O",
organizationalUnitName: "OU",
}, function(shortName, name) {
it(`must serialize ${name} as string`, function() {
var cert = new Certificate(newCertificate({[prop]: [{[name]: "foo"}]}))
cert[prop + "Rfc4514Name"].must.equal(`${shortName}=foo`)
})
})
it("must serialize givenName as DirectoryString in hex", function() {
var cert = new Certificate(newCertificate({
[prop]: [{givenName: "John Smith"}]
}))
var oid = LdapAttributes.get("givenName").oid
var value = Asn.DirectoryString.encode({
type: "utf8String",
value: "John Smith"
})
var dn = `${oid.join(".")}=#${value.toString("hex")}`
cert[prop + "Rfc4514Name"].must.equal(dn)
})
it("must serialize emailAddress as Ia5String in hex", function() {
var cert = new Certificate(newCertificate({
[prop]: [{emailAddress: "user@example.com"}]
}))
var oid = LdapAttributes.get("emailAddress").oid
var value = Asn.Ia5String.encode("user@example.com")
var dn = `${oid.join(".")}=#${value.toString("hex")}`
cert[prop + "Rfc4514Name"].must.equal(dn)
})
}
describe(".prototype.subject", function() {
mustHaveSubject("subject")
})
describe(".prototype.subjectRfc4514Name", function() {
mustHaveSubjectRfc4514Name("subject")
})
describe(".prototype.issuer", function() {
mustHaveSubject("issuer")
})
describe(".prototype.issuerRfc4514Name", function() {
mustHaveSubjectRfc4514Name("issuer")
})
function mustHaveValidity(prop) {
it("must parse utcTime", function() {
var cert = new Certificate(newCertificate({
[prop]: {type: "utcTime", value: Date.UTC(2015, 5, 18, 13, 37, 42)}
}))
cert[prop].must.eql(new Date(Date.UTC(2015, 5, 18, 13, 37, 42)))
})
it("must parse genTime", function() {
var cert = new Certificate(newCertificate({
[prop]: {type: "genTime", value: Date.UTC(2015, 5, 18, 13, 37, 42)}
}))
cert[prop].must.eql(new Date(Date.UTC(2015, 5, 18, 13, 37, 42)))
})
}
describe(".prototype.validFrom", function() {
mustHaveValidity("validFrom")
})
describe(".prototype.validUntil", function() {
mustHaveValidity("validUntil")
})
describe(".prototype.ocspUrl", function() {
it("must return null if no OCSP URL extension", function() {
var cert = new Certificate(newCertificate())
demand(cert.ocspUrl).be.null()
})
it("must return OCSP URL", function() {
var cert = new Certificate(newCertificate({
extensions: [{
extnID: "authorityInformationAccess",
extnValue: [{
accessMethod: OCSP_URL_OID,
accessLocation: {
type: "uniformResourceIdentifier",
value: "http://example.com/ocsp"
}
}]
}]
}))
cert.ocspUrl.must.equal("http://example.com/ocsp")
})
})
describe(".prototype.hasSigned", function() {
// https://github.com/openssl/openssl/issues/9187
it("must verify PEM with line length multiple of 253", function() {
var key = newRsaKeys()
var cert = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(key.publicKey),
extensions: [{
extnID: "authorityInformationAccess",
extnValue: [{
accessMethod: OCSP_URL_OID,
accessLocation: {
type: "uniformResourceIdentifier",
value: "http://example.com/" + new Array(318).join("x")
}
}]
}]
}))
var max = _.max(cert.toString("pem").split("\n").map((l) => l.length))
max.must.equal(253 * 4)
var signable = "Hello, world"
var signer = Crypto.createSign("sha256")
var signature = signer.update(signable).sign(key.privateKey)
cert.hasSigned(signable, signature).must.be.true()
})
describe("given an RSA certificate", function() {
beforeEach(function() {
this.rsa = newRsaKeys()
this.certificate = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(this.rsa.publicKey)
}))
})
it("return true for the correct signature", function() {
var signable = "Hello, world"
var signer = Crypto.createSign("sha256")
var signature = signer.update(signable).sign(this.rsa.privateKey)
this.certificate.hasSigned(signable, signature).must.be.true()
})
it("must return false for the wrong signature", function() {
var signable = "Hello, world"
var signer = Crypto.createSign("sha256")
var signature = signer.update(signable).sign(this.rsa.privateKey)
signature[10] = ~signature[10]
this.certificate.hasSigned(signable, signature).must.be.false()
})
})
describe("given a ECDSA signature", function() {
beforeEach(function() {
this.ec = GENERATE_KEYS
? Crypto.generateKeyPairSync("ec", {
namedCurve: "prime256v1",
privateKeyEncoding: {type: "pkcs8", format: "pem"},
publicKeyEncoding: {type: "spki", format: "der"}
})
: readKeyPairSync(
__dirname + "/../fixtures/ecdsa.key",
__dirname + "/../fixtures/ecdsa.pub"
)
this.certificate = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(this.ec.publicKey)
}))
})
it("must return true for the correct signature", function() {
var signable = "Hello, world"
var signer = Crypto.createSign("sha256")
var signatureDer = signer.update(signable).sign(this.ec.privateKey)
var signatureAsn = X509Asn.EcSignature.decode(signatureDer)
this.certificate.hasSigned(signable, Buffer.concat([
signatureAsn.r.toBuffer("be", 32),
signatureAsn.s.toBuffer("be", 32)
])).must.be.true()
})
it("must return false for the wrong signature", function() {
var signable = "Hello, world"
var signer = Crypto.createSign("sha256")
var signatureDer = signer.update(signable).sign(this.ec.privateKey)
var signatureAsn = X509Asn.EcSignature.decode(signatureDer)
var signature = Buffer.concat([
signatureAsn.r.toBuffer("be", 32),
signatureAsn.s.toBuffer("be", 32)
])
signature[10] = ~signature[10]
this.certificate.hasSigned(signable, signature).must.be.false()
})
})
})
describe(".prototype.hasIssued", function() {
it("must return false if tbsCertificate.signature algorithm mismatch",
function() {
var rsa = newRsaKeys()
var issuer = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(rsa.publicKey)
}))
var unsignedCertificate = newUnsignedCertificate({
signatureAlgorithm: {
algorithm: X509Asn.RSA_SHA512,
parameters: NO_PARAMS
}
})
var der = X509Asn.TBSCertificate.encode(unsignedCertificate)
var signer = Crypto.createSign("sha256")
var signature = signer.update(der).sign(rsa.privateKey)
var certificate = new Certificate(newCertificate({
certificate: unsignedCertificate,
signatureAlgorithm: {
algorithm: X509Asn.RSA_SHA256,
parameters: NO_PARAMS
},
signature: {unused: 0, data: signature}
}))
issuer.hasIssued(certificate).must.be.false()
})
it("must return false if tbsCertificate.signature parameter mismatch",
function() {
var ec = GENERATE_KEYS
? Crypto.generateKeyPairSync("ec", {
namedCurve: "prime256v1",
privateKeyEncoding: {type: "pkcs8", format: "pem"},
publicKeyEncoding: {type: "spki", format: "der"}
})
: readKeyPairSync(
__dirname + "/../fixtures/ecdsa.key",
__dirname + "/../fixtures/ecdsa.pub"
)
var issuer = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(ec.publicKey)
}))
var unsignedCertificate = newUnsignedCertificate({
signatureAlgorithm: {
algorithm: X509Asn.ECDSA_SHA256,
parameters: X509Asn.EcParameters.encode({
type: "namedCurve",
value: "prime192v1"
})
}
})
var der = X509Asn.TBSCertificate.encode(unsignedCertificate)
var signer = Crypto.createSign("sha256")
var signature = signer.update(der).sign(ec.privateKey)
var certificate = new Certificate(newCertificate({
certificate: unsignedCertificate,
signatureAlgorithm: {
algorithm: X509Asn.ECDSA_SHA256,
parameters: X509Asn.EcParameters.encode({
type: "namedCurve",
value: "prime256v1"
})
},
signature: {unused: 0, data: signature}
}))
issuer.hasIssued(certificate).must.be.false()
})
// https://github.com/openssl/openssl/issues/9187
it("must verify PEM with line length multiple of 253", function() {
var key = newRsaKeys()
var issuer = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(key.publicKey),
extensions: [{
extnID: "authorityInformationAccess",
extnValue: [{
accessMethod: OCSP_URL_OID,
accessLocation: {
type: "uniformResourceIdentifier",
value: "http://example.com/" + new Array(318).join("x")
}
}]
}]
}))
var max = _.max(issuer.toString("pem").split("\n").map((l) => l.length))
max.must.equal(253 * 4)
var signatureAlgorithm = {
algorithm: X509Asn.SIGNATURE_OIDS["rsa-sha256"],
parameters: NO_PARAMS
}
var unsignedCertificate = newUnsignedCertificate({
signatureAlgorithm: signatureAlgorithm
})
var der = X509Asn.TBSCertificate.encode(unsignedCertificate)
var signer = Crypto.createSign("sha256")
var signature = signer.update(der).sign(key.privateKey)
var certificate = new Certificate(newCertificate({
certificate: unsignedCertificate,
signatureAlgorithm: signatureAlgorithm,
signature: {unused: 0, data: signature}
}))
issuer.hasIssued(certificate).must.be.true()
})
describe("given an RSA certificate", function() {
beforeEach(function() {
this.rsa = newRsaKeys()
this.issuer = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(this.rsa.publicKey)
}))
})
HASH_ALGORITHMS.forEach(function(hashName) {
it(`return true for the correct signature on ${hashName}`, function() {
var signatureAlgorithm = {
algorithm: X509Asn.SIGNATURE_OIDS["rsa-" + hashName],
parameters: NO_PARAMS
}
var unsignedCertificate = newUnsignedCertificate({
signatureAlgorithm: signatureAlgorithm
})
var der = X509Asn.TBSCertificate.encode(unsignedCertificate)
var signer = Crypto.createSign(hashName)
var signature = signer.update(der).sign(this.rsa.privateKey)
var certificate = new Certificate(newCertificate({
certificate: unsignedCertificate,
signatureAlgorithm: signatureAlgorithm,
signature: {unused: 0, data: signature}
}))
this.issuer.hasIssued(certificate).must.be.true()
})
it(`must return false for the wrong signature on ${hashName}`,
function() {
var signatureAlgorithm = {
algorithm: X509Asn.SIGNATURE_OIDS["rsa-" + hashName],
parameters: NO_PARAMS
}
var unsignedCertificate = newUnsignedCertificate({
signatureAlgorithm: signatureAlgorithm
})
var der = X509Asn.TBSCertificate.encode(unsignedCertificate)
var signer = Crypto.createSign(hashName)
var signature = signer.update(der).sign(this.rsa.privateKey)
signature[10] = ~signature[10]
var certificate = new Certificate(newCertificate({
certificate: unsignedCertificate,
signatureAlgorithm: signatureAlgorithm,
signature: {unused: 0, data: signature}
}))
this.issuer.hasIssued(certificate).must.be.false()
})
})
})
describe("given a ECDSA signature", function() {
beforeEach(function() {
this.ec = GENERATE_KEYS
? Crypto.generateKeyPairSync("ec", {
namedCurve: "prime256v1",
privateKeyEncoding: {type: "pkcs8", format: "pem"},
publicKeyEncoding: {type: "spki", format: "der"}
})
: readKeyPairSync(
__dirname + "/../fixtures/ecdsa.key",
__dirname + "/../fixtures/ecdsa.pub"
)
this.issuer = new Certificate(newCertificate({
publicKey: X509Asn.SubjectPublicKeyInfo.decode(this.ec.publicKey)
}))
})
HASH_ALGORITHMS.forEach(function(hashName) {
it(`return true for the correct signature on ${hashName}`, function() {
var signatureAlgorithm = {
algorithm: X509Asn.SIGNATURE_OIDS["ecdsa-" + hashName],
parameters: NO_PARAMS
}
var unsignedCertificate = newUnsignedCertificate({
signatureAlgorithm: signatureAlgorithm
})
var der = X509Asn.TBSCertificate.encode(unsignedCertificate)
var signer = Crypto.createSign(hashName)
var signature = signer.update(der).sign(this.ec.privateKey)
var certificate = new Certificate(newCertificate({
certificate: unsignedCertificate,
signatureAlgorithm: signatureAlgorithm,
signature: {unused: 0, data: signature}
}))
this.issuer.hasIssued(certificate).must.be.true()
})
it(`must return false for the wrong signature on ${hashName}`,
function() {
var signatureAlgorithm = {
algorithm: X509Asn.SIGNATURE_OIDS["ecdsa-" + hashName],
parameters: NO_PARAMS
}
var unsignedCertificate = newUnsignedCertificate({
signatureAlgorithm: signatureAlgorithm
})
var der = X509Asn.TBSCertificate.encode(unsignedCertificate)
var signer = Crypto.createSign(hashName)
var signature = signer.update(der).sign(this.ec.privateKey)
signature[10] = ~signature[10]
var certificate = new Certificate(newCertificate({
certificate: unsignedCertificate,
signatureAlgorithm: signatureAlgorithm,
signature: {unused: 0, data: signature}
}))
this.issuer.hasIssued(certificate).must.be.false()
})
})
})
})
describe(".prototype.toBuffer", function() {
it("must encode back to DER", function() {
var der = newCertificate()
new Certificate(der).toBuffer().must.eql(der)
})
})
})
function newCertificate(opts) {
return X509Asn.Certificate.encode({
tbsCertificate: opts && opts.certificate || newUnsignedCertificate(opts),
signatureAlgorithm: opts && opts.signatureAlgorithm || {
algorithm: X509Asn.RSA_SHA256,
parameters: NO_PARAMS
},
signature: opts && opts.signature || {unused: 0, data: EMPTY_BUFFER}
})
}
function newUnsignedCertificate(opts) {
var extensions = opts && opts.extensions
var subject = opts && opts.subject && serializeSubject(opts.subject)
var issuer = opts && opts.issuer && serializeSubject(opts.issuer)
var defaultSignatureAlgorithm = {
algorithm: X509Asn.RSA_SHA256,
parameters: NO_PARAMS
}
return {
serialNumber: opts && opts.serialNumber || nextSerialNumber++,
subject: {type: "rdnSequence", value: subject || EMPTY_ARR},
issuer: {type: "rdnSequence", value: issuer || EMPTY_ARR},
signature: opts && opts.signatureAlgorithm || defaultSignatureAlgorithm,
validity: {
notBefore: opts && opts.validFrom || {type: "utcTime", value: new Date},
notAfter: opts && opts.validUntil || {type: "utcTime", value: new Date}
},
subjectPublicKeyInfo: opts && opts.publicKey || {
algorithm: defaultSignatureAlgorithm,
subjectPublicKey: {unused: 0, data: EMPTY_BUFFER}
},
extensions: extensions
}
}
function newRsaKeys() {
return GENERATE_KEYS
? Crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
privateKeyEncoding: {type: "pkcs8", format: "pem"},
publicKeyEncoding: {type: "spki", format: "der"}
})
: readKeyPairSync(
__dirname + "/../fixtures/rsa.key",
__dirname + "/../fixtures/rsa.pub"
)
}
function serializeSubject(relativeNames) {
return relativeNames.map((names) => _.map(names, function(value, name) {
if (LdapAttributes.has(name)) return {
type: LdapAttributes.get(name).oid,
value: LdapAttributes.serialize(name, value)
}
else return {
type: name.split(/\./g).map(Number),
value: encodeUtf8(value)
}
}))
}
function readKeyPairSync(keyPath, pubPath) {
return {
privateKey: Fs.readFileSync(keyPath, "utf8"),
publicKey: Pem.parse(Fs.readFileSync(pubPath, "utf8"))
}
}
function encodeUtf8(value) {
return X509Asn.DirectoryString.encode({type: "utf8String", value: value})
}