Skip to content

Commit 55860bc

Browse files
authored
fix: uint384At misreads DER integers shorter than 48 bytes (#25)
* fix: uint384At misreads DER integers shorter than 48 bytes A P-384 ECDSA signature component (r or s) whose canonical 48-byte big-endian representation starts with 0x00 is DER-encoded with fewer than 48 bytes. After stripping the DER sign-padding byte, uint384At was left with valueLength < 48. It applied a compensating right-shift only to the lo word, leaving hi reading one byte too far into the value: Buggy: hi = readBytesN(start, 16) >> 128 -- absorbs byte 15 into hi lo = readBytesN(start+16, vl-16) >> shift -- shifts lo right, drops last byte Fixed: shift = 48 - valueLength hi = readBytesN(start, 16-shift) >> (128 + shift*8) lo = readBytesN(start + 16-shift, 32) The corrupted scalar is passed to ECDSA384.verify, which correctly rejects it, causing CertManager._verifyCertSignature to revert with "invalid sig". This makes CertManager.verifyCACert non-deterministically fail for ~1/256 of all P-384 signature components (~1/128 per cert, ~3% per full attestation chain). Regression tests use the live cabundle[3] from a 2026-04-02 Base Sepolia dev attestation that reproducibly triggered the on-chain revert. Made-with: Cursor * add fuzz test
1 parent 75c1145 commit 55860bc

2 files changed

Lines changed: 109 additions & 2 deletions

File tree

src/Asn1Decode.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,10 @@ library Asn1Decode {
165165
start++;
166166
valueLength--;
167167
}
168+
uint256 shift = 48 - valueLength;
168169
return (
169-
uint128(uint256(readBytesN(der, start, 16) >> 128)),
170-
uint256(readBytesN(der, start + 16, valueLength - 16) >> (48 - valueLength) * 8)
170+
uint128(uint256(readBytesN(der, start, 16 - shift) >> (128 + shift * 8))),
171+
uint256(readBytesN(der, start + 16 - shift, 32))
171172
);
172173
}
173174

test/CertManager.t.sol

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,29 @@ pragma solidity ^0.8.13;
33

44
import {Test, console} from "forge-std/Test.sol";
55
import {CertManager} from "../src/CertManager.sol";
6+
import {Asn1Decode, LibAsn1Ptr, Asn1Ptr} from "../src/Asn1Decode.sol";
7+
8+
contract Asn1DecodeHarness {
9+
using Asn1Decode for bytes;
10+
11+
function uint384At(bytes calldata der, uint256 header, uint256 content, uint256 length)
12+
external
13+
pure
14+
returns (uint128 hi, uint256 lo)
15+
{
16+
Asn1Ptr ptr = LibAsn1Ptr.toAsn1Ptr(header, content, length);
17+
return der.uint384At(ptr);
18+
}
19+
}
620

721
contract CertManagerTest is Test {
822
CertManager public certManager;
23+
Asn1DecodeHarness public harness;
924

1025
function setUp() public {
1126
vm.warp(1732580000);
1227
certManager = new CertManager();
28+
harness = new Asn1DecodeHarness();
1329
}
1430

1531
function test_VerifyCertBundle() public {
@@ -38,4 +54,94 @@ contract CertManagerTest is Test {
3854
hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4";
3955
certManager.verifyCACert(cert, keccak256(parent));
4056
}
57+
58+
// 's' INTEGER from cabundle[3] (2026-04-02 attestation): DER-encoded with a 0x00
59+
// sign-padding byte, leaving valueLength=47. Verifies hi/lo are correctly zero-padded
60+
// to the full 48-byte scalar rather than packed flush against the stripped bytes.
61+
function test_uint384At_Short47Bytes() public view {
62+
bytes memory der =
63+
hex"023000caf59019bfbcc6f6ed365e5a892ceaa2eda9c549dc01460f5fe650814ebe0e7ee855d3bcffde95afd2e82e21df0eac";
64+
(uint128 hi, uint256 lo) = harness.uint384At(der, 0, 2, 48);
65+
assertEq(uint8(hi >> 120), 0x00, "hi[0]: expected 0x00 (unfixed: 0xca)");
66+
assertEq(uint8(lo >> 248), 0xa2, "lo[0]: expected 0xa2 (unfixed: 0x00, byte absorbed into hi)");
67+
assertEq(hi, uint128(0x00caf59019bfbcc6f6ed365e5a892cea));
68+
assertEq(lo, 0xa2eda9c549dc01460f5fe650814ebe0e7ee855d3bcffde95afd2e82e21df0eac);
69+
}
70+
71+
// Cert chain from the 2026-04-02 ~15:35 UTC dev attestation that produced the live revert.
72+
bytes constant CB0 =
73+
hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6";
74+
bytes constant CB1 =
75+
hex"308202c030820245a003020102021100e9773f7085209425e1de229b000f01f1300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3236303333313035303734355a170d3236303432303036303734355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d316262313063316530323564363536632e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004cd607e358d0c0d7fb6cb74700df27dd799f46a8b3e1e106c2ba4e962b8e1869fb8ee1929a0543e8cf5fe36eb8c85decb1a8e680fde0427d222cd2c43f6103d4f651b98770d9f52439963a83d65ebc45e17f8d16ccb76e343163429e4f4dca36da381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e0416041449ed5cf5770acf285a97690e5027f21b81c1bf0d300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fad8167b186ede6765e2fee311718e1c2dfde817a280a316bab19008eb15356795525d52458bdec9818ea7c1f26d054c023100de456629f7833285dc26f68e74b206b83bf39846a76271c7c364c6fc2c00db5da4b435a92cd85963eab5e1b58d049b49";
76+
bytes constant CB2 =
77+
hex"308203133082029aa003020102021038fed4b0c2167f2e947b381791999d87300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d316262313063316530323564363536632e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303430323037303834365a170d3236303430383036303834365a308189313c303a06035504030c33396162323162376434663562373139622e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b81040022036200041448bacb44a37937f8558d861442956c0c4482f28bd5ced8af0a2a7ec153cb22ab709ef75b37cf273ba2a3415dae125fa5aac4aa063e833c86df6cb68141b35284afb95279a301fadcad98e4edf5740a8461646b31ce52cbba81638395da26b7a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d2304183016801449ed5cf5770acf285a97690e5027f21b81c1bf0d301d0603551d0e041604146a2a38eb4940a886ad9e57e4b7da9dde7f4eb700300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f63336535353365322d363936632d346635302d386233382d6435666165663036306633312e63726c300a06082a8648ce3d0403030367003064023012012108aa4f9678c965ca3746037a230d07c2bc39f3d7ebc9832fbad0e5277c9642d61d3245ba45d741bd88c04692fa02307a4a42535408ed65e5db2bea9aefff587d9187788b715843295efcbbf710d7f65301476ed4338b4755cd3c16b0e12d6a";
78+
bytes constant CB3 =
79+
hex"308202bf30820245a003020102021500f2a021cc6a8466d5fe18e8f487b975f35c7c5b05300a06082a8648ce3d040303308189313c303a06035504030c33396162323162376434663562373139622e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3236303430323135303431355a170d3236303430333135303431355a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30396535656634623664666630323535622e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b810400220362000420ecb0a645c3e6b4437c1c4149c3f1d0af3e90f0343b0e1e3a47d8b314420266843c345a1e64b01b43331f911ab537b78b355a2419eeb00f1ecd6d4fbd7186ab41603ff2a0430cab7b597362a1054ed4cb8ff32dd76b2c108224b42648bbf026a366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e0416041499b2d026ad0212b670d2550fb292ca51fdbf8d66301f0603551d230418301680146a2a38eb4940a886ad9e57e4b7da9dde7f4eb700300a06082a8648ce3d040303036800306502310096ca96c46b5a05de3ad9ecdeaab8670916137461d306cf2fcd8a308885eb6063d96de2e28a1a4ad8c2214e1d1479b5b8023000caf59019bfbcc6f6ed365e5a892ceaa2eda9c549dc01460f5fe650814ebe0e7ee855d3bcffde95afd2e82e21df0eac";
80+
81+
// CB3's 's' component has a 47-byte DER encoding; unpatched code reverts with "invalid sig".
82+
function test_VerifyCACert_ShortS_Regression() public {
83+
vm.warp(1775145600);
84+
CertManager cm = new CertManager();
85+
cm.verifyCACert(CB0, 0);
86+
cm.verifyCACert(CB1, keccak256(CB0));
87+
cm.verifyCACert(CB2, keccak256(CB1));
88+
cm.verifyCACert(CB3, keccak256(CB2)); // reverts with "invalid sig" on unpatched code
89+
}
90+
91+
function testFuzz_uint384At_LeadingZeros(uint8 numZeros, uint128 hiSeed, uint256 loSeed) public view {
92+
numZeros = uint8(bound(numZeros, 0, 16));
93+
94+
uint128 expectedHi;
95+
uint256 expectedLo;
96+
97+
if (numZeros == 16) {
98+
expectedHi = 0;
99+
expectedLo = loSeed | (uint256(1) << 248);
100+
} else {
101+
uint128 mask = type(uint128).max >> (numZeros * 8);
102+
expectedHi = (hiSeed & mask) | (uint128(1) << ((15 - numZeros) * 8));
103+
expectedLo = loSeed;
104+
}
105+
106+
bytes memory scalar48 = abi.encodePacked(expectedHi, expectedLo);
107+
bytes memory der = _derEncodeP384Integer(scalar48);
108+
109+
uint256 contentLen = uint256(uint8(der[1]));
110+
(uint128 hi, uint256 lo) = harness.uint384At(der, 0, 2, contentLen);
111+
112+
assertEq(hi, expectedHi);
113+
assertEq(lo, expectedLo);
114+
}
115+
116+
function _derEncodeP384Integer(bytes memory scalar48) internal pure returns (bytes memory) {
117+
require(scalar48.length == 48);
118+
119+
uint256 i = 0;
120+
while (i < 48 && scalar48[i] == 0) {
121+
i++;
122+
}
123+
124+
if (i == 48) return hex"020100";
125+
126+
uint256 minLen = 48 - i;
127+
bool needsPad = uint8(scalar48[i]) >= 0x80;
128+
uint256 contentLen = needsPad ? minLen + 1 : minLen;
129+
130+
bytes memory der = new bytes(2 + contentLen);
131+
der[0] = 0x02;
132+
der[1] = bytes1(uint8(contentLen));
133+
134+
uint256 offset = 2;
135+
if (needsPad) {
136+
der[offset] = 0x00;
137+
offset++;
138+
}
139+
140+
for (uint256 j = i; j < 48; j++) {
141+
der[offset] = scalar48[j];
142+
offset++;
143+
}
144+
145+
return der;
146+
}
41147
}

0 commit comments

Comments
 (0)