-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split certificate management to separate contract
- Loading branch information
Showing
5 changed files
with
453 additions
and
405 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.15; | ||
|
||
import {Sha2Ext} from "./Sha2Ext.sol"; | ||
import {Asn1Decode} from "./Asn1Decode.sol"; | ||
import {ECDSA384} from "./ECDSA384.sol"; | ||
import {LibBytes} from "./LibBytes.sol"; | ||
import {NodePtr, LibNodePtr} from "./NodePtr.sol"; | ||
|
||
// adapted from https://github.com/marlinprotocol/NitroProver/blob/f1d368d1f172ad3a55cd2aaaa98ad6a6e7dcde9d/src/CertManager.sol | ||
|
||
contract CertManager { | ||
using Asn1Decode for bytes; | ||
using LibBytes for bytes; | ||
using LibNodePtr for NodePtr; | ||
|
||
// @dev download the root CA cert for AWS nitro enclaves from https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip | ||
// @dev convert the base64 encoded pub key into hex to get the cert below | ||
bytes public constant ROOT_CA_CERT = | ||
hex"3082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff6"; | ||
bytes32 public constant ROOT_CA_CERT_HASH = keccak256(ROOT_CA_CERT); | ||
// OID 1.2.840.10045.4.3.3 represents {iso(1) member-body(2) us(840) ansi-x962(10045) signatures(4) ecdsa-with-SHA2(3) ecdsa-with-SHA384(3)} | ||
// which essentially means the signature algorithm is Elliptic curve Digital Signature Algorithm (DSA) coupled with the Secure Hash Algorithm 384 (SHA384) algorithm | ||
// @dev Sig algo is hardcoded here because the root cerificate's sig algorithm is known beforehand | ||
// @dev reference article for encoding https://learn.microsoft.com/en-in/windows/win32/seccertenroll/about-object-identifier | ||
bytes32 public constant CERT_ALGO_OID = keccak256(hex"06082a8648ce3d040303"); | ||
// https://oid-rep.orange-labs.fr/get/1.2.840.10045.2.1 | ||
// 1.2.840.10045.2.1 {iso(1) member-body(2) us(840) ansi-x962(10045) keyType(2) ecPublicKey(1)} represents Elliptic curve public key cryptography | ||
bytes32 public constant EC_PUB_KEY_OID = keccak256(hex"2a8648ce3d0201"); | ||
// https://oid-rep.orange-labs.fr/get/1.3.132.0.34 | ||
// 1.3.132.0.34 {iso(1) identified-organization(3) certicom(132) curve(0) ansip384r1(34)} represents NIST 384-bit elliptic curve | ||
bytes32 public constant SECP_384_R1_OID = keccak256(hex"2b81040022"); | ||
|
||
// extension OID certificate constants | ||
bytes32 public constant BASIC_CONSTRAINTS_OID = keccak256(hex"551d13"); | ||
bytes32 public constant KEY_USAGE_OID = keccak256(hex"551d0f"); | ||
|
||
// ECDSA384 curve parameters (NIST P-384) | ||
bytes public constant CURVE_A = | ||
hex"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"; | ||
bytes public constant CURVE_B = | ||
hex"b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"; | ||
bytes public constant CURVE_GX = | ||
hex"aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"; | ||
bytes public constant CURVE_GY = | ||
hex"3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"; | ||
bytes public constant CURVE_P = | ||
hex"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"; | ||
bytes public constant CURVE_N = | ||
hex"ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"; | ||
bytes public constant CURVE_LOW_S_MAX = | ||
hex"7fffffffffffffffffffffffffffffffffffffffffffffffe3b1a6c0fa1b96efac0d06d9245853bd76760cb5666294b9"; | ||
|
||
struct CachedCert { | ||
uint256 notAfter; | ||
int256 maxPathLen; | ||
bytes pubKey; | ||
} | ||
|
||
// certHash -> CachedCert | ||
mapping(bytes32 => bytes) public verified; | ||
|
||
constructor() { | ||
CachedCert memory empty; | ||
_verifyCert(ROOT_CA_CERT, ROOT_CA_CERT_HASH, false, empty); | ||
} | ||
|
||
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 verifyCertBundle(bytes memory certificate, bytes[] calldata cabundle) | ||
external | ||
returns (CachedCert memory) | ||
{ | ||
CachedCert memory parentCache; | ||
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); | ||
} | ||
return _verifyCert(certificate, keccak256(certificate), true, parentCache); | ||
} | ||
|
||
function _verifyCert(bytes memory certificate, bytes32 certHash, bool clientCert, CachedCert memory parentCache) | ||
internal | ||
returns (CachedCert memory) | ||
{ | ||
// skip verification if already verified | ||
bytes memory cacheBytes = verified[certHash]; | ||
CachedCert memory cache; | ||
if (cacheBytes.length != 0) { | ||
cache = abi.decode(cacheBytes, (CachedCert)); | ||
require(cache.notAfter >= block.timestamp, "cert expired"); | ||
return cache; | ||
} | ||
|
||
NodePtr root = certificate.root(); | ||
NodePtr tbsCertPtr = certificate.firstChildOf(root); | ||
(uint256 notAfter, int256 maxPathLen, bytes memory pubKey) = _parseTbs(certificate, tbsCertPtr, clientCert); | ||
|
||
if (parentCache.pubKey.length != 0 || certHash != ROOT_CA_CERT_HASH) { | ||
if (parentCache.maxPathLen > 0 && (maxPathLen < 0 || maxPathLen >= parentCache.maxPathLen)) { | ||
maxPathLen = parentCache.maxPathLen - 1; | ||
} | ||
require((parentCache.maxPathLen == 0) == clientCert, "maxPathLen exceeded"); | ||
_verifyCertSignature(certificate, tbsCertPtr, parentCache.pubKey); | ||
} | ||
|
||
cache = CachedCert({notAfter: notAfter, maxPathLen: maxPathLen, pubKey: pubKey}); | ||
verified[certHash] = abi.encode(cache); | ||
return cache; | ||
} | ||
|
||
function _parseTbs(bytes memory certificate, NodePtr ptr, bool clientCert) | ||
internal | ||
view | ||
returns (uint256 notAfter, int256 maxPathLen, bytes memory pubKey) | ||
{ | ||
NodePtr versionPtr = certificate.firstChildOf(ptr); | ||
NodePtr vPtr = certificate.firstChildOf(versionPtr); | ||
NodePtr serialPtr = certificate.nextSiblingOf(versionPtr); | ||
NodePtr sigAlgoPtr = certificate.nextSiblingOf(serialPtr); | ||
|
||
require(certificate.keccak(sigAlgoPtr.content(), sigAlgoPtr.length()) == CERT_ALGO_OID, "invalid cert sig algo"); | ||
uint256 version = certificate.uintAt(vPtr); | ||
// 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, pubKey) = _parseTbsInner(certificate, sigAlgoPtr, clientCert); | ||
} | ||
|
||
function _parseTbsInner(bytes memory certificate, NodePtr sigAlgoPtr, bool clientCert) | ||
internal | ||
view | ||
returns (uint256 notAfter, int256 maxPathLen, bytes memory pubKey) | ||
{ | ||
NodePtr issuerPtr = certificate.nextSiblingOf(sigAlgoPtr); | ||
NodePtr validityPtr = certificate.nextSiblingOf(issuerPtr); | ||
NodePtr subjectPtr = certificate.nextSiblingOf(validityPtr); | ||
NodePtr subjectPublicKeyInfoPtr = certificate.nextSiblingOf(subjectPtr); | ||
NodePtr extensionsPtr = certificate.nextSiblingOf(subjectPublicKeyInfoPtr); | ||
|
||
notAfter = _verifyValidity(certificate, validityPtr); | ||
maxPathLen = _verifyExtensions(certificate, extensionsPtr, clientCert); | ||
pubKey = _parsePubKey(certificate, subjectPublicKeyInfoPtr); | ||
} | ||
|
||
function _parsePubKey(bytes memory certificate, NodePtr subjectPublicKeyInfoPtr) | ||
internal | ||
pure | ||
returns (bytes memory subjectPubKey) | ||
{ | ||
NodePtr pubKeyAlgoPtr = certificate.firstChildOf(subjectPublicKeyInfoPtr); | ||
NodePtr pubKeyAlgoIdPtr = certificate.firstChildOf(pubKeyAlgoPtr); | ||
NodePtr algoParamsPtr = certificate.nextSiblingOf(pubKeyAlgoIdPtr); | ||
NodePtr subjectPublicKeyPtr = certificate.nextSiblingOf(pubKeyAlgoPtr); | ||
NodePtr subjectPubKeyPtr = certificate.bitstring(subjectPublicKeyPtr); | ||
|
||
require( | ||
certificate.keccak(pubKeyAlgoIdPtr.content(), pubKeyAlgoIdPtr.length()) == EC_PUB_KEY_OID, | ||
"invalid cert algo id" | ||
); | ||
require( | ||
certificate.keccak(algoParamsPtr.content(), algoParamsPtr.length()) == SECP_384_R1_OID, | ||
"invalid cert algo param" | ||
); | ||
|
||
uint256 end = subjectPubKeyPtr.content() + subjectPubKeyPtr.length(); | ||
subjectPubKey = certificate.slice(end - 96, end); | ||
} | ||
|
||
function _verifyValidity(bytes memory certificate, NodePtr validityPtr) internal view returns (uint256 notAfter) { | ||
NodePtr notBeforePtr = certificate.firstChildOf(validityPtr); | ||
NodePtr notAfterPtr = certificate.nextSiblingOf(notBeforePtr); | ||
|
||
uint256 notBefore = certificate.timestampAt(notBeforePtr); | ||
notAfter = certificate.timestampAt(notAfterPtr); | ||
|
||
require(notBefore <= block.timestamp, "certificate not valid yet"); | ||
require(notAfter >= block.timestamp, "certificate not valid anymore"); | ||
} | ||
|
||
function _verifyExtensions(bytes memory certificate, NodePtr extensionsPtr, bool clientCert) | ||
internal | ||
pure | ||
returns (int256 maxPathLen) | ||
{ | ||
require(certificate[extensionsPtr.header()] == 0xa3, "invalid extensions"); | ||
extensionsPtr = certificate.firstChildOf(extensionsPtr); | ||
NodePtr extensionPtr = certificate.firstChildOf(extensionsPtr); | ||
uint256 end = extensionsPtr.content() + extensionsPtr.length(); | ||
bool basicConstraintsFound = false; | ||
bool keyUsageFound = false; | ||
maxPathLen = -1; | ||
|
||
while (true) { | ||
NodePtr oidPtr = certificate.firstChildOf(extensionPtr); | ||
bytes32 oid = certificate.keccak(oidPtr.content(), oidPtr.length()); | ||
|
||
if (oid == BASIC_CONSTRAINTS_OID || oid == KEY_USAGE_OID) { | ||
NodePtr valuePtr = certificate.nextSiblingOf(oidPtr); | ||
|
||
if (certificate[valuePtr.header()] == 0x01) { | ||
// skip optional critical bool | ||
require(valuePtr.length() == 1, "invalid critical bool value"); | ||
valuePtr = certificate.nextSiblingOf(valuePtr); | ||
} | ||
|
||
valuePtr = certificate.octetString(valuePtr); | ||
|
||
if (oid == BASIC_CONSTRAINTS_OID) { | ||
basicConstraintsFound = true; | ||
maxPathLen = _verifyBasicConstraintsExtension(certificate, valuePtr); | ||
} else { | ||
keyUsageFound = true; | ||
_verifyKeyUsageExtension(certificate, valuePtr, clientCert); | ||
} | ||
} | ||
|
||
if (extensionPtr.content() + extensionPtr.length() == end) { | ||
break; | ||
} | ||
extensionPtr = certificate.nextSiblingOf(extensionPtr); | ||
} | ||
|
||
require(basicConstraintsFound, "basicConstraints not found"); | ||
require(keyUsageFound, "keyUsage not found"); | ||
require(!clientCert || maxPathLen == -1, "maxPathLen must be undefined for client cert"); | ||
} | ||
|
||
function _verifyBasicConstraintsExtension(bytes memory certificate, NodePtr valuePtr) | ||
internal | ||
pure | ||
returns (int256 maxPathLen) | ||
{ | ||
maxPathLen = -1; | ||
NodePtr basicConstraintsPtr = certificate.firstChildOf(valuePtr); | ||
if (certificate[basicConstraintsPtr.header()] == 0x01) { | ||
// skip optional isCA bool | ||
require(basicConstraintsPtr.length() == 1, "invalid isCA bool value"); | ||
basicConstraintsPtr = certificate.nextSiblingOf(basicConstraintsPtr); | ||
} | ||
if (certificate[basicConstraintsPtr.header()] == 0x02) { | ||
maxPathLen = int256(certificate.uintAt(basicConstraintsPtr)); | ||
} | ||
} | ||
|
||
function _verifyKeyUsageExtension(bytes memory certificate, NodePtr valuePtr, bool clientCert) 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 { | ||
require(value & 0x04 == 0x04, "CertSign must be present"); | ||
} | ||
} | ||
|
||
function _verifyCertSignature(bytes memory certificate, NodePtr ptr, bytes memory pubKey) internal view { | ||
NodePtr 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()); | ||
|
||
NodePtr sigPtr = certificate.nextSiblingOf(sigAlgoPtr); | ||
NodePtr sigBPtr = certificate.bitstring(sigPtr); | ||
NodePtr sigRoot = certificate.rootOf(sigBPtr); | ||
NodePtr sigRPtr = certificate.firstChildOf(sigRoot); | ||
NodePtr sigSPtr = certificate.nextSiblingOf(sigRPtr); | ||
(uint128 rhi, uint256 rlo) = certificate.uint384At(sigRPtr); | ||
(uint128 shi, uint256 slo) = certificate.uint384At(sigSPtr); | ||
bytes memory sigPacked = abi.encodePacked(rhi, rlo, shi, slo); | ||
|
||
_verifySignature(pubKey, hash, sigPacked); | ||
} | ||
|
||
function _verifySignature(bytes memory pubKey, bytes memory hash, bytes memory sig) internal view { | ||
ECDSA384.Parameters memory CURVE_PARAMETERS = ECDSA384.Parameters({ | ||
a: CURVE_A, | ||
b: CURVE_B, | ||
gx: CURVE_GX, | ||
gy: CURVE_GY, | ||
p: CURVE_P, | ||
n: CURVE_N, | ||
lowSmax: CURVE_LOW_S_MAX | ||
}); | ||
require(ECDSA384.verify(CURVE_PARAMETERS, hash, sig, pubKey), "invalid sig"); | ||
} | ||
} |
Oops, something went wrong.