Skip to content

Commit

Permalink
Store CA in cert cache (ensures that previously verified client certs…
Browse files Browse the repository at this point in the history
… cannot be used to sign child certs)
  • Loading branch information
mdehoog committed Dec 9, 2024
1 parent 07bc300 commit 4cb5774
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 57 deletions.
124 changes: 75 additions & 49 deletions src/CertManager.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
pragma solidity ^0.8.22;

import {Sha2Ext} from "./Sha2Ext.sol";
import {Asn1Decode, Asn1Ptr, LibAsn1Ptr} from "./Asn1Decode.sol";
Expand All @@ -17,8 +17,8 @@ contract CertManager is ICertManager {

// root CA certificate constants (don't store it to reduce contract size)
bytes32 public constant ROOT_CA_CERT_HASH = 0x311d96fcd5c5e0ccf72ef548e2ea7d4c0cd53ad7c4cc49e67471aed41d61f185;
uint256 public constant ROOT_CA_CERT_NOT_AFTER = 2519044085;
int256 public constant ROOT_CA_CERT_MAX_PATH_LEN = -1;
uint64 public constant ROOT_CA_CERT_NOT_AFTER = 2519044085;
int64 public constant ROOT_CA_CERT_MAX_PATH_LEN = -1;
bytes32 public constant ROOT_CA_CERT_SUBJECT_HASH =
0x3c3e2e5f1dd14dee5db88341ba71521e939afdb7881aa24c9f1e1c007a2fa8b6;
bytes public constant ROOT_CA_CERT_PUB_KEY =
Expand All @@ -44,8 +44,10 @@ contract CertManager is ICertManager {
mapping(bytes32 => bytes) public verified;

constructor() {
verified[ROOT_CA_CERT_HASH] = abi.encode(
_saveVerified(
ROOT_CA_CERT_HASH,
CachedCert({
ca: true,
notAfter: ROOT_CA_CERT_NOT_AFTER,
maxPathLen: ROOT_CA_CERT_MAX_PATH_LEN,
subjectHash: ROOT_CA_CERT_SUBJECT_HASH,
Expand All @@ -54,17 +56,8 @@ contract CertManager is ICertManager {
);
}

function verifyCert(bytes memory cert, bool clientCert, bytes32 parentCertHash)
external
returns (CachedCert memory)
{
bytes memory parentCacheBytes = verified[parentCertHash];
require(parentCacheBytes.length != 0, "parent cert unverified");
CachedCert memory parentCache = abi.decode(parentCacheBytes, (CachedCert));
require(parentCache.notAfter >= block.timestamp, "parent cert expired");
bytes32 certHash = keccak256(cert);
require(verified[certHash].length == 0, "cert already verified");
return _verifyCert(cert, certHash, clientCert, parentCache);
function verifyCert(bytes memory cert, bool ca, bytes32 parentCertHash) external returns (CachedCert memory) {
return _verifyCert(cert, keccak256(cert), ca, _loadVerified(parentCertHash));
}

function verifyCertBundle(bytes memory certificate, bytes[] calldata cabundle)
Expand All @@ -75,46 +68,55 @@ contract CertManager is ICertManager {
for (uint256 i = 0; i < cabundle.length; i++) {
bytes32 certHash = keccak256(cabundle[i]);
require(i > 0 || certHash == ROOT_CA_CERT_HASH, "Root CA cert not matching");
parentCache = _verifyCert(cabundle[i], certHash, false, parentCache);
parentCache = _verifyCert(cabundle[i], certHash, true, parentCache);
}
return _verifyCert(certificate, keccak256(certificate), true, parentCache);
return _verifyCert(certificate, keccak256(certificate), false, parentCache);
}

function _verifyCert(bytes memory certificate, bytes32 certHash, bool clientCert, CachedCert memory parentCache)
function _verifyCert(bytes memory certificate, bytes32 certHash, bool ca, CachedCert memory parentCache)
internal
returns (CachedCert memory)
{
if (certHash != ROOT_CA_CERT_HASH) {
require(parentCache.pubKey.length > 0, "parent cert unverified");
require(parentCache.notAfter >= block.timestamp, "parent cert expired");
require(parentCache.ca, "parent cert is not a CA");
require(!ca || parentCache.maxPathLen != 0, "maxPathLen exceeded");
}

// skip verification if already verified
bytes memory cacheBytes = verified[certHash];
CachedCert memory cache;
if (cacheBytes.length != 0) {
cache = abi.decode(cacheBytes, (CachedCert));
CachedCert memory cache = _loadVerified(certHash);
if (cache.pubKey.length != 0) {
require(cache.notAfter >= block.timestamp, "cert expired");
require(cache.ca == ca, "cert is not a CA");
return cache;
}

Asn1Ptr root = certificate.root();
Asn1Ptr tbsCertPtr = certificate.firstChildOf(root);
(uint256 notAfter, int256 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) =
_parseTbs(certificate, tbsCertPtr, clientCert);
(uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey) =
_parseTbs(certificate, tbsCertPtr, ca);

require(parentCache.subjectHash == issuerHash, "issuer / subject mismatch");

// constrain maxPathLen to parent's maxPathLen-1
if (parentCache.maxPathLen > 0 && (maxPathLen < 0 || maxPathLen >= parentCache.maxPathLen)) {
maxPathLen = parentCache.maxPathLen - 1;
}
require(clientCert || parentCache.maxPathLen != 0, "maxPathLen exceeded");

_verifyCertSignature(certificate, tbsCertPtr, parentCache.pubKey);

cache = CachedCert({notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey});
verified[certHash] = abi.encode(cache);
cache =
CachedCert({ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey});
_saveVerified(certHash, cache);

return cache;
}

function _parseTbs(bytes memory certificate, Asn1Ptr ptr, bool clientCert)
function _parseTbs(bytes memory certificate, Asn1Ptr ptr, bool ca)
internal
view
returns (uint256 notAfter, int256 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey)
returns (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey)
{
Asn1Ptr versionPtr = certificate.firstChildOf(ptr);
Asn1Ptr vPtr = certificate.firstChildOf(versionPtr);
Expand All @@ -126,13 +128,13 @@ contract CertManager is ICertManager {
// as extensions are used in cert, version should be 3 (value 2) as per https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.1
require(version == 2, "version should be 3");

(notAfter, maxPathLen, issuerHash, subjectHash, pubKey) = _parseTbsInner(certificate, sigAlgoPtr, clientCert);
(notAfter, maxPathLen, issuerHash, subjectHash, pubKey) = _parseTbsInner(certificate, sigAlgoPtr, ca);
}

function _parseTbsInner(bytes memory certificate, Asn1Ptr sigAlgoPtr, bool clientCert)
function _parseTbsInner(bytes memory certificate, Asn1Ptr sigAlgoPtr, bool ca)
internal
view
returns (uint256 notAfter, int256 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey)
returns (uint64 notAfter, int64 maxPathLen, bytes32 issuerHash, bytes32 subjectHash, bytes memory pubKey)
{
Asn1Ptr issuerPtr = certificate.nextSiblingOf(sigAlgoPtr);
issuerHash = certificate.keccak(issuerPtr.content(), issuerPtr.length());
Expand All @@ -152,7 +154,7 @@ contract CertManager is ICertManager {
}

notAfter = _verifyValidity(certificate, validityPtr);
maxPathLen = _verifyExtensions(certificate, extensionsPtr, clientCert);
maxPathLen = _verifyExtensions(certificate, extensionsPtr, ca);
pubKey = _parsePubKey(certificate, subjectPublicKeyInfoPtr);
}

Expand Down Expand Up @@ -180,21 +182,21 @@ contract CertManager is ICertManager {
subjectPubKey = certificate.slice(end - 96, 96);
}

function _verifyValidity(bytes memory certificate, Asn1Ptr validityPtr) internal view returns (uint256 notAfter) {
function _verifyValidity(bytes memory certificate, Asn1Ptr validityPtr) internal view returns (uint64 notAfter) {
Asn1Ptr notBeforePtr = certificate.firstChildOf(validityPtr);
Asn1Ptr notAfterPtr = certificate.nextSiblingOf(notBeforePtr);

uint256 notBefore = certificate.timestampAt(notBeforePtr);
notAfter = certificate.timestampAt(notAfterPtr);
notAfter = uint64(certificate.timestampAt(notAfterPtr));

require(notBefore <= block.timestamp, "certificate not valid yet");
require(notAfter >= block.timestamp, "certificate not valid anymore");
}

function _verifyExtensions(bytes memory certificate, Asn1Ptr extensionsPtr, bool clientCert)
function _verifyExtensions(bytes memory certificate, Asn1Ptr extensionsPtr, bool ca)
internal
pure
returns (int256 maxPathLen)
returns (int64 maxPathLen)
{
require(certificate[extensionsPtr.header()] == 0xa3, "invalid extensions");
extensionsPtr = certificate.firstChildOf(extensionsPtr);
Expand All @@ -221,10 +223,10 @@ contract CertManager is ICertManager {

if (oid == BASIC_CONSTRAINTS_OID) {
basicConstraintsFound = true;
maxPathLen = _verifyBasicConstraintsExtension(certificate, valuePtr, clientCert);
maxPathLen = _verifyBasicConstraintsExtension(certificate, valuePtr, ca);
} else {
keyUsageFound = true;
_verifyKeyUsageExtension(certificate, valuePtr, clientCert);
_verifyKeyUsageExtension(certificate, valuePtr, ca);
}
}

Expand All @@ -236,13 +238,13 @@ contract CertManager is ICertManager {

require(basicConstraintsFound, "basicConstraints not found");
require(keyUsageFound, "keyUsage not found");
require(!clientCert || maxPathLen == -1, "maxPathLen must be undefined for client cert");
require(ca || maxPathLen == -1, "maxPathLen must be undefined for client cert");
}

function _verifyBasicConstraintsExtension(bytes memory certificate, Asn1Ptr valuePtr, bool clientCert)
function _verifyBasicConstraintsExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca)
internal
pure
returns (int256 maxPathLen)
returns (int64 maxPathLen)
{
maxPathLen = -1;
Asn1Ptr basicConstraintsPtr = certificate.firstChildOf(valuePtr);
Expand All @@ -252,20 +254,19 @@ contract CertManager is ICertManager {
isCA = certificate[basicConstraintsPtr.content()] == 0xff;
basicConstraintsPtr = certificate.nextSiblingOf(basicConstraintsPtr);
}
// check isCA bool is equivalent to !clientCert
require(clientCert != isCA, "isCA must be opposite to clientCert");
require(ca == isCA, "isCA must be true for CA certs");
if (certificate[basicConstraintsPtr.header()] == 0x02) {
maxPathLen = int256(certificate.uintAt(basicConstraintsPtr));
maxPathLen = int64(uint64(certificate.uintAt(basicConstraintsPtr)));
}
}

function _verifyKeyUsageExtension(bytes memory certificate, Asn1Ptr valuePtr, bool clientCert) internal pure {
function _verifyKeyUsageExtension(bytes memory certificate, Asn1Ptr valuePtr, bool ca) internal pure {
uint256 value = certificate.bitstringUintAt(valuePtr);
// bits are reversed (DigitalSignature 0x01 => 0x80, CertSign 0x32 => 0x04)
if (clientCert) {
require(value & 0x80 == 0x80, "DigitalSignature must be present");
} else {
if (ca) {
require(value & 0x04 == 0x04, "CertSign must be present");
} else {
require(value & 0x80 == 0x80, "DigitalSignature must be present");
}
}

Expand All @@ -290,4 +291,29 @@ contract CertManager is ICertManager {
function _verifySignature(bytes memory pubKey, bytes memory hash, bytes memory sig) internal view {
require(ECDSA384.verify(ECDSA384Curve.p384(), hash, sig, pubKey), "invalid sig");
}

function _saveVerified(bytes32 certHash, CachedCert memory cache) internal {
verified[certHash] =
abi.encodePacked(cache.ca, cache.notAfter, cache.maxPathLen, cache.subjectHash, cache.pubKey);
}

function _loadVerified(bytes32 certHash) internal view returns (CachedCert memory) {
bytes memory packed = verified[certHash];
if (packed.length == 0) {
return CachedCert({ca: false, notAfter: 0, maxPathLen: 0, subjectHash: 0, pubKey: ""});
}
bool ca;
uint64 notAfter;
int64 maxPathLen;
bytes32 subjectHash;
assembly {
ca := mload(add(packed, 0x1))
notAfter := mload(add(packed, 0x9))
maxPathLen := mload(add(packed, 0x11))
subjectHash := mload(add(packed, 0x31))
}
bytes memory pubKey = packed.slice(0x31, packed.length - 0x31);
return
CachedCert({ca: ca, notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey});
}
}
5 changes: 3 additions & 2 deletions src/ICertManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {LibBytes} from "./LibBytes.sol";

interface ICertManager {
struct CachedCert {
uint256 notAfter;
int256 maxPathLen;
bool ca;
uint64 notAfter;
int64 maxPathLen;
bytes32 subjectHash;
bytes pubKey;
}
Expand Down
2 changes: 1 addition & 1 deletion test/CertManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ contract CertManagerTest is Test {
hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6";
bytes memory cert =
hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4";
certManager.verifyCert(cert, false, keccak256(parent));
certManager.verifyCert(cert, true, keccak256(parent));
}
}
9 changes: 4 additions & 5 deletions test/RootCertCheck.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ contract RootCertCheckTest is Test, CertManager {
function test_ParseCert() public view {
Asn1Ptr root = ROOT_CA_CERT.root();
Asn1Ptr tbsCertPtr = ROOT_CA_CERT.firstChildOf(root);
(uint256 notAfter, int256 maxPathLen,, bytes32 subjectHash, bytes memory pubKey) =
_parseTbs(ROOT_CA_CERT, tbsCertPtr, false);
CachedCert memory cert =
CachedCert({notAfter: notAfter, maxPathLen: maxPathLen, subjectHash: subjectHash, pubKey: pubKey});
(uint64 notAfter, int64 maxPathLen,, bytes32 subjectHash, bytes memory pubKey) =
_parseTbs(ROOT_CA_CERT, tbsCertPtr, true);

bytes32 certHash = keccak256(ROOT_CA_CERT);
assertEq(verified[certHash], abi.encode(cert));
bytes memory cache = abi.encodePacked(true, notAfter, maxPathLen, subjectHash, pubKey);
assertEq(verified[certHash], cache);
}
}

0 comments on commit 4cb5774

Please sign in to comment.