Skip to content

Commit

Permalink
verify RFC3161 timestamps (#932)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <[email protected]>
  • Loading branch information
bdehamer authored Jan 5, 2024
1 parent bf9d8ac commit 08b7957
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-gorillas-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sigstore/verify": minor
---

Enable verification of RFC3161 timestamps
2 changes: 1 addition & 1 deletion packages/mock-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function assembleTrustedRoot({
ctlogs: [transparencyLogInstance(ctlog.publicKey, url)],
tlogs: [transparencyLogInstance(tlog.publicKey, url)],
timestampAuthorities: [
certificateAuthority([tsa.rootCertificate, tsa.intCertificate], url),
certificateAuthority([tsa.intCertificate, tsa.rootCertificate], url),
],
};
}
Expand Down
121 changes: 121 additions & 0 deletions packages/verify/src/__tests__/timestamp/tsa.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { RFC3161Timestamp, X509Certificate } from '@sigstore/core';
import { VerificationError } from '../../error';
import { verifyRFC3161Timestamp } from '../../timestamp/tsa';
import { CertAuthority } from '../../trust';

describe('verifyRFC3161Timestamp', () => {
const artifact = Buffer.from('hello, world!');
const tsBytes = Buffer.from(
'MIICIDADAgEAMIICFwYJKoZIhvcNAQcCoIICCDCCAgQCAQMxDzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0BCRABBKBpBGcwZQIBAQYJKwYBBAGDvzACMC8wCwYJYIZIAWUDBAIBBCBo5layUeZ+g1i++Eg6sNUcZhnz56Gp8OdYONQf82j3KAIE3q2+7xgTMjAyMzEyMjMwMDE3MDMuMDk5WjADAgEBAgRJlgLSoAAxggFwMIIBbAIBATArMCYxDDAKBgNVBAMTA3RzYTEWMBQGA1UEChMNc2lnc3RvcmUubW9jawIBAjANBglghkgBZQMEAgEFAKCB1TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTIzMTIyMzAwMTcwM1owLwYJKoZIhvcNAQkEMSIEIGZcDv7LEHC7SY1TBs1MKmI0MCMChJVp+RCMeyE1mWYZMGgGCyqGSIb3DQEJEAIvMVkwVzBVMFMEIL4AsycoQkVSQDvExPW0mWmW0BAOwQXYUdo6JtfWSP4nMC8wKqQoMCYxDDAKBgNVBAMTA3RzYTEWMBQGA1UEChMNc2lnc3RvcmUubW9jawIBAjAKBggqhkjOPQQDAgRHMEUCIA9EmBywagALb8P8jnn+iZTpb1R9SvQ1QOJ26ICn8SbiAiEAs21Z3xI++DUtb74Yaq0y8Nf5kVPYFtXHQAiG/iBnbKs=',
'base64'
);
const signingPEM = `-----BEGIN CERTIFICATE-----
MIIBuzCCAWGgAwIBAgIBAjAKBggqhkjOPQQDAzAmMQwwCgYDVQQDEwN0c2ExFjAU
BgNVBAoTDXNpZ3N0b3JlLm1vY2swHhcNMjMxMjIzMDAxMzU4WhcNMjQxMjIyMDAx
MzU4WjAuMRQwEgYDVQQDEwt0c2Egc2lnbmluZzEWMBQGA1UEChMNc2lnc3RvcmUu
bW9jazBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIVX5ocFJ8M3Z2urwxgiKlpg
cuOI/kU/2Kw3uWtDXhX/J408sXDC+KadlbCOnJW1wk6gnKEw08taYKMN0CVxXbOj
eDB2MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoG
CCsGAQUFBwMIMB0GA1UdDgQWBBTw4R0V5NxFPZCd34gdGn1eqokHhjAfBgNVHSME
GDAWgBTw4R0V5NxFPZCd34gdGn1eqokHhjAKBggqhkjOPQQDAwNIADBFAiA8eoBs
sQJyxMwPuAgtqsf33aA6F+9HmJbRmt2dvYBfvgIhAL5bxfSG/5VjH5+DHFX0XF7h
6rqeM9DKbNU0HmxMV0Te
-----END CERTIFICATE-----`;
const rootPEM = `-----BEGIN CERTIFICATE-----
MIIBnjCCAUSgAwIBAgIBATAKBggqhkjOPQQDAzAmMQwwCgYDVQQDEwN0c2ExFjAU
BgNVBAoTDXNpZ3N0b3JlLm1vY2swHhcNMjMxMjIzMDAxMzU4WhcNMjQxMjIyMDAx
MzU4WjAmMQwwCgYDVQQDEwN0c2ExFjAUBgNVBAoTDXNpZ3N0b3JlLm1vY2swWTAT
BgcqhkjOPQIBBggqhkjOPQMBBwNCAASFV+aHBSfDN2drq8MYIipaYHLjiP5FP9is
N7lrQ14V/yeNPLFwwvimnZWwjpyVtcJOoJyhMNPLWmCjDdAlcV2zo2MwYTAPBgNV
HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU8OEdFeTcRT2Q
nd+IHRp9XqqJB4YwHwYDVR0jBBgwFoAU8OEdFeTcRT2Qnd+IHRp9XqqJB4YwCgYI
KoZIzj0EAwMDSAAwRQIhAPRvk0qPFRYZWSIH7UQnG00D0I7pV6B4TPjgpL5VvYaQ
AiB13l000lcAWegsMFZJmPEpKNCndg9LC4ttahNN7REmjw==
-----END CERTIFICATE-----`;

describe('when the timestamp is valid', () => {
const ts = RFC3161Timestamp.parse(tsBytes);
const signingCert = X509Certificate.parse(signingPEM);
const rootCert = X509Certificate.parse(rootPEM);
const certAuthority: CertAuthority = {
certChain: [signingCert, rootCert],
validFor: { start: new Date(1), end: new Date() },
};

it('does NOT throw an error', () => {
verifyRFC3161Timestamp(ts, artifact, [certAuthority]);
});
});

describe('when there are no matching CAs to verify against', () => {
const ts = RFC3161Timestamp.parse(tsBytes);
const certAuthority: CertAuthority = {
certChain: [],
validFor: { start: new Date(1), end: new Date() },
};

it('throws an error', () => {
expect(() =>
verifyRFC3161Timestamp(ts, artifact, [certAuthority])
).toThrowWithCode(VerificationError, 'TIMESTAMP_ERROR');
});
});

describe('when the CA certificate chain is invalid', () => {
const ts = RFC3161Timestamp.parse(tsBytes);
const signingCert = X509Certificate.parse(signingPEM);

// No root cert in CA
const certAuthority: CertAuthority = {
certChain: [signingCert],
validFor: { start: new Date(1), end: new Date() },
};

it('throws an error', () => {
expect(() =>
verifyRFC3161Timestamp(ts, artifact, [certAuthority])
).toThrowWithCode(VerificationError, 'TIMESTAMP_ERROR');
});
});

describe('when the artifact does NOT match the signature', () => {
const ts = RFC3161Timestamp.parse(tsBytes);
const signingCert = X509Certificate.parse(signingPEM);
const rootCert = X509Certificate.parse(rootPEM);
const certAuthority: CertAuthority = {
certChain: [signingCert, rootCert],
validFor: { start: new Date(1), end: new Date() },
};

it('throws an error', () => {
expect(() =>
verifyRFC3161Timestamp(ts, Buffer.from('oops'), [certAuthority])
).toThrowWithCode(VerificationError, 'TIMESTAMP_ERROR');
});
});

describe('when the timestamp is outside the validity window of the CA', () => {
// The signed timestamp is 2030-01-01, but the CA is only valid until 2024-12-22
const ts = RFC3161Timestamp.parse(
Buffer.from(
'MIICGzADAgEAMIICEgYJKoZIhvcNAQcCoIICAzCCAf8CAQMxDzANBglghkgBZQMEAgEFADB0BgsqhkiG9w0BCRABBKBlBGMwYQIBAQYJKwYBBAGDvzACMC8wCwYJYIZIAWUDBAIBBCBo5layUeZ+g1i++Eg6sNUcZhnz56Gp8OdYONQf82j3KAIE3q2+7xgPMjAzMDAxMDEwMDAwMDBaMAMCAQECBEmWAtKgADGCAW8wggFrAgEBMCswJjEMMAoGA1UEAxMDdHNhMRYwFAYDVQQKEw1zaWdzdG9yZS5tb2NrAgECMA0GCWCGSAFlAwQCAQUAoIHVMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMzAwMTAxMDAwMDAwWjAvBgkqhkiG9w0BCQQxIgQghLJ03rMmmDV8/1BGWyKNpH5kwjqbDox0jKcux/tbCKMwaAYLKoZIhvcNAQkQAi8xWTBXMFUwUwQgvgCzJyhCRVJAO8TE9bSZaZbQEA7BBdhR2jom19ZI/icwLzAqpCgwJjEMMAoGA1UEAxMDdHNhMRYwFAYDVQQKEw1zaWdzdG9yZS5tb2NrAgECMAoGCCqGSM49BAMCBEYwRAIgdgyZVd9hRlf0eL6ConhIwsuQD5CcxqluBWgNmU8p03sCIEsO7OyjSVyk8wzMntY17C4UaRCnNKIjjHElijJDUb4t',
'base64'
)
);
const signingCert = X509Certificate.parse(signingPEM);
const rootCert = X509Certificate.parse(rootPEM);

// Setting the validity window to be far in the future so that the timestamp can be
// compared against the validity window embedded in the CA certs themselves.
const certAuthority: CertAuthority = {
certChain: [signingCert, rootCert],
validFor: { start: new Date(1), end: new Date('2040-01-01') },
};

it('throws an error', () => {
expect(() =>
verifyRFC3161Timestamp(ts, artifact, [certAuthority])
).toThrowWithCode(VerificationError, 'TIMESTAMP_ERROR');
});
});
});
5 changes: 5 additions & 0 deletions packages/verify/src/__tests__/verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ describe('Verifier', () => {
const subject = new Verifier(trustMaterial);

describe('when the certificate-signed message signature bundle is valid', () => {
const subject = new Verifier(trustMaterial, {
ctlogThreshold: 1,
tlogThreshold: 1,
tsaThreshold: 1,
});
const bundle = bundleFromJSON(
bundles.V1.MESSAGE_SIGNATURE.WITH_SIGNING_CERT
);
Expand Down
2 changes: 1 addition & 1 deletion packages/verify/src/key/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ interface CertificateChainVerifierOptions {
untrustedCert: X509Certificate;
}

class CertificateChainVerifier {
export class CertificateChainVerifier {
private untrustedCert: X509Certificate;
private trustedCerts: X509Certificate[];
private localCerts: X509Certificate[];
Expand Down
13 changes: 7 additions & 6 deletions packages/verify/src/timestamp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ 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 type { RFC3161Timestamp } from '@sigstore/core';
import assert from 'assert';
import { RFC3161Timestamp } from '@sigstore/core';
import { VerificationError } from '../error';
import { verifyCheckpoint } from './checkpoint';
import { verifyMerkleInclusion } from './merkle';
import { verifyTLogSET } from './set';
import { verifyRFC3161Timestamp } from './tsa';

import type {
TLogEntryWithInclusionPromise,
Expand All @@ -37,14 +37,15 @@ export type TimestampVerificationResult = {

export function verifyTSATimestamp(
timestamp: RFC3161Timestamp,
data: Buffer,
timestampAuthorities: CertAuthority[]
): TimestampVerificationResult {
// TODO: Insert TSA verification logic here.
assert(timestampAuthorities);
verifyRFC3161Timestamp(timestamp, data, timestampAuthorities);

return {
logID: timestamp.signerSerialNumber,
timestamp: timestamp.tstInfo.genTime,
type: 'timestamp-authority',
logID: timestamp.signerSerialNumber,
timestamp: timestamp.signingTime,
};
}

Expand Down
95 changes: 95 additions & 0 deletions packages/verify/src/timestamp/tsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { RFC3161Timestamp, crypto } from '@sigstore/core';
import { VerificationError } from '../error';
import { CertificateChainVerifier } from '../key/certificate';
import { CertAuthority, filterCertAuthorities } from '../trust';

export function verifyRFC3161Timestamp(
timestamp: RFC3161Timestamp,
data: Buffer,
timestampAuthorities: CertAuthority[]
): void {
const signingTime = timestamp.signingTime;

// Filter for CAs which were valid at the time of signing
timestampAuthorities = filterCertAuthorities(timestampAuthorities, {
start: signingTime,
end: signingTime,
});

// Filter for CAs which match serial and issuer embedded in the timestamp
timestampAuthorities = filterCAsBySerialAndIssuer(timestampAuthorities, {
serialNumber: timestamp.signerSerialNumber,
issuer: timestamp.signerIssuer,
});

// Check that we can verify the timestamp with AT LEAST ONE of the remaining
// CAs
const verified = timestampAuthorities.some((ca) => {
try {
verifyTimestampForCA(timestamp, data, ca);
return true;
} catch (e) {
return false;
}
});

if (!verified) {
throw new VerificationError({
code: 'TIMESTAMP_ERROR',
message: 'timestamp could not be verified',
});
}
}

function verifyTimestampForCA(
timestamp: RFC3161Timestamp,
data: Buffer,
ca: CertAuthority
): void {
const [leaf, ...cas] = ca.certChain;
const signingKey = crypto.createPublicKey(leaf.publicKey);
const signingTime = timestamp.signingTime;

// Verify the certificate chain for the provided CA
try {
new CertificateChainVerifier({
untrustedCert: leaf,
trustedCerts: cas,
}).verify();
} catch (e) {
throw new VerificationError({
code: 'TIMESTAMP_ERROR',
message: 'invalid certificate chain',
});
}

// Check that all of the CA certs were valid at the time of signing
const validAtSigningTime = ca.certChain.every((cert) =>
cert.validForDate(signingTime)
);

if (!validAtSigningTime) {
throw new VerificationError({
code: 'TIMESTAMP_ERROR',
message: 'timestamp was signed with an expired certificate',
});
}

// Check that the signing certificate's key can be used to verify the
// timestamp signature.
timestamp.verify(data, signingKey);
}

// Filters the list of CAs to those which have a leaf signing certificate which
// matches the given serial number and issuer.
function filterCAsBySerialAndIssuer(
timestampAuthorities: CertAuthority[],
criteria: { serialNumber: Buffer; issuer: Buffer }
): CertAuthority[] {
return timestampAuthorities.filter(
(ca) =>
ca.certChain.length > 0 &&
crypto.bufferEqual(ca.certChain[0].serialNumber, criteria.serialNumber) &&
crypto.bufferEqual(ca.certChain[0].issuer, criteria.issuer)
);
}
6 changes: 3 additions & 3 deletions packages/verify/src/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ export class Verifier {
// Checks that all of the timestamps in the entity are valid and returns them
private verifyTimestamps(entity: SignedEntity): Date[] {
let tlogCount = 0;
const tsaCount = 0;
let tsaCount = 0;

const timestamps = entity.timestamps.map((timestamp) => {
switch (timestamp.$case) {
case 'timestamp-authority':
// TODO: uncomment this when we are actually verifying timestamps
// tsaCount++;
tsaCount++;
return verifyTSATimestamp(
timestamp.timestamp,
entity.signature.signature,
this.trustMaterial.timestampAuthorities
);
case 'transparency-log':
Expand Down

0 comments on commit 08b7957

Please sign in to comment.