Skip to content

Commit 18ac647

Browse files
authored
fix: bound BasicConstraints ASN.1 parsing (#41)
* fix: bound basic constraints parsing * fix: reject empty basic constraints path length
1 parent fe30a6a commit 18ac647

2 files changed

Lines changed: 88 additions & 8 deletions

File tree

src/CertManager.sol

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -477,18 +477,49 @@ contract CertManager is ICertManager {
477477
pure
478478
returns (int64 maxPathLen)
479479
{
480+
require(certificate[valuePtr.header()] == 0x30, "invalid basicConstraints");
481+
480482
maxPathLen = -1;
481-
Asn1Ptr basicConstraintsPtr = certificate.firstChildOf(valuePtr);
482483
bool isCA;
483-
if (certificate[basicConstraintsPtr.header()] == 0x01) {
484-
require(basicConstraintsPtr.length() == 1, "invalid isCA bool value");
485-
isCA = certificate[basicConstraintsPtr.content()] == 0xff;
486-
basicConstraintsPtr = certificate.nextSiblingOf(basicConstraintsPtr);
484+
uint256 end = valuePtr.content() + valuePtr.length();
485+
uint256 cursor = valuePtr.content();
486+
487+
if (cursor < end) {
488+
Asn1Ptr basicConstraintsPtr = certificate.firstChildOf(valuePtr);
489+
cursor = _requireAsn1ChildWithin(basicConstraintsPtr, end);
490+
491+
if (certificate[basicConstraintsPtr.header()] == 0x01) {
492+
require(basicConstraintsPtr.length() == 1, "invalid isCA bool value");
493+
isCA = certificate[basicConstraintsPtr.content()] == 0xff;
494+
495+
if (cursor == end) {
496+
require(ca == isCA, "isCA must be true for CA certs");
497+
return maxPathLen;
498+
}
499+
500+
basicConstraintsPtr = certificate.nextSiblingOf(basicConstraintsPtr);
501+
cursor = _requireAsn1ChildWithin(basicConstraintsPtr, end);
502+
}
503+
504+
require(ca == isCA, "isCA must be true for CA certs");
505+
506+
if (certificate[basicConstraintsPtr.header()] == 0x02) {
507+
require(basicConstraintsPtr.length() > 0, "invalid pathLenConstraint");
508+
maxPathLen = int64(uint64(certificate.uintAt(basicConstraintsPtr)));
509+
} else {
510+
revert("invalid basicConstraints field");
511+
}
512+
513+
require(cursor == end, "trailing basicConstraints fields");
514+
return maxPathLen;
487515
}
516+
488517
require(ca == isCA, "isCA must be true for CA certs");
489-
if (certificate[basicConstraintsPtr.header()] == 0x02) {
490-
maxPathLen = int64(uint64(certificate.uintAt(basicConstraintsPtr)));
491-
}
518+
}
519+
520+
function _requireAsn1ChildWithin(Asn1Ptr ptr, uint256 parentEnd) internal pure returns (uint256 childEnd) {
521+
childEnd = ptr.header() + ptr.totalLength();
522+
require(childEnd <= parentEnd, "basicConstraints out of bounds");
492523
}
493524

494525
function _verifyKeyUsageExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca) internal pure {

test/CertManager.t.sol

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,27 @@ contract Asn1DecodeHarness {
2222
}
2323
}
2424

25+
contract CertManagerHarness is CertManager {
26+
using Asn1Decode for bytes;
27+
28+
constructor() CertManager(new P384Verifier()) {}
29+
30+
function verifyBasicConstraints(bytes memory der, bool ca) external pure returns (int64) {
31+
return _verifyBasicConstraintsExtension(der, der.root(), ca);
32+
}
33+
}
34+
2535
contract CertManagerTest is Test {
2636
using Asn1Decode for bytes;
2737
using LibAsn1Ptr for Asn1Ptr;
2838
using LibBytes for bytes;
2939

3040
Asn1DecodeHarness public harness;
41+
CertManagerHarness public certManagerHarness;
3142

3243
function setUp() public {
3344
harness = new Asn1DecodeHarness();
45+
certManagerHarness = new CertManagerHarness();
3446
}
3547

3648
// 's' INTEGER from cabundle[3] (2026-04-02 attestation): DER-encoded with a 0x00
@@ -46,6 +58,43 @@ contract CertManagerTest is Test {
4658
assertEq(lo, 0xa2eda9c549dc01460f5fe650814ebe0e7ee855d3bcffde95afd2e82e21df0eac);
4759
}
4860

61+
function test_BasicConstraintsEmptySequenceIsClientCert() public view {
62+
assertEq(int256(certManagerHarness.verifyBasicConstraints(hex"3000", false)), -1);
63+
}
64+
65+
function test_BasicConstraintsEmptySequenceRejectsCACert() public {
66+
vm.expectRevert("isCA must be true for CA certs");
67+
certManagerHarness.verifyBasicConstraints(hex"3000", true);
68+
}
69+
70+
function test_BasicConstraintsAcceptsCAWithoutPathLen() public view {
71+
assertEq(int256(certManagerHarness.verifyBasicConstraints(hex"30030101ff", true)), -1);
72+
}
73+
74+
function test_BasicConstraintsAcceptsCAWithPathLen() public view {
75+
assertEq(int256(certManagerHarness.verifyBasicConstraints(hex"30060101ff020100", true)), 0);
76+
}
77+
78+
function test_BasicConstraintsRejectsEmptyPathLen() public {
79+
vm.expectRevert("invalid pathLenConstraint");
80+
certManagerHarness.verifyBasicConstraints(hex"30050101ff0200", true);
81+
}
82+
83+
function test_BasicConstraintsRejectsOutOfBoundsChild() public {
84+
vm.expectRevert("basicConstraints out of bounds");
85+
certManagerHarness.verifyBasicConstraints(hex"3003020200", false);
86+
}
87+
88+
function test_BasicConstraintsRejectsTrailingFields() public {
89+
vm.expectRevert("trailing basicConstraints fields");
90+
certManagerHarness.verifyBasicConstraints(hex"30090101ff020100020100", true);
91+
}
92+
93+
function test_BasicConstraintsRejectsUnknownField() public {
94+
vm.expectRevert("invalid basicConstraints field");
95+
certManagerHarness.verifyBasicConstraints(hex"30020400", false);
96+
}
97+
4998
// Cert chain from the 2026-04-02 ~15:35 UTC dev attestation that produced the live revert.
5099
// CB0 is the AWS Nitro root (keccak256(CB0) == CertManager.ROOT_CA_CERT_HASH, pinned in the
51100
// constructor), so the chain is verified starting from CB1.

0 commit comments

Comments
 (0)