Skip to content

Commit

Permalink
support for parsing RFC3161 timestamps (#913)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <bdehamer@github.com>
  • Loading branch information
bdehamer authored Dec 22, 2023
1 parent 2dd55a0 commit 6869511
Show file tree
Hide file tree
Showing 13 changed files with 581 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-walls-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sigstore/core": minor
---

Add support for parsing RFC3161 signed timestamps
1 change: 1 addition & 0 deletions packages/core/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
158 changes: 158 additions & 0 deletions packages/core/src/__tests__/rfc3161/timestamp.test.ts
Original file line number Diff line number Diff line change
@@ -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
);
});
});
});
});
71 changes: 71 additions & 0 deletions packages/core/src/__tests__/rfc3161/tstinfo.test.ts
Original file line number Diff line number Diff line change
@@ -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
);
});
});
});
});
3 changes: 3 additions & 0 deletions packages/core/src/__tests__/x509/cert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
12 changes: 12 additions & 0 deletions packages/core/src/oid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const ECDSA_SIGNATURE_ALGOS: Record<string, string> = {
'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<string, string> = {
'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',
};
16 changes: 16 additions & 0 deletions packages/core/src/rfc3161/error.ts
Original file line number Diff line number Diff line change
@@ -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 {}
17 changes: 17 additions & 0 deletions packages/core/src/rfc3161/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading

0 comments on commit 6869511

Please sign in to comment.