From 6869511e5d785e8c9d9d0c1dfa3b1b24bacaf3e4 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Fri, 22 Dec 2023 07:53:26 -0800 Subject: [PATCH] support for parsing RFC3161 timestamps (#913) Signed-off-by: Brian DeHamer --- .changeset/healthy-walls-raise.md | 5 + packages/core/src/__tests__/index.test.ts | 1 + .../src/__tests__/rfc3161/timestamp.test.ts | 158 +++++++++++++ .../src/__tests__/rfc3161/tstinfo.test.ts | 71 ++++++ packages/core/src/__tests__/x509/cert.test.ts | 3 + packages/core/src/crypto.ts | 9 + packages/core/src/index.ts | 1 + packages/core/src/oid.ts | 12 + packages/core/src/rfc3161/error.ts | 16 ++ packages/core/src/rfc3161/index.ts | 17 ++ packages/core/src/rfc3161/timestamp.ts | 215 ++++++++++++++++++ packages/core/src/rfc3161/tstinfo.ts | 62 +++++ packages/core/src/x509/cert.ts | 18 +- 13 files changed, 581 insertions(+), 7 deletions(-) create mode 100644 .changeset/healthy-walls-raise.md create mode 100644 packages/core/src/__tests__/rfc3161/timestamp.test.ts create mode 100644 packages/core/src/__tests__/rfc3161/tstinfo.test.ts create mode 100644 packages/core/src/oid.ts create mode 100644 packages/core/src/rfc3161/error.ts create mode 100644 packages/core/src/rfc3161/index.ts create mode 100644 packages/core/src/rfc3161/timestamp.ts create mode 100644 packages/core/src/rfc3161/tstinfo.ts diff --git a/.changeset/healthy-walls-raise.md b/.changeset/healthy-walls-raise.md new file mode 100644 index 00000000..f780565b --- /dev/null +++ b/.changeset/healthy-walls-raise.md @@ -0,0 +1,5 @@ +--- +"@sigstore/core": minor +--- + +Add support for parsing RFC3161 signed timestamps diff --git a/packages/core/src/__tests__/index.test.ts b/packages/core/src/__tests__/index.test.ts index a29615ab..73edf1f5 100644 --- a/packages/core/src/__tests__/index.test.ts +++ b/packages/core/src/__tests__/index.test.ts @@ -18,6 +18,7 @@ import * as core from '..'; it('exports classes', () => { expect(core.ASN1Obj).toBeInstanceOf(Function); expect(core.ByteStream).toBeInstanceOf(Function); + expect(core.RFC3161Timestamp).toBeInstanceOf(Function); expect(core.X509Certificate).toBeInstanceOf(Function); expect(core.X509SCTExtension).toBeInstanceOf(Function); }); diff --git a/packages/core/src/__tests__/rfc3161/timestamp.test.ts b/packages/core/src/__tests__/rfc3161/timestamp.test.ts new file mode 100644 index 00000000..5ac8fbab --- /dev/null +++ b/packages/core/src/__tests__/rfc3161/timestamp.test.ts @@ -0,0 +1,158 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { createPublicKey } from '../../crypto'; +import { RFC3161TimestampVerificationError } from '../../rfc3161/error'; +import { RFC3161Timestamp } from '../../rfc3161/timestamp'; + +describe('RFC3161Timestamp', () => { + const artifact = Buffer.from('hello, world\n'); + + const publicKey = + '-----BEGIN PUBLIC KEY-----\n' + + 'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEV/zJhNTdu0Fa9hGCUih/JvqEoE81tEWr\n' + + 'AVwUXXhdRgIY9hIFErLhNo6sSOpV9d7Zuy0KWMHhcimCUr41a1732ByVRy3f+Z4Q\n' + + 'hqpsgFMh5b5J90HJLK7HOyUZjehAnvSn\n' + + '-----END PUBLIC KEY-----\n'; + + const ts = Buffer.from( + 'MIIC0TADAgEAMIICyAYJKoZIhvcNAQcCoIICuTCCArUCAQExDTALBglghkgBZQMEAgIwgbwGCyqGSIb3DQEJEAEEoIGsBIGpMIGmAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQghT/5N2Kgbdv3IsTr6d3WbY9j3a6pf1IcPswg2nyXYCACFQCyi6gMhpheZVlBHi153EZai5EdTBgPMjAyMzEyMjAyMTQ5MThaMAMCAQGgNqQ0MDIxFTATBgNVBAoTDEdpdEh1YiwgSW5jLjEZMBcGA1UEAxMQVFNBIFRpbWVzdGFtcGluZ6AAMYIB3jCCAdoCAQEwSjAyMRUwEwYDVQQKEwxHaXRIdWIsIEluYy4xGTAXBgNVBAMTEFRTQSBpbnRlcm1lZGlhdGUCFDQ1ZZrWbr6Lo5+CsIgv6MSK/IcQMAsGCWCGSAFlAwQCAqCCAQUwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yMzEyMjAyMTQ5MThaMD8GCSqGSIb3DQEJBDEyBDDvZfw23I/Jvgh0uo9mfMqkEwBvpUfpkmJfUUImoY0Ist/AwWJZxk/yJvNZ464B7vowgYcGCyqGSIb3DQEJEAIvMXgwdjB0MHIEIC4X67ezV4q0OFecnkRUAx6sVRjb/Q6nZYy0cfMfHKgBME4wNqQ0MDIxFTATBgNVBAoTDEdpdEh1YiwgSW5jLjEZMBcGA1UEAxMQVFNBIGludGVybWVkaWF0ZQIUNDVlmtZuvoujn4KwiC/oxIr8hxAwCgYIKoZIzj0EAwMEZzBlAjEAjplaA2ukG3aI+Zf2nqbI8QqpWXTeJGt7OUT23bYDx84OPK/BBn9NR8JkeO41EtN7AjAozvkg9Wi8deG8pZt3Pj/ip+cvL4X3IktD63SS/+rh+/BrYMWawKT6yu8T3MMsQ+E=', + 'base64' + ); + + const subject = RFC3161Timestamp.parse(ts); + + describe('status', () => { + it('should return the timestamp status', () => { + expect(subject.status).toEqual(0n); + }); + }); + + describe('signingTime', () => { + it('should return the timestamp signing time', () => { + expect(subject.signingTime).toEqual(new Date('2023-12-20T21:49:18.000Z')); + }); + }); + + describe('signerIssuer', () => { + it('should return the issuer name of the signing certificate', () => { + expect(subject.signerIssuer).toHaveLength(50); + }); + }); + + describe('signerSerialNumber', () => { + it('should return the serial number of the signing certificate', () => { + expect(subject.signerSerialNumber).toEqual( + Buffer.from('3435659AD66EBE8BA39F82B0882FE8C48AFC8710', 'hex') + ); + }); + }); + + describe('signerDigestAlgorithm', () => { + it('should return the digest algo used by the signer', () => { + expect(subject.signerDigestAlgorithm).toEqual('sha384'); + }); + }); + + describe('signatureAlgorithm', () => { + it('should return the timestamp signature algorithm', () => { + expect(subject.signatureAlgorithm).toEqual('sha384'); + }); + }); + + describe('verify', () => { + describe('when the timestamp is valid', () => { + const subject = RFC3161Timestamp.parse(ts); + const key = createPublicKey(publicKey); + const data = artifact; + + it('does not throw an error', () => { + expect(() => subject.verify(data, key)).not.toThrow(); + }); + }); + + describe('when the artifact does NOT the one which was signed', () => { + const subject = RFC3161Timestamp.parse(ts); + const key = createPublicKey(publicKey); + const data = Buffer.from('oops'); + + it('throws an error', () => { + expect(() => subject.verify(data, key)).toThrow( + RFC3161TimestampVerificationError + ); + }); + }); + + describe('when the key does NOT match the signature', () => { + const subject = RFC3161Timestamp.parse(ts); + const data = artifact; + const key = createPublicKey( + '-----BEGIN PUBLIC KEY-----\n' + + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9DbYBIMQLtWb6J5gtL69jgRwwEfd\n' + + 'tQtKvvG4+o3ZzlOroJplpXaVgF6wBDob++rNG9/AzSaBmApkEwI52XBjWg==\n' + + '-----END PUBLIC KEY-----\n' + ); + + it('throws an error', () => { + expect(() => subject.verify(data, key)).toThrow( + RFC3161TimestampVerificationError + ); + }); + }); + + describe('when the encapsulated content type is NOT tstInfo', () => { + const data = artifact; + const subject = RFC3161Timestamp.parse( + Buffer.from( + 'MIICITADAgEAMIICGAYJKoZIhvcNAQcCoIICCTCCAgUCAQMxDzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0BCRABBaBpBGcwZQIBAQYJKwYBBAGDvzACMC8wCwYJYIZIAWUDBAIBBCCFP/k3YqBt2/cixOvp3dZtj2Pdrql/Uhw+zCDafJdgIAIE3q2+7xgTMjAyMzEyMjExOTEwNTguNDI2WjADAgEBAgRJlgLSoAAxggFxMIIBbQIBATArMCYxDDAKBgNVBAMTA3RzYTEWMBQGA1UEChMNc2lnc3RvcmUubW9jawIBAjANBglghkgBZQMEAgEFAKCB1TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQUwHAYJKoZIhvcNAQkFMQ8XDTIzMTIyMTE5MTA1OFowLwYJKoZIhvcNAQkEMSIEILhMPeMNPj1iPtE7ztAzXNMco3qGrxS5LwFORL66zN6PMGgGCyqGSIb3DQEJEAIvMVkwVzBVMFMEIIdLV5cij9fs6Nm62roSAclDR2JC116C4Hp61tEMWTsZMC8wKqQoMCYxDDAKBgNVBAMTA3RzYTEWMBQGA1UEChMNc2lnc3RvcmUubW9jawIBAjAKBggqhkjOPQQDAgRIMEYCIQDQN2PgaZ38no/tP/NpncFPskEh1tVGqj4n+pe4VeGYPgIhAKtXSYiKZARsIHepbROedQvFnq0JP3mZaQ+r1kYI5Tuk', + 'base64' + ) + ); + const key = createPublicKey( + '-----BEGIN PUBLIC KEY-----\n' + + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEblxL3hYMGPJAyKCRfNH9zoCbLtb\n' + + 'dOuVnDwPoAUK/sw7vjBNtqaVbet0dTymhDiMLRoKlq95OIM3Hl9Fvbi0gg==\n' + + '-----END PUBLIC KEY-----\n' + ); + + it('throws an error', () => { + expect(() => subject.verify(data, key)).toThrow( + RFC3161TimestampVerificationError + ); + }); + }); + + describe('when the signed message does NOT match the tstInfo', () => { + const data = artifact; + const subject = RFC3161Timestamp.parse( + Buffer.from( + 'MIICHzADAgEAMIICFgYJKoZIhvcNAQcCoIICBzCCAgMCAQMxDzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0BCRABBKBpBGcwZQIBAQYJKwYBBAGDvzACMC8wCwYJYIZIAWUDBAIBBCCFP/k3YqBt2/cixOvp3dZtj2Pdrql/Uhw+zCDafJdgIAIE3q2+7xgTMjAyMzEyMjExOTE2MzIuNzE4WjADAgEBAgRJlgLSoAAxggFvMIIBawIBATArMCYxDDAKBgNVBAMTA3RzYTEWMBQGA1UEChMNc2lnc3RvcmUubW9jawIBAjANBglghkgBZQMEAgEFAKCB1TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTIzMTIyMTE5MTYzMlowLwYJKoZIhvcNAQkEMSIEIJuptzDaKrDb239c8R2sNeHm6xCkRN81uAsObeDpWZHJMGgGCyqGSIb3DQEJEAIvMVkwVzBVMFMEILXG2YeUshi7GamxglcNv0Udq53SwfXSM6LhJbmA82OQMC8wKqQoMCYxDDAKBgNVBAMTA3RzYTEWMBQGA1UEChMNc2lnc3RvcmUubW9jawIBAjAKBggqhkjOPQQDAgRGMEQCIARr+DJVotgq2uQAEoTzkSg2Fo/LHarefdlxvUUvvxZfAiAzCJhmXfchOfsl6wlPfV9zCZsJXIMG4yY7sCQOS/wiHQ==', + 'base64' + ) + ); + const key = createPublicKey( + '-----BEGIN PUBLIC KEY-----\n' + + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvBYWB7Aqp6+E4SeBCAkBWGhQZW2O\n' + + 'u3T+xRv5VpDSfvYJPT46EMpov8AJkR1G4rs0iV1csuammXKF+BQzvLh/qQ==\n' + + '-----END PUBLIC KEY-----\n' + ); + it('throws an error', () => { + expect(() => subject.verify(data, key)).toThrow( + RFC3161TimestampVerificationError + ); + }); + }); + }); +}); diff --git a/packages/core/src/__tests__/rfc3161/tstinfo.test.ts b/packages/core/src/__tests__/rfc3161/tstinfo.test.ts new file mode 100644 index 00000000..b4a229ff --- /dev/null +++ b/packages/core/src/__tests__/rfc3161/tstinfo.test.ts @@ -0,0 +1,71 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { ASN1Obj } from '../../asn1'; +import { RFC3161TimestampVerificationError } from '../../rfc3161/error'; +import { TSTInfo } from '../../rfc3161/tstinfo'; + +describe('TSTInfo', () => { + const tstInfoDER = Buffer.from( + '3081a602010106092b0601040183bf30023031300d060960864801650304020105000420853ff93762a06ddbf722c4ebe9ddd66d8f63ddaea97f521c3ecc20da7c976020021500b28ba80c86985e6559411e2d79dc465a8b911d4c180f32303233313232303231343931385a3003020101a036a434303231153013060355040a130c4769744875622c20496e632e31193017060355040313105453412054696d657374616d70696e67', + 'hex' + ); + const asn1 = ASN1Obj.parseBuffer(tstInfoDER); + const subject = new TSTInfo(asn1); + + describe('version', () => { + it('returns the version', () => { + expect(subject.version).toEqual(BigInt(1)); + }); + }); + + describe('genTime', () => { + it('returns the genTime', () => { + expect(subject.genTime).toEqual(new Date('2023-12-20T21:49:18.000Z')); + }); + }); + + describe('messageImprintHashAlgorithm', () => { + it('returns the messageImprintHashAlgorithm', () => { + expect(subject.messageImprintHashAlgorithm).toEqual('sha256'); + }); + }); + + describe('messageImprintHashedMessage', () => { + it('returns the messageImprintHashedMessage', () => { + expect(subject.messageImprintHashedMessage).toBeDefined(); + }); + }); + + describe('verify', () => { + describe('when the messageImprintHashedMessage matches the artifact', () => { + const artifact = Buffer.from('hello, world\n'); + + it('does not throw an error', () => { + expect(() => subject.verify(artifact)).not.toThrow(); + }); + }); + + describe('when the messageImprintHashedMessage does NOT match the artifact', () => { + const artifact = Buffer.from('oops'); + + it('does not throw an error', () => { + expect(() => subject.verify(artifact)).toThrow( + RFC3161TimestampVerificationError + ); + }); + }); + }); +}); diff --git a/packages/core/src/__tests__/x509/cert.test.ts b/packages/core/src/__tests__/x509/cert.test.ts index bdc3ce5e..5c686adf 100644 --- a/packages/core/src/__tests__/x509/cert.test.ts +++ b/packages/core/src/__tests__/x509/cert.test.ts @@ -23,6 +23,9 @@ describe('X509Certificate', () => { const cert = X509Certificate.parse(certificates.root); expect(cert.version).toBe('v3'); + expect(cert.serialNumber).toEqual( + Buffer.from('61CC29EC72F2E28458A0C330B7E8D40357FAFE9E', 'hex') + ); expect(cert.notBefore).toBeInstanceOf(Date); expect(cert.notBefore.toISOString()).toBe('1990-01-01T00:00:00.000Z'); expect(cert.notAfter).toBeInstanceOf(Date); diff --git a/packages/core/src/crypto.ts b/packages/core/src/crypto.ts index 3d4af7a9..279be4af 100644 --- a/packages/core/src/crypto.ts +++ b/packages/core/src/crypto.ts @@ -26,6 +26,15 @@ export function createPublicKey(key: string | Buffer): crypto.KeyObject { } } +export function digest(algorithm: string, ...data: BinaryLike[]): Buffer { + const hash = crypto.createHash(algorithm); + for (const d of data) { + hash.update(d); + } + return hash.digest(); +} + +// TODO: deprecate this in favor of digest() export function hash(...data: BinaryLike[]): Buffer { const hash = crypto.createHash(SHA256_ALGORITHM); for (const d of data) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6e087b9b..af2b8487 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,5 +19,6 @@ export * as dsse from './dsse'; export * as encoding from './encoding'; export * as json from './json'; export * as pem from './pem'; +export { RFC3161Timestamp } from './rfc3161'; export { ByteStream } from './stream'; export { EXTENSION_OID_SCT, X509Certificate, X509SCTExtension } from './x509'; diff --git a/packages/core/src/oid.ts b/packages/core/src/oid.ts new file mode 100644 index 00000000..7adb4847 --- /dev/null +++ b/packages/core/src/oid.ts @@ -0,0 +1,12 @@ +export const ECDSA_SIGNATURE_ALGOS: Record = { + '1.2.840.10045.4.3.1': 'sha224', + '1.2.840.10045.4.3.2': 'sha256', + '1.2.840.10045.4.3.3': 'sha384', + '1.2.840.10045.4.3.4': 'sha512', +}; + +export const SHA2_HASH_ALGOS: Record = { + '2.16.840.1.101.3.4.2.1': 'sha256', + '2.16.840.1.101.3.4.2.2': 'sha384', + '2.16.840.1.101.3.4.2.3': 'sha512', +}; diff --git a/packages/core/src/rfc3161/error.ts b/packages/core/src/rfc3161/error.ts new file mode 100644 index 00000000..874cbe46 --- /dev/null +++ b/packages/core/src/rfc3161/error.ts @@ -0,0 +1,16 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +export class RFC3161TimestampVerificationError extends Error {} diff --git a/packages/core/src/rfc3161/index.ts b/packages/core/src/rfc3161/index.ts new file mode 100644 index 00000000..d2e3db76 --- /dev/null +++ b/packages/core/src/rfc3161/index.ts @@ -0,0 +1,17 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export { RFC3161Timestamp } from './timestamp'; diff --git a/packages/core/src/rfc3161/timestamp.ts b/packages/core/src/rfc3161/timestamp.ts new file mode 100644 index 00000000..012aa028 --- /dev/null +++ b/packages/core/src/rfc3161/timestamp.ts @@ -0,0 +1,215 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { ASN1Obj } from '../asn1'; +import * as crypto from '../crypto'; +import { ECDSA_SIGNATURE_ALGOS, SHA2_HASH_ALGOS } from '../oid'; +import { RFC3161TimestampVerificationError } from './error'; +import { TSTInfo } from './tstinfo'; + +const OID_PKCS9_CONTENT_TYPE_TSTINFO = '1.2.840.113549.1.9.16.1.4'; +const OID_PKCS9_MESSAGE_DIGEST_KEY = '1.2.840.113549.1.9.4'; + +export class RFC3161Timestamp { + public root: ASN1Obj; + + constructor(asn1: ASN1Obj) { + this.root = asn1; + } + + public static parse(der: Buffer): RFC3161Timestamp { + const asn1 = ASN1Obj.parseBuffer(der); + return new RFC3161Timestamp(asn1); + } + + get status(): bigint { + return this.pkiStatusInfoObj.subs[0].toInteger(); + } + + get eContentType(): string { + return this.eContentTypeObj.toOID(); + } + + get signingTime(): Date { + return this.tstInfo.genTime; + } + + get signerIssuer(): Buffer { + return this.signerSidObj.subs[0].value; + } + + get signerSerialNumber(): Buffer { + return this.signerSidObj.subs[1].value; + } + + get signerDigestAlgorithm(): string { + const oid = this.signerDigestAlgorithmObj.subs[0].toOID(); + return SHA2_HASH_ALGOS[oid]; + } + + get signatureAlgorithm(): string { + const oid = this.signatureAlgorithmObj.subs[0].toOID(); + return ECDSA_SIGNATURE_ALGOS[oid]; + } + + get signatureValue(): Buffer { + return this.signatureValueObj.value; + } + + get tstInfo(): TSTInfo { + // Need to unpack tstInfo from an OCTET STRING + return new TSTInfo(this.eContentObj.subs[0].subs[0]); + } + + public verify(data: Buffer, publicKey: crypto.KeyObject): void { + // Check for expected encapsulated content type + if (this.eContentType !== OID_PKCS9_CONTENT_TYPE_TSTINFO) { + throw new RFC3161TimestampVerificationError( + `incorrect encapsulated content type: ${this.eContentType}` + ); + } + + // Check that the tstInfo references the correct artifact + this.tstInfo.verify(data); + // Check that the signed message digest matches the tstInfo + this.verifyMessageDigest(); + // Check that the signature is valid for the signed attributes + this.verifySignature(publicKey); + } + + private verifyMessageDigest(): void { + // Check that the tstInfo matches the signed data + const tstInfoDigest = crypto.digest( + this.signerDigestAlgorithm, + this.tstInfo.raw + ); + const expectedDigest = this.messageDigestAttributeObj.subs[1].subs[0].value; + + if (!crypto.bufferEqual(tstInfoDigest, expectedDigest)) { + throw new RFC3161TimestampVerificationError( + 'signed data does not match tstInfo' + ); + } + } + + private verifySignature(key: crypto.KeyObject): void { + // Encode the signed attributes for verification + const signedAttrs = this.signedAttrsObj.toDER(); + signedAttrs[0] = 0x31; // Change context-specific tag to SET + + // Check that the signature is valid for the signed attributes + const verified = crypto.verify( + signedAttrs, + key, + this.signatureValue, + this.signatureAlgorithm + ); + + if (!verified) { + throw new RFC3161TimestampVerificationError( + 'signature verification failed' + ); + } + } + + // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2 + private get pkiStatusInfoObj(): ASN1Obj { + // pkiStatusInfo is the first element of the timestamp response sequence + return this.root.subs[0]; + } + + // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2 + private get timeStampTokenObj(): ASN1Obj { + // timeStampToken is the first element of the timestamp response sequence + return this.root.subs[1]; + } + + // https://www.rfc-editor.org/rfc/rfc5652#section-3 + private get signedDataObj(): ASN1Obj { + const obj = this.timeStampTokenObj.subs.find((sub) => + sub.tag.isContextSpecific(0x00) + ); + return obj!.subs[0]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.1 + private get encapContentInfoObj(): ASN1Obj { + return this.signedDataObj.subs[2]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.1 + private get signerInfosObj(): ASN1Obj { + // SignerInfos is the last element of the signed data sequence + const sd = this.signedDataObj; + return sd.subs[sd.subs.length - 1]; + } + + // https://www.rfc-editor.org/rfc/rfc5652#section-5.1 + private get signerInfoObj(): ASN1Obj { + // Only supporting one signer + return this.signerInfosObj.subs[0]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.2 + private get eContentTypeObj(): ASN1Obj { + return this.encapContentInfoObj.subs[0]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.2 + private get eContentObj(): ASN1Obj { + return this.encapContentInfoObj.subs[1]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.3 + private get signedAttrsObj(): ASN1Obj { + const signedAttrs = this.signerInfoObj.subs.find((sub) => + sub.tag.isContextSpecific(0x00) + ); + return signedAttrs!; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.3 + private get messageDigestAttributeObj(): ASN1Obj { + const messageDigest = this.signedAttrsObj.subs.find( + (sub) => + sub.subs[0].tag.isOID() && + sub.subs[0].toOID() === OID_PKCS9_MESSAGE_DIGEST_KEY + ); + return messageDigest!; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.3 + private get signerSidObj(): ASN1Obj { + return this.signerInfoObj.subs[1]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.3 + private get signerDigestAlgorithmObj(): ASN1Obj { + // Signature is the 2nd element of the signerInfoObj object + return this.signerInfoObj.subs[2]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.3 + private get signatureAlgorithmObj(): ASN1Obj { + // Signature is the 4th element of the signerInfoObj object + return this.signerInfoObj.subs[4]; + } + + // https://datatracker.ietf.org/doc/html/rfc5652#section-5.3 + private get signatureValueObj(): ASN1Obj { + // Signature is the 6th element of the signerInfoObj object + return this.signerInfoObj.subs[5]; + } +} diff --git a/packages/core/src/rfc3161/tstinfo.ts b/packages/core/src/rfc3161/tstinfo.ts new file mode 100644 index 00000000..96fbd75a --- /dev/null +++ b/packages/core/src/rfc3161/tstinfo.ts @@ -0,0 +1,62 @@ +/* +Copyright 2023 The Sigstore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { ASN1Obj } from '../asn1'; +import * as crypto from '../crypto'; +import { SHA2_HASH_ALGOS } from '../oid'; +import { RFC3161TimestampVerificationError } from './error'; + +export class TSTInfo { + public root: ASN1Obj; + + constructor(asn1: ASN1Obj) { + this.root = asn1; + } + + get version(): bigint { + return this.root.subs[0].toInteger(); + } + + get genTime(): Date { + return this.root.subs[4].toDate(); + } + + get messageImprintHashAlgorithm(): string { + const oid = this.messageImprintObj.subs[0].subs[0].toOID(); + return SHA2_HASH_ALGOS[oid]; + } + + get messageImprintHashedMessage(): Buffer { + return this.messageImprintObj.subs[1].value; + } + + get raw(): Buffer { + return this.root.toDER(); + } + + public verify(data: Buffer): void { + const digest = crypto.digest(this.messageImprintHashAlgorithm, data); + if (!crypto.bufferEqual(digest, this.messageImprintHashedMessage)) { + throw new RFC3161TimestampVerificationError( + 'message imprint does not match artifact' + ); + } + } + + // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2 + private get messageImprintObj(): ASN1Obj { + return this.root.subs[2]; + } +} diff --git a/packages/core/src/x509/cert.ts b/packages/core/src/x509/cert.ts index 5521a03b..69a1c0f0 100644 --- a/packages/core/src/x509/cert.ts +++ b/packages/core/src/x509/cert.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { ASN1Obj } from '../asn1'; import * as crypto from '../crypto'; +import { ECDSA_SIGNATURE_ALGOS } from '../oid'; import * as pem from '../pem'; import { X509AuthorityKeyIDExtension, @@ -33,13 +34,6 @@ const EXTENSION_OID_BASIC_CONSTRAINTS = '2.5.29.19'; const EXTENSION_OID_AUTHORITY_KEY_ID = '2.5.29.35'; export const EXTENSION_OID_SCT = '1.3.6.1.4.1.11129.2.4.2'; -const ECDSA_SIGNATURE_ALGOS: Record = { - '1.2.840.10045.4.3.1': 'sha224', - '1.2.840.10045.4.3.2': 'sha256', - '1.2.840.10045.4.3.3': 'sha384', - '1.2.840.10045.4.3.4': 'sha512', -}; - export class X509Certificate { public root: ASN1Obj; @@ -63,6 +57,10 @@ export class X509Certificate { return `v${(ver + BigInt(1)).toString()}`; } + get serialNumber(): Buffer { + return this.serialNumberObj.value; + } + get notBefore(): Date { // notBefore is the first element of the validity sequence return this.validityObj.subs[0].toDate(); @@ -219,6 +217,12 @@ export class X509Certificate { return this.tbsCertificateObj.subs[0]; } + // https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.2 + private get serialNumberObj(): ASN1Obj { + // serialNumber is the second element of the tbsCertificate sequence + return this.tbsCertificateObj.subs[1]; + } + // https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.4 private get issuerObj(): ASN1Obj { // issuer is the fourth element of the tbsCertificate sequence