Skip to content

Commit bc53b17

Browse files
authored
fix: normalize ASN.1 BIT STRING key usage bits (#39)
1 parent 956bebc commit bc53b17

3 files changed

Lines changed: 64 additions & 3 deletions

File tree

src/Asn1Decode.sol

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,33 @@ library Asn1Decode {
118118
* @dev Extract value of bitstring node from DER-encoded structure
119119
* @param der The DER-encoded ASN1 structure
120120
* @param ptr Points to the indices of the current node
121-
* @return A bitstring encoded in a uint256
121+
* @return A bitstring encoded in a uint256 with the first payload octet in the least
122+
* significant byte, so X.509 bit masks are stable across multi-octet encodings.
122123
*/
123124
function bitstringUintAt(bytes memory der, Asn1Ptr ptr) internal pure returns (uint256) {
124125
require(der[ptr.header()] == 0x03, "Not type BIT STRING");
126+
require(ptr.length() > 0, "invalid BIT STRING length");
127+
128+
uint256 unusedBits = uint8(der[ptr.content()]);
129+
require(unusedBits <= 7, "invalid BIT STRING padding");
130+
125131
uint256 len = ptr.length() - 1;
126-
return uint256(readBytesN(der, ptr.content() + 1, len) >> ((32 - len) * 8));
132+
require(len <= 32, "BIT STRING too long");
133+
if (len == 0) {
134+
require(unusedBits == 0, "invalid BIT STRING padding");
135+
return 0;
136+
}
137+
138+
if (unusedBits != 0) {
139+
uint8 unusedMask = uint8((uint256(1) << unusedBits) - 1);
140+
require(uint8(der[ptr.content() + len]) & unusedMask == 0, "Non-zero unused BIT STRING bits");
141+
}
142+
143+
uint256 value;
144+
for (uint256 i = 0; i < len; ++i) {
145+
value |= uint256(uint8(der[ptr.content() + 1 + i])) << (i * 8);
146+
}
147+
return value;
127148
}
128149

129150
/*

src/CertManager.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,8 @@ contract CertManager is ICertManager {
469469

470470
function _verifyKeyUsageExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca) internal pure {
471471
uint256 value = certificate.bitstringUintAt(valuePtr);
472-
// bits are reversed (DigitalSignature 0x01 => 0x80, CertSign 0x32 => 0x04)
472+
// X.509 KeyUsage bits are MSB-first. bitstringUintAt keeps the first KeyUsage octet in the
473+
// low byte, so these masks continue to target the same bits for one- or multi-octet values.
473474
if (ca) {
474475
require(value & 0x04 == 0x04, "CertSign must be present");
475476
} else {

test/Asn1Decode.t.sol

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ contract Asn1Harness {
2828
return der.bitstring(der.root()).content();
2929
}
3030

31+
function bitstringUintAtRoot(bytes memory der) external pure returns (uint256) {
32+
return der.bitstringUintAt(der.root());
33+
}
34+
3135
function firstChildHeader(bytes memory der) external pure returns (uint256) {
3236
return der.firstChildOf(der.root()).header();
3337
}
@@ -131,6 +135,41 @@ contract Asn1DecodeTest is Test {
131135
h.bitstringContent(hex"03020100"); // pad byte is 0x01, not 0x00
132136
}
133137

138+
function test_bitstringUintAt_oneByteKeyUsage() public view {
139+
// X.509 KeyUsage bit 0 (digitalSignature): one content byte, 7 unused low bits.
140+
assertEq(h.bitstringUintAtRoot(hex"03020780"), 0x80);
141+
}
142+
143+
function test_bitstringUintAt_twoByteKeyUsageNormalizesFirstOctet() public view {
144+
// X.509 KeyUsage bits 5 and 8 (keyCertSign | decipherOnly). The first content octet must
145+
// remain in the low byte so CertManager's 0x04 keyCertSign mask still targets bit 5.
146+
uint256 keyCertSignAndDecipherOnly = h.bitstringUintAtRoot(hex"0303070480");
147+
assertEq(keyCertSignAndDecipherOnly & 0x04, 0x04, "keyCertSign must stay in low byte");
148+
assertEq(keyCertSignAndDecipherOnly & 0x80, 0, "decipherOnly must not alias digitalSignature");
149+
assertEq(keyCertSignAndDecipherOnly, 0x8004);
150+
}
151+
152+
function test_bitstringUintAt_twoByteDecipherOnlyDoesNotAliasDigitalSignature() public view {
153+
uint256 decipherOnly = h.bitstringUintAtRoot(hex"0303070080");
154+
assertEq(decipherOnly & 0x80, 0, "decipherOnly must not satisfy digitalSignature");
155+
assertEq(decipherOnly, 0x8000);
156+
}
157+
158+
function test_bitstringUintAt_nonZeroUnusedBits_reverts() public {
159+
vm.expectRevert("Non-zero unused BIT STRING bits");
160+
h.bitstringUintAtRoot(hex"03030700ff");
161+
}
162+
163+
function test_bitstringUintAt_invalidUnusedBits_reverts() public {
164+
vm.expectRevert("invalid BIT STRING padding");
165+
h.bitstringUintAtRoot(hex"03020880");
166+
}
167+
168+
function test_bitstringUintAt_missingUnusedBits_reverts() public {
169+
vm.expectRevert("invalid BIT STRING length");
170+
h.bitstringUintAtRoot(hex"0300");
171+
}
172+
134173
// --- firstChildOf ---
135174

136175
function test_firstChildOf_notConstructed_reverts() public {

0 commit comments

Comments
 (0)