From 7cbc3ea5f1615573747975b37c728a3496329fd6 Mon Sep 17 00:00:00 2001 From: Jeschke Date: Tue, 27 Nov 2018 17:43:24 +0100 Subject: [PATCH 1/3] first version of CSR implementation --- src/index.ts | 2 +- src/x509.ts | 342 +++++++++++++++++++++++++++++++++------------ test/cert/test.csr | 8 ++ test/x509.ts | 8 +- 4 files changed, 266 insertions(+), 94 deletions(-) create mode 100644 test/cert/test.csr diff --git a/src/index.ts b/src/index.ts index 806c08e..53b7f75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,4 @@ export { bytesFromIP, bytesToIP, getOID, getOIDName } from './common' export { PublicKey, PrivateKey, RSAPublicKey, RSAPrivateKey, Verifier, Signer } from './pki' -export { Certificate, DistinguishedName, Extension, Attribute } from './x509' +export { Certificate, CertificateSigningRequest, DistinguishedName, Extension, Attribute } from './x509' diff --git a/src/x509.ts b/src/x509.ts index 6a98c2d..4ecfd2a 100644 --- a/src/x509.ts +++ b/src/x509.ts @@ -158,6 +158,103 @@ const x509CertificateValidator: Template = { capture: 'certSignature', }], } +// validator for a x509 CSR +const x509CertificateSigningRequestValidator = { + name: 'CertificateSigningRequest', + class: Class.UNIVERSAL, + tag: Tag.SEQUENCE, + value: [{ + name: 'Certificate.TBSCertificate', + class: Class.UNIVERSAL, + tag: Tag.SEQUENCE, + capture: 'tbsCertificate', + value: [{ + name: 'Certificate.TBSCertificate.version', + class: Class.CONTEXT_SPECIFIC, + tag: Tag.NONE, + optional: true, + value: [{ + name: 'Certificate.TBSCertificate.version.integer', + class: Class.UNIVERSAL, + tag: Tag.INTEGER, + capture: 'certVersion', + }], + }, { + name: 'Certificate.TBSCertificate.serialNumber', + class: Class.UNIVERSAL, + tag: Tag.INTEGER, + capture: 'certSerialNumber', + }, { + name: 'Certificate.TBSCertificate.issuer', + class: Class.UNIVERSAL, + tag: Tag.SEQUENCE, + capture: 'certIssuer', + }, { + // Name (subject) (RDNSequence) + name: 'Certificate.TBSCertificate.subject', + class: Class.UNIVERSAL, + tag: Tag.SEQUENCE, + capture: 'certSubject', + }, { + // subjectUniqueID (optional) + name: 'Certificate.TBSCertificate.subjectUniqueID', + class: Class.CONTEXT_SPECIFIC, + tag: Tag.INTEGER, + optional: true, + value: [{ + name: 'Certificate.TBSCertificate.subjectUniqueID.id', + class: Class.UNIVERSAL, + tag: Tag.BITSTRING, + capture: 'certSubjectUniqueId', + }], + }, { + // Extensions (optional) + name: 'Certificate.TBSCertificate.extensionRequest', + class: Class.CONTEXT_SPECIFIC, + tag: Tag.NONE, + value: [{ + name: 'Certificate.TBSCertificate.extensionRequest', + class: Class.UNIVERSAL, + tag: Tag.SEQUENCE, + value: [{ + name: 'Certificate.TBSCertificate.extensionRequestt', + class: Class.UNIVERSAL, + tag: Tag.OID, + }, + { + name: 'Certificate.TBSCertificate.extensionRequest', + class: Class.UNIVERSAL, + tag: Tag.SET, + capture: 'certExtensions', + }] + }], + optional: true, + }], + }, { + // AlgorithmIdentifier (signature algorithm) + name: 'Certificate.signatureAlgorithm', + class: Class.UNIVERSAL, + tag: Tag.SEQUENCE, + value: [{ + // algorithm + name: 'Certificate.signatureAlgorithm.algorithm', + class: Class.UNIVERSAL, + tag: Tag.OID, + capture: 'certSignatureOID', + }, { + name: 'Certificate.TBSCertificate.signature.parameters', + class: Class.UNIVERSAL, + tag: Tag.OCTETSTRING, + optional: true, + capture: 'certSignatureParams', + }], + }, { + name: 'Certificate.signatureValue', + class: Class.UNIVERSAL, + tag: Tag.BITSTRING, + capture: 'certSignature', + }], +}; /** * Attribute for X.509v3 certificate. @@ -257,55 +354,15 @@ export class DistinguishedName { } } -/** - * X.509v3 Certificate. - */ -export class Certificate { - /** - * Parse one or more X.509 certificates from PEM formatted buffer. - * If there is no certificate, it will throw error. - * @param data PEM formatted buffer - */ - static fromPEMs (data: Buffer): Certificate[] { - const certs = [] - const pems = PEM.parse(data) - - for (const pem of pems) { - if (pem.type !== 'CERTIFICATE' && - pem.type !== 'X509 CERTIFICATE' && - pem.type !== 'TRUSTED CERTIFICATE') { - throw new Error('Could not convert certificate from PEM: invalid type') - } - if (pem.procType.includes('ENCRYPTED')) { - throw new Error('Could not convert certificate from PEM: PEM is encrypted.') - } - - const obj = ASN1.fromDER(pem.body) - certs.push(new Certificate(obj)) - } - if (certs.length === 0) { - throw new Error('No Certificate') - } - return certs - } - - /** - * Parse an X.509 certificate from PEM formatted buffer. - * @param data PEM formatted buffer - */ - static fromPEM (data: Buffer): Certificate { - return Certificate.fromPEMs(data)[0] - } - +export abstract class X509 { + readonly captures: Captures readonly raw: Buffer readonly version: number readonly serialNumber: string readonly signatureOID: string readonly signatureAlgorithm: string - readonly infoSignatureOID: string readonly signature: Buffer readonly subjectKeyIdentifier: string - readonly authorityKeyIdentifier: string readonly ocspServer: string readonly issuingCertificateURL: string readonly isCA: boolean @@ -316,54 +373,35 @@ export class Certificate { readonly emailAddresses: string[] readonly ipAddresses: string[] readonly uris: string[] - readonly validFrom: Date - readonly validTo: Date readonly issuer: DistinguishedName - readonly subject: DistinguishedName readonly extensions: Extension[] - readonly publicKey: PublicKey - readonly publicKeyRaw: Buffer readonly tbsCertificate: ASN1 + readonly subject: DistinguishedName - /** - * Creates an X.509 certificate from an ASN.1 object - * @param obj an ASN.1 object - */ - constructor (obj: ASN1) { + constructor(validator: Template, obj: ASN1) { + this.captures = Object.create(null) as Captures // validate certificate and capture data - const captures: Captures = Object.create(null) - const err = obj.validate(x509CertificateValidator, captures) + const err = obj.validate(validator, this.captures) if (err != null) { throw new Error('Cannot read X.509 certificate: ' + err.message) } this.raw = obj.DER - this.version = captures.certVersion == null ? 0 : (ASN1.parseIntegerNum(captures.certVersion.bytes) + 1) - this.serialNumber = ASN1.parseIntegerStr(captures.certSerialNumber.bytes) - this.signatureOID = ASN1.parseOID(captures.certSignatureOID.bytes) + this.version = this.captures.certVersion == null ? 0 : (ASN1.parseIntegerNum(this.captures.certVersion.bytes) + 1) + this.serialNumber = ASN1.parseIntegerStr(this.captures.certSerialNumber.bytes) + this.signatureOID = ASN1.parseOID(this.captures.certSignatureOID.bytes) this.signatureAlgorithm = getOIDName(this.signatureOID) - this.infoSignatureOID = ASN1.parseOID(captures.certinfoSignatureOID.bytes) - this.signature = ASN1.parseBitString(captures.certSignature.bytes).buf - - this.validFrom = ASN1.parseTime(captures.certValidityNotBefore.tag, captures.certValidityNotBefore.bytes) - this.validTo = ASN1.parseTime(captures.certValidityNotAfter.tag, captures.certValidityNotAfter.bytes) + this.signature = ASN1.parseBitString(this.captures.certSignature.bytes).buf this.issuer = new DistinguishedName() - this.issuer.setAttrs(RDNAttributesAsArray(captures.certIssuer)) - if (captures.certIssuerUniqueId != null) { - this.issuer.uniqueId = ASN1.parseBitString(captures.certIssuerUniqueId.bytes) - } - - this.subject = new DistinguishedName() - this.subject.setAttrs(RDNAttributesAsArray(captures.certSubject)) - if (captures.certSubjectUniqueId != null) { - this.subject.uniqueId = ASN1.parseBitString(captures.certSubjectUniqueId.bytes) + this.issuer.setAttrs(RDNAttributesAsArray(this.captures.certIssuer)) + if (this.captures.certIssuerUniqueId != null) { + this.issuer.uniqueId = ASN1.parseBitString(this.captures.certIssuerUniqueId.bytes) } this.extensions = [] this.subjectKeyIdentifier = '' - this.authorityKeyIdentifier = '' this.ocspServer = '' this.issuingCertificateURL = '' this.isCA = false @@ -374,15 +412,12 @@ export class Certificate { this.emailAddresses = [] this.ipAddresses = [] this.uris = [] - if (captures.certExtensions != null) { - this.extensions = certificateExtensionsFromAsn1(captures.certExtensions) + if (this.captures.certExtensions != null) { + this.extensions = certificateExtensionsFromAsn1(this.captures.certExtensions) for (const ext of this.extensions) { if (typeof ext.subjectKeyIdentifier === 'string') { this.subjectKeyIdentifier = ext.subjectKeyIdentifier } - if (typeof ext.authorityKeyIdentifier === 'string') { - this.authorityKeyIdentifier = ext.authorityKeyIdentifier - } if (typeof ext.authorityInfoAccessOcsp === 'string') { this.ocspServer = ext.authorityInfoAccessOcsp } @@ -417,9 +452,17 @@ export class Certificate { } } - this.publicKey = new PublicKey(captures.publicKeyInfo) - this.publicKeyRaw = this.publicKey.toDER() - this.tbsCertificate = captures.tbsCertificate + this.subject = new DistinguishedName() + try { + this.subject.setAttrs(RDNAttributesAsArray(this.captures.certSubject)) + if (this.captures.certSubjectUniqueId != null) { + this.subject.uniqueId = ASN1.parseBitString(this.captures.certSubjectUniqueId.bytes) + } + } catch (e) { + console.debug("Could not read cert subject: " + e.message); + } + + this.tbsCertificate = this.captures.tbsCertificate } /** @@ -455,6 +498,97 @@ export class Certificate { return null } + /** + * Return a friendly JSON object for debuging. + */ + toJSON (): any { + const obj = {} as any + for (const key of Object.keys(this)) { + obj[key] = toJSONify((this as any)[key]) + } + delete obj.tbsCertificate + return obj + } + + protected [inspect.custom] (_depth: any, options: any): string { + if (options.depth <= 2) { + options.depth = 10 + } + return `<${this.constructor.name} ${inspect(this.toJSON(), options)}>` + } +} + +/** + * X.509v3 Certificate. + */ +export class Certificate extends X509 { + /** + * Parse one or more X.509 certificates from PEM formatted buffer. + * If there is no certificate, it will throw error. + * @param data PEM formatted buffer + */ + static fromPEMs (data: Buffer): Certificate[] { + const certs = [] + const pems = PEM.parse(data) + + for (const pem of pems) { + if (pem.type !== 'CERTIFICATE' && + pem.type !== 'X509 CERTIFICATE' && + pem.type !== 'TRUSTED CERTIFICATE') { + throw new Error('Could not convert certificate from PEM: invalid type') + } + if (pem.procType.includes('ENCRYPTED')) { + throw new Error('Could not convert certificate from PEM: PEM is encrypted.') + } + + const obj = ASN1.fromDER(pem.body) + certs.push(new Certificate(obj)) + } + if (certs.length === 0) { + throw new Error('No Certificate') + } + return certs + } + + /** + * Parse an X.509 certificate from PEM formatted buffer. + * @param data PEM formatted buffer + */ + static fromPEM (data: Buffer): Certificate { + return Certificate.fromPEMs(data)[0] + } + + readonly infoSignatureOID: string + readonly authorityKeyIdentifier: string + readonly validFrom: Date + readonly validTo: Date + readonly publicKey: PublicKey + readonly publicKeyRaw: Buffer + + /** + * Creates an X.509 certificate from an ASN.1 object + * @param obj an ASN.1 object + */ + constructor (obj: ASN1) { + super(x509CertificateValidator, obj); + + this.infoSignatureOID = ASN1.parseOID(this.captures.certinfoSignatureOID.bytes) + + this.authorityKeyIdentifier = '' + + this.validFrom = ASN1.parseTime(this.captures.certValidityNotBefore.tag, this.captures.certValidityNotBefore.bytes) + this.validTo = ASN1.parseTime(this.captures.certValidityNotAfter.tag, this.captures.certValidityNotAfter.bytes) + + for (const ext of this.extensions) { + if (typeof ext.authorityKeyIdentifier === 'string') { + this.authorityKeyIdentifier = ext.authorityKeyIdentifier + } + } + + this.publicKey = new PublicKey(this.captures.publicKeyInfo) + this.publicKeyRaw = this.publicKey.toDER() + } + /** * Returns null if a subject certificate is valid, or error if invalid. * Note that it does not check validity time, DNS name, ip or others. @@ -508,24 +642,48 @@ export class Certificate { const ski = this.publicKey.getFingerprint('sha1', 'PublicKey') return ski.toString('hex') === this.subjectKeyIdentifier } +} +/** + * X.509v3 Certificate. + */ +export class CertificateSigningRequest extends X509 { /** - * Return a friendly JSON object for debuging. + * Parse one or more X.509 certificates from PEM formatted buffer. + * If there is no certificate, it will throw error. + * @param data PEM formatted buffer */ - toJSON (): any { - const obj = {} as any - for (const key of Object.keys(this)) { - obj[key] = toJSONify((this as any)[key]) + static fromPEMs (data: Buffer): CertificateSigningRequest[] { + const certRequests = [] + const pems = PEM.parse(data) + + for (const pem of pems) { + if (pem.type !== 'CERTIFICATE REQUEST') { + throw new Error('Could not convert certificate signing request from PEM: invalid type') + } + + const obj = ASN1.fromDER(pem.body) + certRequests.push(new CertificateSigningRequest(obj)) } - delete obj.tbsCertificate - return obj + if (certRequests.length === 0) { + throw new Error('No Certificate request') + } + return certRequests } - protected [inspect.custom] (_depth: any, options: any): string { - if (options.depth <= 2) { - options.depth = 10 - } - return `<${this.constructor.name} ${inspect(this.toJSON(), options)}>` + /** + * Parse an X.509 certificate signing request from PEM formatted buffer. + * @param data PEM formatted buffer + */ + static fromPEM (data: Buffer): CertificateSigningRequest { + return CertificateSigningRequest.fromPEMs(data)[0] + } + /** + * Creates an X.509 certificate signing from an ASN.1 object + * @param obj an ASN.1 object + */ + constructor (obj: ASN1) { + super(x509CertificateSigningRequestValidator, obj); } } diff --git a/test/cert/test.csr b/test/cert/test.csr new file mode 100644 index 0000000..fb3533b --- /dev/null +++ b/test/cert/test.csr @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBHDCBwwIBADATMREwDwYDVQQDDAhibHViLmNvbTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABHR0vVhyg30lpmiYlWf9ZNw6QD425SyZklX2hT6GV259IIo3VaoM +epb9t1GtusVHImZEuGWqRZk6iJYuDoGDIe2gTjBMBgkqhkiG9w0BCQ4xPzA9MAwG +A1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBSUSskMgPf+zkJl +n18VdW5rm7gpyzAKBggqhkjOPQQDAgNIADBFAiAGOxeWk17j5KPosP5sSaQpU+EH +Nnyf8RXn7MkM8IIKHQIhAPeE7iPdGHVed0u8zKjIeZZzdRH2ocY1xuw+PuXeFKOT +-----END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/test/x509.ts b/test/x509.ts index 3fa48db..6a205ff 100644 --- a/test/x509.ts +++ b/test/x509.ts @@ -7,7 +7,7 @@ import fs from 'fs' import { strictEqual, deepEqual, ok } from 'assert' import { suite, it } from 'tman' import { PEM } from '@fidm/asn1' -import { Certificate, RSAPublicKey, PrivateKey } from '../src/index' +import { Certificate, CertificateSigningRequest, RSAPublicKey, PrivateKey } from '../src/index' suite('X509', function () { it('should work for github certificate', function () { @@ -139,6 +139,12 @@ suite('X509', function () { ok(cert.publicKey.verify(data, signature, 'sha256')) }) + it('should support CSRs', function () { + const csr = CertificateSigningRequest.fromPEM(fs.readFileSync('./test/cert/test.csr')) + strictEqual(csr.getExtension("keyUsage").digitalSignature, true) + strictEqual(csr.extensions.length, 3) + }) + // it.skip('should support Mozilla\'s publicly trusted list of CAs', function () { // const certs = Certificate.fromPEMs(fs.readFileSync('./test/cert/mozilla-publicly-certs.pem')) // for (const cert of certs) { From d33fab20b88b99fc0282999039012e531823edd2 Mon Sep 17 00:00:00 2001 From: Jeschke Date: Tue, 27 Nov 2018 18:00:14 +0100 Subject: [PATCH 2/3] added test for simple CSR increased version --- package.json | 2 +- src/x509.ts | 1 + test/cert/csr1.pem | 17 +++++++++++++++++ test/cert/{test.csr => csr2.pem} | 0 test/x509.ts | 7 ++++++- 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 test/cert/csr1.pem rename test/cert/{test.csr => csr2.pem} (100%) diff --git a/package.json b/package.json index 390d662..0444767 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "authors": [ "Yan Qing " ], - "version": "1.2.0", + "version": "1.3.0", "main": "build/index.js", "types": "build/index.d.ts", "license": "MIT", diff --git a/src/x509.ts b/src/x509.ts index 4ecfd2a..8d6cc4b 100644 --- a/src/x509.ts +++ b/src/x509.ts @@ -507,6 +507,7 @@ export abstract class X509 { obj[key] = toJSONify((this as any)[key]) } delete obj.tbsCertificate + delete obj.captures return obj } diff --git a/test/cert/csr1.pem b/test/cert/csr1.pem new file mode 100644 index 0000000..cabb5a6 --- /dev/null +++ b/test/cert/csr1.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNV +BAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGln +aUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmo +wp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c +1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiI +WDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZ +wIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPR +BPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJ +KoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6D +hJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpY +Q4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/ +ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn +29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO2 +97Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w= +-----END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/test/cert/test.csr b/test/cert/csr2.pem similarity index 100% rename from test/cert/test.csr rename to test/cert/csr2.pem diff --git a/test/x509.ts b/test/x509.ts index 6a205ff..3a7b23b 100644 --- a/test/x509.ts +++ b/test/x509.ts @@ -140,7 +140,12 @@ suite('X509', function () { }) it('should support CSRs', function () { - const csr = CertificateSigningRequest.fromPEM(fs.readFileSync('./test/cert/test.csr')) + const csr = CertificateSigningRequest.fromPEM(fs.readFileSync('./test/cert/csr1.pem')) + strictEqual(csr.extensions.length, 0) + }) + + it('should support CSRs with extensions', function () { + const csr = CertificateSigningRequest.fromPEM(fs.readFileSync('./test/cert/csr2.pem')) strictEqual(csr.getExtension("keyUsage").digitalSignature, true) strictEqual(csr.extensions.length, 3) }) From c541d009c362b9f83bd69ae905b9daa88cc1d8a9 Mon Sep 17 00:00:00 2001 From: Jeschke Date: Wed, 28 Nov 2018 07:33:47 +0100 Subject: [PATCH 3/3] lint issues --- src/x509.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/x509.ts b/src/x509.ts index 8d6cc4b..a5a3f28 100644 --- a/src/x509.ts +++ b/src/x509.ts @@ -226,7 +226,7 @@ const x509CertificateSigningRequestValidator = { class: Class.UNIVERSAL, tag: Tag.SET, capture: 'certExtensions', - }] + }], }], optional: true, }], @@ -378,7 +378,7 @@ export abstract class X509 { readonly tbsCertificate: ASN1 readonly subject: DistinguishedName - constructor(validator: Template, obj: ASN1) { + constructor (validator: Template, obj: ASN1) { this.captures = Object.create(null) as Captures // validate certificate and capture data const err = obj.validate(validator, this.captures)