Skip to content

Commit 2ca2cf3

Browse files
leopoldjoyOpenCode
andcommitted
fix: reject non-canonical ASN.1 integers
Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
1 parent c0a9f21 commit 2ca2cf3

2 files changed

Lines changed: 94 additions & 16 deletions

File tree

src/Asn1Decode.sol

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,8 @@ library Asn1Decode {
165165
* @return Uint value of node
166166
*/
167167
function uintAt(bytes memory der, Asn1Ptr ptr) internal pure returns (uint256) {
168-
require(der[ptr.header()] == 0x02, "Not type INTEGER");
169-
require(der[ptr.content()] & 0x80 == 0, "Not positive");
170-
uint256 len = ptr.length();
171-
return uint256(readBytesN(der, ptr.content(), len) >> (32 - len) * 8);
168+
(uint256 start, uint256 len) = positiveIntegerContent(der, ptr, 32);
169+
return uint256(readBytesN(der, start, len) >> (32 - len) * 8);
172170
}
173171

174172
/*
@@ -178,19 +176,40 @@ library Asn1Decode {
178176
* @return 384-bit uint encoded in uint128 and uint256
179177
*/
180178
function uint384At(bytes memory der, Asn1Ptr ptr) internal pure returns (uint128, uint256) {
179+
(uint256 start, uint256 valueLength) = positiveIntegerContent(der, ptr, 48);
180+
181+
if (valueLength > 32) {
182+
uint256 hiLen = valueLength - 32;
183+
return (
184+
uint128(uint256(readBytesN(der, start, hiLen) >> (32 - hiLen) * 8)),
185+
uint256(readBytesN(der, start + hiLen, 32))
186+
);
187+
}
188+
189+
return (0, uint256(readBytesN(der, start, valueLength) >> (32 - valueLength) * 8));
190+
}
191+
192+
function positiveIntegerContent(bytes memory der, Asn1Ptr ptr, uint256 maxValueLength)
193+
private
194+
pure
195+
returns (uint256 start, uint256 valueLength)
196+
{
181197
require(der[ptr.header()] == 0x02, "Not type INTEGER");
182-
require(der[ptr.content()] & 0x80 == 0, "Not positive");
183-
uint256 valueLength = ptr.length();
184-
uint256 start = ptr.content();
198+
valueLength = ptr.length();
199+
require(valueLength > 0, "invalid INTEGER length");
200+
start = ptr.content();
201+
185202
if (der[start] == 0) {
186-
start++;
187-
valueLength--;
203+
if (valueLength > 1) {
204+
require(der[start + 1] & 0x80 == 0x80, "non-canonical INTEGER");
205+
start++;
206+
valueLength--;
207+
}
208+
} else {
209+
require(der[start] & 0x80 == 0, "Not positive");
188210
}
189-
uint256 shift = 48 - valueLength;
190-
return (
191-
uint128(uint256(readBytesN(der, start, 16 - shift) >> (128 + shift * 8))),
192-
uint256(readBytesN(der, start + 16 - shift, 32))
193-
);
211+
212+
require(valueLength <= maxValueLength, "invalid INTEGER length");
194213
}
195214

196215
/*

test/Asn1Decode.t.sol

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ contract Asn1Harness {
2020
return der.uintAt(der.root());
2121
}
2222

23+
function uint384AtRoot(bytes memory der) external pure returns (uint128 hi, uint256 lo) {
24+
return der.uint384At(der.root());
25+
}
26+
2327
function timestampAtRoot(bytes memory der) external pure returns (uint256) {
2428
return der.timestampAt(der.root());
2529
}
@@ -68,6 +72,20 @@ contract Asn1DecodeTest is Test {
6872
assertEq(h.uintAtRoot(hex"0203012345"), 0x012345); // INTEGER 0x012345
6973
}
7074

75+
function test_uintAt_requiredLeadingZero() public view {
76+
assertEq(h.uintAtRoot(hex"02020080"), 0x80);
77+
}
78+
79+
function test_uintAt_unnecessaryLeadingZero_reverts() public {
80+
vm.expectRevert("non-canonical INTEGER");
81+
h.uintAtRoot(hex"0202007f");
82+
}
83+
84+
function test_uintAt_empty_reverts() public {
85+
vm.expectRevert("invalid INTEGER length");
86+
h.uintAtRoot(hex"0200");
87+
}
88+
7189
function test_uintAt_notInteger_reverts() public {
7290
vm.expectRevert("Not type INTEGER");
7391
h.uintAtRoot(hex"0401ff"); // OCTET STRING, not INTEGER
@@ -84,6 +102,22 @@ contract Asn1DecodeTest is Test {
84102
h.uintAtRoot(hex"02050000"); // claims 5 content bytes, only 2 present
85103
}
86104

105+
function test_uint384At_requiredLeadingZero() public view {
106+
(uint128 hi, uint256 lo) = h.uint384AtRoot(hex"02020080");
107+
assertEq(hi, 0);
108+
assertEq(lo, 0x80);
109+
}
110+
111+
function test_uint384At_unnecessaryLeadingZero_reverts() public {
112+
vm.expectRevert("non-canonical INTEGER");
113+
h.uint384AtRoot(hex"0202007f");
114+
}
115+
116+
function test_uint384At_empty_reverts() public {
117+
vm.expectRevert("invalid INTEGER length");
118+
h.uint384AtRoot(hex"0200");
119+
}
120+
87121
// --- timestampAt ---
88122

89123
function test_timestamp_utcEpoch() public view {
@@ -187,11 +221,36 @@ contract Asn1DecodeTest is Test {
187221
}
188222

189223
function testFuzz_uintAt_positive(uint64 v) public view {
190-
// INTEGER with an explicit 0x00 sign byte so the value is always positive
191-
bytes memory der = abi.encodePacked(bytes1(0x02), bytes1(0x09), bytes1(0x00), bytes8(v));
224+
bytes memory der = _derEncodeUint64(v);
192225
assertEq(h.uintAtRoot(der), v);
193226
}
194227

228+
function _derEncodeUint64(uint64 v) internal pure returns (bytes memory) {
229+
if (v == 0) {
230+
return hex"020100";
231+
}
232+
233+
bytes8 raw = bytes8(v);
234+
uint256 offset;
235+
while (offset < 8 && raw[offset] == 0) {
236+
offset++;
237+
}
238+
239+
uint256 len = 8 - offset;
240+
bool needsPad = uint8(raw[offset]) >= 0x80;
241+
bytes memory der = new bytes(2 + len + (needsPad ? 1 : 0));
242+
der[0] = 0x02;
243+
der[1] = bytes1(uint8(der.length - 2));
244+
uint256 dst = 2;
245+
if (needsPad) {
246+
der[dst++] = 0x00;
247+
}
248+
for (uint256 i = offset; i < 8; ++i) {
249+
der[dst++] = raw[i];
250+
}
251+
return der;
252+
}
253+
195254
function _utcTime(string memory s) internal pure returns (bytes memory) {
196255
bytes memory b = bytes(s);
197256
require(b.length == 13, "test: UTCTime must be 13 chars");

0 commit comments

Comments
 (0)