Skip to content

Commit 69e34e1

Browse files
asgrimTimWolla
andcommitted
Split out and improve attestation verification from PR discussion
Co-authored-by: Tim Düsterhus <[email protected]>
1 parent 5ddde91 commit 69e34e1

File tree

2 files changed

+62
-34
lines changed

2 files changed

+62
-34
lines changed

src/SelfManage/Verify/FailedToVerifyRelease.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use function implode;
1313
use function is_array;
1414
use function sprintf;
15+
use function strlen;
1516
use function trim;
1617

1718
class FailedToVerifyRelease extends RuntimeException
@@ -57,6 +58,16 @@ public static function fromNoIssuerCertificateInTrustedRoot(array|string $issuer
5758
));
5859
}
5960

61+
public static function fromInvalidDerEncodedStringLength(string $derEncodedString, int $expectedLength): self
62+
{
63+
return new self(sprintf(
64+
'DER encoded string length of "%s" was wrong; expected %d characters, was actually %d characters',
65+
$derEncodedString,
66+
$expectedLength,
67+
strlen($derEncodedString),
68+
));
69+
}
70+
6071
public static function fromMismatchingExtensionValues(string $extension, string $expected, string $actual): self
6172
{
6273
return new self(sprintf(

src/SelfManage/Verify/FallbackVerificationUsingOpenSsl.php

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,11 @@ public function verify(ReleaseMetadata $releaseMetadata, BinaryFile $pharFilenam
7676
* - https://docs.sigstore.dev/logging/verify-release/
7777
* - https://github.com/secure-systems-lab/dsse/blob/master/protocol.md#protocol
7878
*/
79-
$this->assertCertificateClaims($attestation);
80-
$output->writeln('#' . $attestationIndex . ': Certificate facts verified.', OutputInterface::VERBOSITY_VERBOSE);
79+
$this->assertCertificateSignedByTrustedRoot($attestation);
80+
$output->writeln('#' . $attestationIndex . ': Certificate was signed by a trusted root.', OutputInterface::VERBOSITY_VERBOSE);
81+
82+
$this->assertCertificateExtensionClaims($attestation);
83+
$output->writeln('#' . $attestationIndex . ': Certificate extension claims match.', OutputInterface::VERBOSITY_VERBOSE);
8184

8285
$this->assertDigestFromAttestationMatchesActual($pharFilename, $attestation);
8386
$output->writeln('#' . $attestationIndex . ': Payload digest matches downloaded file.', OutputInterface::VERBOSITY_VERBOSE);
@@ -89,39 +92,9 @@ public function verify(ReleaseMetadata $releaseMetadata, BinaryFile $pharFilenam
8992
$output->writeln('<info>✅ Verified the new PIE version (using fallback verification)</info>');
9093
}
9194

92-
private function assertCertificateClaims(Attestation $attestation): void
95+
private function assertCertificateSignedByTrustedRoot(Attestation $attestation): void
9396
{
9497
$attestationCertificateInfo = openssl_x509_parse($attestation->certificate);
95-
Assert::isArray($attestationCertificateInfo['extensions']);
96-
97-
/**
98-
* See {@link https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#136141572641--fulcio} for details
99-
* on the Fulcio extension keys; note the values are DER-encoded strings; the ASN.1 tag is UTF8String (0x0C).
100-
*
101-
* First up, check the extension values are what we expect; these are hard-coded, as we don't expect them
102-
* to change unless the namespace/repo name change, etc.
103-
*/
104-
foreach (self::ATTESTATION_CERTIFICATE_EXPECTED_EXTENSION_VALUES as $extension => $expectedValue) {
105-
Assert::keyExists($attestationCertificateInfo['extensions'], $extension);
106-
Assert::stringNotEmpty($attestationCertificateInfo['extensions'][$extension]);
107-
$actualValue = $attestationCertificateInfo['extensions'][$extension];
108-
109-
// First character (the ASN.1 tag) is expected to be UTF8String (0x0C)
110-
if (ord($actualValue[0]) !== 0x0C) {
111-
throw FailedToVerifyRelease::fromMismatchingExtensionValues($extension, $expectedValue, $actualValue);
112-
}
113-
114-
/**
115-
* Second character is expected to be the length of the actual value
116-
* as long as they are less than 127 bytes (short form)
117-
*
118-
* @link https://www.oss.com/asn1/resources/asn1-made-simple/asn1-quick-reference/basic-encoding-rules.html#Lengths
119-
*/
120-
$derDecodedValue = substr($actualValue, 2, ord($actualValue[1]));
121-
if ($derDecodedValue !== $expectedValue) {
122-
throw FailedToVerifyRelease::fromMismatchingExtensionValues($extension, $expectedValue, $derDecodedValue);
123-
}
124-
}
12598

12699
// @todo process in place to make sure this gets updated frequently enough: gh attestation trusted-root > resources/trusted-root.jsonl
127100
$trustedRootJsonLines = explode("\n", trim(file_get_contents($this->trustedRootFilePath)));
@@ -141,7 +114,11 @@ private function assertCertificateClaims(Attestation $attestation): void
141114
$decoded = json_decode($jsonLine, true);
142115

143116
// No certificate authorities defined in this JSON line, skip it...
144-
if (! is_array($decoded) || ! array_key_exists('certificateAuthorities', $decoded)) {
117+
if (
118+
! is_array($decoded)
119+
|| ! array_key_exists('certificateAuthorities', $decoded)
120+
|| ! is_array($decoded['certificateAuthorities'])
121+
) {
145122
continue;
146123
}
147124

@@ -208,6 +185,46 @@ private function assertCertificateClaims(Attestation $attestation): void
208185
throw FailedToVerifyRelease::fromNoIssuerCertificateInTrustedRoot($attestationCertificateInfo['issuer']);
209186
}
210187

188+
private function assertCertificateExtensionClaims(Attestation $attestation): void
189+
{
190+
$attestationCertificateInfo = openssl_x509_parse($attestation->certificate);
191+
Assert::isArray($attestationCertificateInfo['extensions']);
192+
193+
/**
194+
* See {@link https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#136141572641--fulcio} for details
195+
* on the Fulcio extension keys; note the values are DER-encoded strings; the ASN.1 tag is UTF8String (0x0C).
196+
*
197+
* Check the extension values are what we expect; these are hard-coded, as we don't expect them
198+
* to change unless the namespace/repo name change, etc.
199+
*/
200+
foreach (self::ATTESTATION_CERTIFICATE_EXPECTED_EXTENSION_VALUES as $extension => $expectedValue) {
201+
Assert::keyExists($attestationCertificateInfo['extensions'], $extension);
202+
Assert::stringNotEmpty($attestationCertificateInfo['extensions'][$extension]);
203+
$actualValue = $attestationCertificateInfo['extensions'][$extension];
204+
205+
// First character (the ASN.1 tag) is expected to be UTF8String (0x0C)
206+
if (ord($actualValue[0]) !== 0x0C) {
207+
throw FailedToVerifyRelease::fromMismatchingExtensionValues($extension, $expectedValue, $actualValue);
208+
}
209+
210+
/**
211+
* Second character is expected to be the length of the actual value
212+
* as long as they are less than 127 bytes (short form)
213+
*
214+
* @link https://www.oss.com/asn1/resources/asn1-made-simple/asn1-quick-reference/basic-encoding-rules.html#Lengths
215+
*/
216+
$expectedValueLength = ord($actualValue[1]);
217+
if (strlen($actualValue) !== 2 + $expectedValueLength) {
218+
throw FailedToVerifyRelease::fromInvalidDerEncodedStringLength($actualValue, 2 + $expectedValueLength);
219+
}
220+
221+
$derDecodedValue = substr($actualValue, 2, $expectedValueLength);
222+
if ($derDecodedValue !== $expectedValue) {
223+
throw FailedToVerifyRelease::fromMismatchingExtensionValues($extension, $expectedValue, $derDecodedValue);
224+
}
225+
}
226+
}
227+
211228
private function verifyDsseEnvelopeSignature(ReleaseMetadata $releaseMetadata, int $attestationIndex, Attestation $attestation): void
212229
{
213230
if (! extension_loaded('openssl')) {

0 commit comments

Comments
 (0)