Skip to content

Commit 0802191

Browse files
committed
fix: reject non-canonical ASN.1 lengths
1 parent bc53b17 commit 0802191

3 files changed

Lines changed: 57 additions & 0 deletions

File tree

src/Asn1Decode.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ library Asn1Decode {
231231
}
232232

233233
function readNodeLength(bytes memory der, uint256 ix) private pure returns (Asn1Ptr) {
234+
require(ix + 1 < der.length, "truncated ASN.1 header");
234235
require(der[ix] & 0x1f != 0x1f, "ASN.1 tags longer than 1-byte are not supported");
235236
uint256 length;
236237
uint256 ixFirstContentByte;
@@ -239,6 +240,10 @@ library Asn1Decode {
239240
ixFirstContentByte = ix + 2;
240241
} else {
241242
uint8 lengthbytesLength = uint8(der[ix + 1] & 0x7F);
243+
require(lengthbytesLength != 0, "indefinite ASN.1 length");
244+
require(lengthbytesLength <= 32, "ASN.1 length too long");
245+
require(ix + 2 + lengthbytesLength <= der.length, "truncated ASN.1 length");
246+
require(der[ix + 2] != 0, "non-canonical ASN.1 length");
242247
if (lengthbytesLength == 1) {
243248
length = uint8(der[ix + 2]);
244249
} else if (lengthbytesLength == 2) {
@@ -247,8 +252,10 @@ library Asn1Decode {
247252
length = uint256(readBytesN(der, ix + 2, lengthbytesLength) >> (32 - lengthbytesLength) * 8);
248253
require(length <= 2 ** 64 - 1); // bound to max uint64 to be safe
249254
}
255+
require(length >= 128, "non-canonical ASN.1 length");
250256
ixFirstContentByte = ix + 2 + lengthbytesLength;
251257
}
258+
require(ixFirstContentByte + length <= der.length, "ASN.1 node length exceeds input");
252259
return LibAsn1Ptr.toAsn1Ptr(ix, ixFirstContentByte, length);
253260
}
254261

test/Asn1Decode.t.sol

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ contract Asn1DecodeTest is Test {
6262
h.rootLength(hex"0289ffffffffffffffffff"); // INTEGER, 9 length bytes all 0xff
6363
}
6464

65+
function test_root_indefiniteLength_reverts() public {
66+
vm.expectRevert("indefinite ASN.1 length");
67+
h.rootLength(hex"0480"); // DER requires definite lengths
68+
}
69+
70+
function test_root_longFormForShortLength_reverts() public {
71+
vm.expectRevert("non-canonical ASN.1 length");
72+
h.rootLength(hex"04810100"); // length 1 must use short form 0x01
73+
}
74+
75+
function test_root_longFormLeadingZero_reverts() public {
76+
vm.expectRevert("non-canonical ASN.1 length");
77+
h.rootLength(hex"04820080"); // length 128 must be 0x81 0x80, not 0x82 0x00 0x80
78+
}
79+
80+
function test_root_canonicalLongFormLength() public view {
81+
bytes memory der = abi.encodePacked(bytes3(0x048180), new bytes(128));
82+
83+
assertEq(h.rootLength(der), 128);
84+
assertEq(h.rootContent(der), 3);
85+
}
86+
6587
// --- uintAt ---
6688

6789
function test_uintAt_value() public view {

test/hinted/HintedNitroAttestation.t.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,19 @@ contract HintedNitroAttestationTest is Test {
478478
certManager.verifyCACertWithHints(abi.encodePacked(caCert, bytes1(0x00)), parentHash, "");
479479
}
480480

481+
function test_HintedCACertRejectsNonCanonicalOuterLength() public {
482+
bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64()));
483+
(bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation);
484+
NitroValidator.Ptrs memory ptrs = parser.parseAttestation(attestationTbs);
485+
(bytes memory caCert, bytes32 parentHash,) = _firstNonRootCA(attestationTbs, ptrs);
486+
bytes memory nonCanonicalCert = _nonCanonicalOuterSequenceLength(caCert);
487+
bytes32 nonCanonicalHash = keccak256(nonCanonicalCert);
488+
489+
vm.expectRevert("non-canonical ASN.1 length");
490+
certManager.verifyCACertWithHints(nonCanonicalCert, parentHash, "");
491+
assertEq(certManager.loadVerified(nonCanonicalHash).pubKey.length, 0, "non-canonical cert must not cache");
492+
}
493+
481494
function test_HintedTrailingRootBytesCannotPoisonParentCache() public {
482495
bytes memory attestation = _repairMissingPublicKeyBytes(_decodeBase64(_realAttestationB64()));
483496
(bytes memory attestationTbs,) = validator.decodeAttestationTbs(attestation);
@@ -1255,6 +1268,21 @@ contract HintedNitroAttestationTest is Test {
12551268
output[3] = bytes1(uint8(length));
12561269
}
12571270

1271+
function _nonCanonicalOuterSequenceLength(bytes memory der) internal pure returns (bytes memory output) {
1272+
require(der.length >= 4 && der[0] == 0x30 && der[1] == 0x82, "test: expected long sequence");
1273+
1274+
output = new bytes(der.length + 1);
1275+
output[0] = der[0];
1276+
output[1] = 0x83;
1277+
output[2] = 0x00;
1278+
output[3] = der[2];
1279+
output[4] = der[3];
1280+
1281+
for (uint256 i = 4; i < der.length; ++i) {
1282+
output[i + 1] = der[i];
1283+
}
1284+
}
1285+
12581286
function _repairMissingPublicKeyBytes(bytes memory attestation) internal pure returns (bytes memory repaired) {
12591287
// The pasted Base64 sample is missing "ic_" in the CBOR key
12601288
// "public_key", but the key length and outer COSE payload length still

0 commit comments

Comments
 (0)