-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Brian DeHamer <[email protected]>
- Loading branch information
Showing
8 changed files
with
238 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@sigstore/verify": minor | ||
--- | ||
|
||
Enable verification of RFC3161 timestamps |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters