Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/CertManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ contract CertManager is ICertManager {
using LibBytes for bytes;

event CertVerified(bytes32 indexed certHash);
event CertHashCached(bytes32 indexed certHash);

// Cached SHA-384 hashes for certificate TBS data, keyed by keccak256(cert).
// Allows splitting the expensive SHA-384 + ECDSA384.verify across two transactions
// to stay within the EIP-7825 per-transaction gas limit (2^24 gas).
mapping(bytes32 => bytes) public certSha384Cache;

// root CA certificate constants (don't store it to reduce contract size)
bytes32 public constant ROOT_CA_CERT_HASH = 0x311d96fcd5c5e0ccf72ef548e2ea7d4c0cd53ad7c4cc49e67471aed41d61f185;
Expand Down Expand Up @@ -61,6 +67,21 @@ contract CertManager is ICertManager {
);
}

/// @notice Pre-computes and caches the SHA-384 hash of a certificate's TBS (to-be-signed) data.
/// @dev Call this in a separate transaction before verifyCACert/verifyClientCert to split
/// the gas cost. Necessary on networks with per-transaction gas limits (e.g. EIP-7825).
/// @param cert The DER-encoded certificate.
function cacheCertHash(bytes memory cert) external {
bytes32 key = keccak256(cert);
require(certSha384Cache[key].length == 0, "cert hash already cached");

Asn1Ptr root = cert.root();
Asn1Ptr tbsCertPtr = cert.firstChildOf(root);
certSha384Cache[key] = Sha2Ext.sha384(cert, tbsCertPtr.header(), tbsCertPtr.totalLength());

emit CertHashCached(key);
}

function verifyCACert(bytes memory cert, bytes32 parentCertHash) external returns (bytes32) {
bytes32 certHash = keccak256(cert);
_verifyCert(cert, certHash, true, _loadVerified(parentCertHash));
Expand Down Expand Up @@ -271,11 +292,11 @@ contract CertManager is ICertManager {
}
}

function _verifyCertSignature(bytes memory certificate, Asn1Ptr ptr, bytes memory pubKey) internal view {
function _verifyCertSignature(bytes memory certificate, Asn1Ptr ptr, bytes memory pubKey) internal {
Asn1Ptr sigAlgoPtr = certificate.nextSiblingOf(ptr);
require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo");

bytes memory hash = Sha2Ext.sha384(certificate, ptr.header(), ptr.totalLength());
bytes memory hash = _getOrComputeCertHash(certificate, ptr);

Asn1Ptr sigPtr = certificate.nextSiblingOf(sigAlgoPtr);
Asn1Ptr sigBPtr = certificate.bitstring(sigPtr);
Expand All @@ -289,6 +310,18 @@ contract CertManager is ICertManager {
_verifySignature(pubKey, hash, sigPacked);
}

/// @dev Returns the cached SHA-384 hash if available, otherwise computes it inline.
/// Deletes the cache entry after use to reclaim storage and prevent stale entries.
function _getOrComputeCertHash(bytes memory certificate, Asn1Ptr ptr) internal returns (bytes memory) {
bytes32 key = keccak256(certificate);
bytes memory cached = certSha384Cache[key];
if (cached.length != 0) {
delete certSha384Cache[key];
return cached;
}
return Sha2Ext.sha384(certificate, ptr.header(), ptr.totalLength());
}

function _verifySignature(bytes memory pubKey, bytes memory hash, bytes memory sig) internal view {
require(ECDSA384.verify(ECDSA384Curve.p384(), hash, sig, pubKey), "invalid sig");
}
Expand Down
34 changes: 33 additions & 1 deletion src/NitroValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,32 @@ contract NitroValidator {
CborElement nonce;
}

// Cached SHA-384 hashes for attestation TBS data, keyed by keccak256(attestationTbs).
// This allows splitting the expensive SHA-384 + ECDSA384.verify operations across
// two transactions to stay within the EIP-7825 per-transaction gas limit (2^24 gas).
mapping(bytes32 => bytes) public sha384Cache;

event AttestationHashCached(bytes32 indexed attestationTbsHash);

ICertManager public immutable certManager;

constructor(ICertManager _certManager) {
certManager = _certManager;
}

/// @notice Pre-computes and caches the SHA-384 hash of an attestation TBS.
/// @dev Call this in a separate transaction before validateAttestation() to split the
/// gas cost across two transactions. This is necessary on networks with per-transaction
/// gas limits (e.g. Ethereum L1 post-EIP-7825) where computing SHA-384 (~8.4M gas)
/// and ECDSA384.verify (~7.8M gas) together exceeds the limit.
/// @param attestationTbs The attestation to-be-signed data.
function cacheAttestationHash(bytes memory attestationTbs) external {
bytes32 key = keccak256(attestationTbs);
require(sha384Cache[key].length == 0, "hash already cached");
sha384Cache[key] = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length);
emit AttestationHashCached(key);
}

function decodeAttestationTbs(bytes memory attestation)
external
pure
Expand Down Expand Up @@ -99,7 +119,7 @@ contract NitroValidator {
}

ICertManager.VerifiedCert memory parent = verifyCertBundle(cert, cabundle);
bytes memory hash = Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length);
bytes memory hash = _getOrComputeHash(attestationTbs);
_verifySignature(parent.pubKey, hash, signature);

return ptrs;
Expand Down Expand Up @@ -202,6 +222,18 @@ contract NitroValidator {
return ptrs;
}

/// @dev Returns the cached SHA-384 hash if available, otherwise computes it inline.
/// Deletes the cache entry after use to reclaim storage and prevent stale entries.
function _getOrComputeHash(bytes memory attestationTbs) internal returns (bytes memory) {
bytes32 key = keccak256(attestationTbs);
bytes memory cached = sha384Cache[key];
if (cached.length != 0) {
delete sha384Cache[key];
return cached;
}
return Sha2Ext.sha384(attestationTbs, 0, attestationTbs.length);
}

function _verifySignature(bytes memory pubKey, bytes memory hash, bytes memory sig) internal view {
require(ECDSA384.verify(ECDSA384Curve.p384(), hash, sig, pubKey), "invalid sig");
}
Expand Down
38 changes: 38 additions & 0 deletions test/CertManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,42 @@ contract CertManagerTest is Test {
hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4";
certManager.verifyCACert(cert, keccak256(parent));
}

// --- Split-flow tests (EIP-7825 compatible) ---

function test_VerifyCACertWithCachedHash() public {
bytes memory parent =
hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6";
bytes memory cert =
hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4";

// TX 1: cache the SHA-384 hash
certManager.cacheCertHash(cert);

// TX 2: verify using cached hash (skips SHA-384, only does ECDSA384)
certManager.verifyCACert(cert, keccak256(parent));
}

function test_CertCacheCleanedUpAfterUse() public {
bytes memory parent =
hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6";
bytes memory cert =
hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4";
bytes32 key = keccak256(cert);

certManager.cacheCertHash(cert);
assertEq(certManager.certSha384Cache(key).length, 48, "cached hash should be 48 bytes");

certManager.verifyCACert(cert, keccak256(parent));
assertEq(certManager.certSha384Cache(key).length, 0, "cache should be cleared after use");
}

function test_CacheCertHashRevertIfAlreadyCached() public {
bytes memory cert =
hex"308202bf30820244a00302010202100b93e39c65609c59e8144a2ad34ba3a0300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3234313132333036333235355a170d3234313231333037333235355a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d353133623665666332313639303264372e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004ee78108039725a03e0b63a5d7d1244f6294eb7631f305e360997c8e5c06c779f23cfaeb64cb9aeac8a031bfac9f4dafc3621b4367f003c08c0ce410c2118396cc5d56ec4e92e1b17f9709b2bffcef462f7bcb97d6ca11325c4a30156c9720de7a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e041604142b3d75d274a3cdd61b2c13f539e08c960ce757dd300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030369003066023100fce7a6c2b38e0a8ebf0d28348d74463458b84bfe8b2b95315dd4da665e8e83d4ab911852a4e92a8263ecf571d2df3b89023100ab92be511136be76aa313018f9f4825eaad602d0342d268e6da632767f68f55f761fa9fd2a7ee716c481c67f26e3f8f4";

certManager.cacheCertHash(cert);
vm.expectRevert("cert hash already cached");
certManager.cacheCertHash(cert);
}
}
Loading
Loading