Skip to content

fix: uint384At misreads DER integers shorter than 48 bytes#25

Merged
leopoldjoy merged 2 commits into
base:mainfrom
robriks:fix/uint384at-short-integer
Jun 3, 2026
Merged

fix: uint384At misreads DER integers shorter than 48 bytes#25
leopoldjoy merged 2 commits into
base:mainfrom
robriks:fix/uint384at-short-integer

Conversation

@robriks
Copy link
Copy Markdown
Contributor

@robriks robriks commented Apr 2, 2026

Summary

uint384At in Asn1Decode.sol incorrectly parses P-384 ECDSA signature components when their DER encoding is shorter than 48 bytes. This causes CertManager._verifyCertSignature to receive a corrupted scalar, ECDSA384.verify to return false, and the transaction to revert with "invalid sig".

Root cause

A P-384 ECDSA scalar is a 48-byte big-endian integer. When the scalar's most-significant byte is 0x00, DER omits it, encoding only 47 bytes. uint384At strips any DER sign-padding byte and then splits the remaining bytes into (hi: uint128, lo: uint256). The split must treat the value as zero-padded to exactly 48 bytes. The unfixed code does not do this for hi:

Unfixed (valueLength = 47, shift = 1):

hi = readBytesN(der, start, 16)              // reads bytes 0–15 of the 47-byte value — no leading 0x00
lo = readBytesN(der, start+16, 31) >> 8      // shifts right; byte 15 absorbed into hi, byte 46 lost

Fixed:

hi = readBytesN(der, start, 15) >> 136       // right-aligns 15 bytes in uint128, implicitly prepending 0x00
lo = readBytesN(der, start+15, 32)           // reads the correct trailing 32 bytes

For the concrete cert that triggered this (CB3 in the regression test), the s component's DER-encoded value was 47 bytes. The unfixed code produced:

Unfixed Correct
hi 0xcaf59019bfbcc6f6ed365e5a892ceaa2 0x00caf59019bfbcc6f6ed365e5a892cea
lo[0] 0x00… (0xa2 absorbed into hi) 0xa2…

Probability

The bug fires whenever the most-significant byte of an ECDSA scalar is exactly 0x00. For a uniformly distributed 48-byte scalar this occurs with probability 1/256 ≈ 0.4% per scalar. Each cert signature has two scalars (r and s); a typical Nitro attestation chain contains 4 certs — 8 scalars total:

P(at least one hit) = 1 − (255/256)^8 ≈ 3%

Processing a meaningful volume of attestations should likely encounter this revert eventually.

Reproduction

# Unit test — verifies hi/lo are correctly zero-padded for the real 47-byte DER encoding
forge test --match-test test_uint384At_Short47Bytes

# Integration test — full 4-cert chain reverts with "invalid sig" on unpatched code
forge test --match-test test_VerifyCACert_ShortS_Regression

Both tests fail on unpatched code and pass after this fix. The cert data embedded in the tests is the exact chain from the 2026-04-02 ~15:35 UTC dev enclave attestation that produced the live revert.

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
Comment thread test/CertManager.t.sol Outdated
@robriks robriks requested a review from leopoldjoy April 7, 2026 01:41
@ambery18
Copy link
Copy Markdown

Good catch. Nice!

@leopoldjoy leopoldjoy merged commit 55860bc into base:main Jun 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants