diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 945a786..808191e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: [pull_request] env: - SOLC_VERSION: 0.8.20 + SOLC_VERSION: 0.8.28 jobs: contract-test: diff --git a/.gitmodules b/.gitmodules index 7d79667..2ca6cfa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-foundry-upgrades"] path = lib/openzeppelin-foundry-upgrades url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "lib/risc0-ethereum"] + path = lib/risc0-ethereum + url = https://github.com/risc0/risc0-ethereum diff --git a/Makefile b/Makefile index 8694cc1..f975673 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SOLC_VERSION=0.8.20 +SOLC_VERSION=0.8.28 FORGE=forge SLITHER=slither TEST_UPGRADEABLE=false diff --git a/contracts/AVRValidator.sol b/contracts/AVRValidator.sol index b16857b..c8c49f8 100644 --- a/contracts/AVRValidator.sol +++ b/contracts/AVRValidator.sol @@ -7,6 +7,7 @@ import {Base64} from "base64/base64.sol"; import {Asn1Decode, NodePtr} from "./Asn1Decode.sol"; import {LCPUtils} from "./LCPUtils.sol"; import {ILCPClientErrors} from "./ILCPClientErrors.sol"; +import {RemoteAttestation} from "./RemoteAttestation.sol"; /** * @dev AVRValidator provides the validation functions of Intel's Attestation Verification Report(AVR) @@ -21,10 +22,6 @@ library AVRValidator { 0x2a864886f70d01010b0000000000000000000000000000000000000000000000; // OID_RSA_ENCRYPTION is the OID of rsaEncryption(1.2.840.113549.1.1.1) bytes32 internal constant OID_RSA_ENCRYPTION = 0x2a864886f70d0101010000000000000000000000000000000000000000000000; - // FLAG_DISALLOWED indicates that the advisory or quote status is not allowed. - uint256 internal constant FLAG_DISALLOWED = 0; - // FLAG_ALLOWED indicates that the advisory or quote status is allowed. - uint256 internal constant FLAG_ALLOWED = 1; // '"' bytes32 internal constant CHAR_DOUBLE_QUOTE = bytes32(hex"22"); // ',' @@ -50,13 +47,6 @@ library AVRValidator { uint256 notAfter; // seconds since epoch } - struct ReportAllowedStatus { - // quote status => flag(0: not allowed, 1: allowed) - mapping(string => uint256) allowedQuoteStatuses; - // advisory id => flag(0: not allowed, 1: allowed) - mapping(string => uint256) allowedAdvisories; - } - // ------------------ Public functions ------------------ struct ReportExtractedElements { @@ -70,7 +60,7 @@ library AVRValidator { bool developmentMode, AVRValidator.RSAParams storage verifiedRootCAParams, mapping(bytes32 => AVRValidator.RSAParams) storage verifiedSigningRSAParams, - ReportAllowedStatus storage allowedStatuses, + RemoteAttestation.ReportAllowedStatus storage allowedStatuses, bytes calldata report, bytes calldata signingCert, bytes calldata signature @@ -174,7 +164,7 @@ library AVRValidator { function validateAndExtractElements( bool developmentMode, bytes calldata report, - ReportAllowedStatus storage allowedStatus + RemoteAttestation.ReportAllowedStatus storage allowedStatus ) public view returns (ReportExtractedElements memory) { // find 'timestamp' key (uint256 i, bytes memory timestamp) = consumeTimestampReportJSON(report, 0); @@ -190,7 +180,8 @@ library AVRValidator { // skip the validation for quote status and advisories if status is "OK" if (!(status.length == 2 && status[0] == 0x4f && status[1] == 0x4b)) { require( - allowedStatus.allowedQuoteStatuses[string(status)] == FLAG_ALLOWED, "the quote status is not allowed" + allowedStatus.allowedQuoteStatuses[string(status)] == RemoteAttestation.FLAG_ALLOWED, + "the quote status is not allowed" ); bytes32 h = keccak256(status); if ( @@ -263,13 +254,13 @@ library AVRValidator { } } else if (chr == CHAR_COMMA) { require( - allowedAdvisories[string(report[lastStart:offset - 1])] == FLAG_ALLOWED, + allowedAdvisories[string(report[lastStart:offset - 1])] == RemoteAttestation.FLAG_ALLOWED, "disallowed advisory is included" ); } else if (chr == CHAR_LIST_END) { if (offset - lastStart > 0) { require( - allowedAdvisories[string(report[lastStart:offset - 1])] == FLAG_ALLOWED, + allowedAdvisories[string(report[lastStart:offset - 1])] == RemoteAttestation.FLAG_ALLOWED, "disallowed advisory is included" ); } diff --git a/contracts/DCAPValidator.sol b/contracts/DCAPValidator.sol new file mode 100644 index 0000000..30d8a58 --- /dev/null +++ b/contracts/DCAPValidator.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +library DCAPValidator { + /// @notice The offset of the SGX quote body in the output bytes + uint256 internal constant SGX_QUOTE_BODY_OFFSET = 67; + /// @notice The offset of the attributes of the SGX quote body in the output bytes + uint256 internal constant ATTRIBUTES_OFFSET = SGX_QUOTE_BODY_OFFSET + 16 + 4 + 28; + /// @notice The offset of the MRENCLAVE of the SGX quote body in the output bytes + uint256 internal constant MRENCLAVE_OFFSET = ATTRIBUTES_OFFSET + 16; + /// @notice The end offset of the MRENCLAVE of the SGX quote body in the output bytes + uint256 internal constant MRENCLAVE_END_OFFSET = MRENCLAVE_OFFSET + 32; + /// @notice The offset of the report data of the SGX quote body in the output bytes + uint256 internal constant REPORT_DATA_OFFSET = SGX_QUOTE_BODY_OFFSET + 320; + /// @notice The offset of the enclave key in the report data of the SGX quote body in the output bytes + uint256 internal constant REPORT_DATA_ENCLAVE_KEY_OFFSET = REPORT_DATA_OFFSET + 1; + /// @notice The offset of the operator in the report data of the SGX quote body in the output bytes + uint256 internal constant REPORT_DATA_OPERATOR_OFFSET = REPORT_DATA_ENCLAVE_KEY_OFFSET + 20; + /// @notice The end offset of the operator in the report data of the SGX quote body in the output bytes + uint256 internal constant REPORT_DATA_OPERATOR_END_OFFSET = REPORT_DATA_OPERATOR_OFFSET + 20; + /// @notice The offset of the advisory IDs in the output bytes + uint256 internal constant ADVISORY_IDS_OFFSET = REPORT_DATA_OFFSET + 64; + + /// @notice The TCB status + uint8 internal constant TCB_STATUS_UP_TO_DATE = 0; + uint8 internal constant TCB_STATUS_OUT_OF_DATE = 1; + uint8 internal constant TCB_STATUS_REVOKED = 2; + uint8 internal constant TCB_STATUS_CONFIGURATION_NEEDED = 3; + uint8 internal constant TCB_STATUS_OUT_OF_DATE_CONFIGURATION_NEEDED = 4; + uint8 internal constant TCB_STATUS_SW_HARDENING_NEEDED = 5; + uint8 internal constant TCB_STATUS_CONFIGURATION_AND_SW_HARDENING_NEEDED = 6; + + /// @notice The string representation of the TCB status + string internal constant TCB_STATUS_UP_TO_DATE_STRING = "UpToDate"; + string internal constant TCB_STATUS_OUT_OF_DATE_STRING = "OutOfDate"; + string internal constant TCB_STATUS_REVOKED_STRING = "Revoked"; + string internal constant TCB_STATUS_CONFIGURATION_NEEDED_STRING = "ConfigurationNeeded"; + string internal constant TCB_STATUS_OUT_OF_DATE_CONFIGURATION_NEEDED_STRING = "OutOfDateConfigurationNeeded"; + string internal constant TCB_STATUS_SW_HARDENING_NEEDED_STRING = "SWHardeningNeeded"; + string internal constant TCB_STATUS_CONFIGURATION_AND_SW_HARDENING_NEEDED_STRING = + "ConfigurationAndSWHardeningNeeded"; + + /// @notice The keccak256 hash of the string representation of the TCB status + bytes32 internal constant TCB_STATUS_UP_TO_DATE_KECCAK256_HASH = keccak256(bytes(TCB_STATUS_UP_TO_DATE_STRING)); + + /** + * @notice The output of the quote verification + * @dev This struct corresponds to `QuoteVerificationOutput` in the dcap-quote-verifier library. + * Note that some fields from the original output are omitted or parsed in greater detail in Solidity for our use case. + * ref. https://github.com/datachainlab/zkdcap/blob/9616d7976a84e97a128fa02175ec994b95e3c137/crates/quote-verifier/src/verifier.rs#L19 + */ + struct Output { + string tcbStatus; + uint32 minTcbEvaluationDataNumber; + bytes32 sgxIntelRootCAHash; + uint64 validityNotBefore; + uint64 validityNotAfter; + bool enclaveDebugEnabled; + bytes32 mrenclave; + address enclaveKey; + address operator; + string[] advisoryIDs; + } + + /** + * @notice Parse the output bytes from the quote verification + * @param outputBytes The output bytes from the quote verification + * @return output The parsed output + */ + function parseOutput(bytes calldata outputBytes) public pure returns (Output memory) { + require(bytes2(outputBytes[0:2]) == hex"0000", "unexpected version"); + require(uint16(bytes2(outputBytes[2:4])) == 3, "unexpected quote version"); + require(uint32(bytes4(outputBytes[4:8])) == 0, "unexpected tee type"); + + Output memory output; + output.tcbStatus = tcbStatusToString(uint8(outputBytes[8])); + output.minTcbEvaluationDataNumber = uint32(bytes4(outputBytes[9:13])); + output.sgxIntelRootCAHash = bytes32(outputBytes[19:51]); + output.validityNotBefore = uint64(bytes8(outputBytes[51:59])); + output.validityNotAfter = uint64(bytes8(outputBytes[59:67])); + output.enclaveDebugEnabled = uint8(outputBytes[ATTRIBUTES_OFFSET]) & uint8(2) != 0; + output.mrenclave = bytes32(outputBytes[MRENCLAVE_OFFSET:MRENCLAVE_END_OFFSET]); + // The initial byte of the report data is the version of the report data + require(outputBytes[REPORT_DATA_OFFSET] == 0x01, "unexpected report data version"); + output.enclaveKey = address(bytes20(outputBytes[REPORT_DATA_ENCLAVE_KEY_OFFSET:REPORT_DATA_OPERATOR_OFFSET])); + output.operator = address(bytes20(outputBytes[REPORT_DATA_OPERATOR_OFFSET:REPORT_DATA_OPERATOR_END_OFFSET])); + output.advisoryIDs = abi.decode(outputBytes[ADVISORY_IDS_OFFSET:outputBytes.length], (string[])); + return output; + } + + /** + * @notice Convert the TCB status to a string + * @param tcbStatus The TCB status + * @return The string representation of the TCB status + */ + function tcbStatusToString(uint8 tcbStatus) internal pure returns (string memory) { + // The if-else chain is ordered based on the expected frequency of allowed TCB statuses + // (most common statuses first), rather than the order of the enum definition. + // This ordering may result in minor gas savings by reducing the average number of comparisons in common cases. + if (tcbStatus == TCB_STATUS_UP_TO_DATE) { + return TCB_STATUS_UP_TO_DATE_STRING; + } else if (tcbStatus == TCB_STATUS_SW_HARDENING_NEEDED) { + return TCB_STATUS_SW_HARDENING_NEEDED_STRING; + } else if (tcbStatus == TCB_STATUS_CONFIGURATION_NEEDED) { + return TCB_STATUS_CONFIGURATION_NEEDED_STRING; + } else if (tcbStatus == TCB_STATUS_CONFIGURATION_AND_SW_HARDENING_NEEDED) { + return TCB_STATUS_CONFIGURATION_AND_SW_HARDENING_NEEDED_STRING; + } else if (tcbStatus == TCB_STATUS_OUT_OF_DATE_CONFIGURATION_NEEDED) { + return TCB_STATUS_OUT_OF_DATE_CONFIGURATION_NEEDED_STRING; + } else if (tcbStatus == TCB_STATUS_OUT_OF_DATE) { + return TCB_STATUS_OUT_OF_DATE_STRING; + } else if (tcbStatus == TCB_STATUS_REVOKED) { + return TCB_STATUS_REVOKED_STRING; + } else { + revert("unexpected TCB status"); + } + } +} diff --git a/contracts/ILCPClientErrors.sol b/contracts/ILCPClientErrors.sol index be2b1dd..3bb1a68 100644 --- a/contracts/ILCPClientErrors.sol +++ b/contracts/ILCPClientErrors.sol @@ -58,4 +58,22 @@ interface ILCPClientErrors { error LCPClientUpdateOperatorsPermissionless(); error LCPClientUpdateOperatorsSignatureUnexpectedOperator(address actual, address expected); + + error LCPClientZKDCAPInvalidConstructorParams(); + error LCPClientZKDCAPOutputNotValid(); + error LCPClientZKDCAPUnrecognizedTCBStatus(); + error LCPClientZKDCAPCurrentTcbEvaluationDataNumberNotSet(); + error LCPClientZKDCAPInvalidNextTcbEvaluationDataNumberInfo(); + error LCPClientZKDCAPInvalidVerifierInfos(); + error LCPClientZKDCAPInvalidVerifierInfoLength(); + error LCPClientZKDCAPInvalidVerifierInfoZKVMType(); + error LCPClientZKDCAPUnsupportedZKVMType(); + error LCPClientZKDCAPRisc0ImageIdNotSet(); + error LCPClientZKDCAPUnexpectedIntelRootCAHash(); + error LCPClientZKDCAPOutputReportUnexpectedOperator(address actual, address expected); + + error LCPClientZKDCAPDisallowedTCBStatus(); + error LCPClientZKDCAPDisallowedAdvisoryID(); + error LCPClientZKDCAPUnexpectedEnclaveDebugMode(); + error LCPClientZKDCAPUnexpectedTcbEvaluationDataNumber(uint64 currentTcbEvaluationDataNumber); } diff --git a/contracts/LCPClientBase.sol b/contracts/LCPClientBase.sol index a104c3e..6eccd40 100644 --- a/contracts/LCPClientBase.sol +++ b/contracts/LCPClientBase.sol @@ -8,14 +8,13 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { IbcLightclientsLcpV1ClientState as ProtoClientState, IbcLightclientsLcpV1ConsensusState as ProtoConsensusState, - IbcLightclientsLcpV1RegisterEnclaveKeyMessage as RegisterEnclaveKeyMessage, IbcLightclientsLcpV1UpdateClientMessage as UpdateClientMessage, IbcLightclientsLcpV1UpdateOperatorsMessage as UpdateOperatorsMessage } from "./proto/ibc/lightclients/lcp/v1/LCP.sol"; import {LCPCommitment} from "./LCPCommitment.sol"; import {LCPOperator} from "./LCPOperator.sol"; import {LCPProtoMarshaler} from "./LCPProtoMarshaler.sol"; -import {AVRValidator} from "./AVRValidator.sol"; +import {RemoteAttestation} from "./RemoteAttestation.sol"; import {ILCPClientErrors} from "./ILCPClientErrors.sol"; abstract contract LCPClientBase is ILightClient, ILCPClientErrors { @@ -35,47 +34,33 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { struct ClientStorage { ProtoClientState.Data clientState; - AVRValidator.ReportAllowedStatus allowedStatuses; + uint256[50] __gap0; + RemoteAttestation.ReportAllowedStatus allowedStatuses; + uint256[50] __gap1; // height => consensus state mapping(uint128 => ConsensusState) consensusStates; // enclave key => EKInfo mapping(address => EKInfo) ekInfos; + bytes32 zkDCAPRisc0ImageId; } - // --------------------- Events --------------------- - - event RegisteredEnclaveKey(string clientId, address enclaveKey, uint256 expiredAt, address operator); - // --------------------- Immutable fields --------------------- /// @dev ibcHandler is the address of the IBC handler contract. /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address internal immutable ibcHandler; - /// @dev if developmentMode is true, the client allows the remote attestation of IAS in development. - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - bool internal immutable developmentMode; // --------------------- Storage fields --------------------- /// @dev clientId => client storage mapping(string => ClientStorage) internal clientStorages; - /// @dev RootCA's public key parameters - AVRValidator.RSAParams internal verifiedRootCAParams; - /// @dev keccak256(signingCert) => RSAParams of signing public key - mapping(bytes32 => AVRValidator.RSAParams) internal verifiedSigningRSAParams; - - /// @dev Reserved storage space to allow for layout changes in the future - uint256[50] private __gap; - // --------------------- Constructor --------------------- /// @custom:oz-upgrades-unsafe-allow constructor /// @param ibcHandler_ the address of the IBC handler contract - /// @param developmentMode_ if true, the client allows the enclave debug mode - constructor(address ibcHandler_, bool developmentMode_) { + constructor(address ibcHandler_) { ibcHandler = ibcHandler_; - developmentMode = developmentMode_; } // --------------------- Modifiers --------------------- @@ -85,33 +70,13 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { _; } - // --------------------- Public methods --------------------- - - /// @dev isDevelopmentMode returns true if the client allows the enclave debug mode. - function isDevelopmentMode() public view returns (bool) { - return developmentMode; - } - - /// @dev initializeRootCACert initializes the root CA's public key parameters. - /// All contracts that inherit LCPClientBase should call this in the constructor or initializer. - function initializeRootCACert(bytes memory rootCACert) internal { - if (verifiedRootCAParams.notAfter != 0) { - revert LCPClientRootCACertAlreadyInitialized(); - } - verifiedRootCAParams = AVRValidator.verifyRootCACert(rootCACert); - } + // --------------------- Internal methods --------------------- - /** - * @dev initializeClient initializes a new client with the given state. - * If succeeded, it returns heights at which the consensus state are stored. - * This function is guaranteed by the IBC contract to be called only once for each `clientId`. - * @param clientId the client identifier which is unique within the IBC handler - */ - function initializeClient( - string calldata clientId, + function _initializeClient( + ClientStorage storage clientStorage, bytes calldata protoClientState, bytes calldata protoConsensusState - ) public onlyIBC returns (Height.Data memory height) { + ) internal returns (ProtoClientState.Data memory, ProtoConsensusState.Data memory) { ProtoClientState.Data memory clientState = LCPProtoMarshaler.unmarshalClientState(protoClientState); ProtoConsensusState.Data memory consensusState = LCPProtoMarshaler.unmarshalConsensusState(protoConsensusState); @@ -122,9 +87,6 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { if (clientState.frozen) { revert LCPClientClientStateFrozen(); } - if (clientState.key_expiration == 0) { - revert LCPClientClientStateInvalidKeyExpiration(); - } if (clientState.mrenclave.length != 32) { revert LCPClientClientStateInvalidMrenclaveLength(); } @@ -164,7 +126,6 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { } prev = addr; } - ClientStorage storage clientStorage = clientStorages[clientId]; clientStorage.clientState = clientState; // set allowed quote status and advisories @@ -173,23 +134,29 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { if (bytes(allowedQuoteStatus).length == 0) { revert LCPClientClientStateInvalidAllowedQuoteStatus(); } - clientStorage.allowedStatuses.allowedQuoteStatuses[allowedQuoteStatus] = AVRValidator.FLAG_ALLOWED; + clientStorage.allowedStatuses.allowedQuoteStatuses[allowedQuoteStatus] = RemoteAttestation.FLAG_ALLOWED; } for (uint256 i = 0; i < clientState.allowed_advisory_ids.length; i++) { string memory allowedAdvisoryId = clientState.allowed_advisory_ids[i]; if (bytes(allowedAdvisoryId).length == 0) { revert LCPClientClientStateInvalidAllowedAdvisoryId(); } - clientStorage.allowedStatuses.allowedAdvisories[allowedAdvisoryId] = AVRValidator.FLAG_ALLOWED; + clientStorage.allowedStatuses.allowedAdvisories[allowedAdvisoryId] = RemoteAttestation.FLAG_ALLOWED; } - - return clientState.latest_height; + return (clientState, consensusState); } + // --------------------- Public methods --------------------- + /** * @dev getTimestampAtHeight returns the timestamp of the consensus state at the given height. */ - function getTimestampAtHeight(string calldata clientId, Height.Data calldata height) public view returns (uint64) { + function getTimestampAtHeight(string calldata clientId, Height.Data calldata height) + public + view + override + returns (uint64) + { ConsensusState storage consensusState = clientStorages[clientId].consensusStates[height.toUint128()]; if (consensusState.timestamp == 0) { revert LCPClientConsensusStateNotFound(); @@ -200,18 +167,18 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { /** * @dev getLatestHeight returns the latest height of the client state corresponding to `clientId`. */ - function getLatestHeight(string calldata clientId) public view returns (Height.Data memory) { + function getLatestHeight(string calldata clientId) public view override returns (Height.Data memory) { ProtoClientState.Data storage clientState = clientStorages[clientId].clientState; if (clientState.latest_height.revision_height == 0) { revert LCPClientClientStateNotFound(); } return clientState.latest_height; } + /** * @dev getStatus returns the status of the client corresponding to `clientId`. */ - - function getStatus(string calldata clientId) public view returns (ClientStatus) { + function getStatus(string calldata clientId) public view override returns (ClientStatus) { return clientStorages[clientId].clientState.frozen ? ClientStatus.Frozen : ClientStatus.Active; } @@ -221,6 +188,7 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { function getLatestInfo(string calldata clientId) public view + override returns (Height.Data memory latestHeight, uint64 latestTimestamp, ClientStatus status) { ClientStorage storage clientStorage = clientStorages[clientId]; @@ -230,24 +198,23 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { } /** - * @dev routeUpdateClient returns the calldata to the receiving function of the client message. - * Light client contract may encode a client message as other encoding scheme(e.g. ethereum ABI) - * Check ADR-001 for details. + * @dev updateClient updates the client state and the consensus state of the client corresponding to `clientId`. */ - function routeUpdateClient(string calldata clientId, bytes calldata protoClientMessage) + function updateClient(string calldata clientId, UpdateClientMessage.Data calldata message) public - pure - returns (bytes4, bytes memory) + returns (Height.Data[] memory heights) { - (bytes32 typeUrlHash, bytes memory args) = LCPProtoMarshaler.routeClientMessage(clientId, protoClientMessage); - if (typeUrlHash == LCPProtoMarshaler.UPDATE_CLIENT_MESSAGE_TYPE_URL_HASH) { - return (this.updateClient.selector, args); - } else if (typeUrlHash == LCPProtoMarshaler.REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL_HASH) { - return (this.registerEnclaveKey.selector, args); - } else if (typeUrlHash == LCPProtoMarshaler.UPDATE_OPERATORS_MESSAGE_TYPE_URL_HASH) { - return (this.updateOperators.selector, args); + ClientStorage storage clientStorage = clientStorages[clientId]; + verifySignatures(clientStorage, keccak256(message.proxy_message), message.signatures); + + LCPCommitment.HeaderedProxyMessage memory hm = + abi.decode(message.proxy_message, (LCPCommitment.HeaderedProxyMessage)); + if (hm.header == LCPCommitment.LCP_MESSAGE_HEADER_UPDATE_STATE) { + return updateState(clientStorage, abi.decode(hm.message, (LCPCommitment.UpdateStateProxyMessage))); + } else if (hm.header == LCPCommitment.LCP_MESSAGE_HEADER_MISBEHAVIOUR) { + return submitMisbehaviour(clientStorage, abi.decode(hm.message, (LCPCommitment.MisbehaviourProxyMessage))); } else { - revert LCPClientUnknownProtoTypeUrl(); + revert LCPClientUnknownProxyMessageHeader(); } } @@ -304,41 +271,6 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { return true; } - function validateProxyMessage( - ClientStorage storage clientStorage, - LCPCommitment.VerifyMembershipProxyMessage memory message, - Height.Data calldata height, - bytes memory prefix, - bytes memory path - ) internal view { - uint128 messageHeight = message.height.toUint128(); - uint128 heightValue = height.toUint128(); - ConsensusState storage consensusState = clientStorage.consensusStates[messageHeight]; - if (consensusState.stateId == bytes32(0)) { - revert LCPClientConsensusStateNotFound(); - } - if (heightValue != messageHeight) { - revert LCPClientMembershipVerificationInvalidHeight(); - } - if (keccak256(prefix) != keccak256(message.prefix)) { - revert LCPClientMembershipVerificationInvalidPrefix(); - } - if (keccak256(path) != keccak256(message.path)) { - revert LCPClientMembershipVerificationInvalidPath(); - } - if (consensusState.stateId != message.stateId) { - revert LCPClientMembershipVerificationInvalidStateId(); - } - } - - function verifyCommitmentProofs( - ClientStorage storage clientStorage, - LCPCommitment.CommitmentProofs memory commitmentProofs - ) internal view { - bytes32 commitment = keccak256(commitmentProofs.message); - verifySignatures(clientStorage, commitment, commitmentProofs.signatures); - } - /** * @dev getClientState returns the clientState corresponding to `clientId`. * If it's not found, the function returns false. @@ -367,56 +299,7 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { return (LCPProtoMarshaler.marshalConsensusState(consensusState.stateId, consensusState.timestamp), true); } - function verifySignatures(ClientStorage storage clientStorage, bytes32 commitment, bytes[] memory signatures) - internal - view - { - uint256 sigNum = signatures.length; - uint256 opNum = clientStorage.clientState.operators.length; - if (opNum == 0) { - if (sigNum != 1) { - revert LCPClientInvalidSignaturesLength(); - } - ensureActiveKey(clientStorage, verifyECDSASignature(commitment, signatures[0])); - } else { - if (sigNum != opNum) { - revert LCPClientInvalidSignaturesLength(); - } - uint256 success = 0; - for (uint256 i = 0; i < sigNum; i++) { - bytes memory sig = signatures[i]; - if (sig.length != 0) { - ensureActiveKey( - clientStorage, - verifyECDSASignature(commitment, sig), - address(bytes20(clientStorage.clientState.operators[i])) - ); - unchecked { - success++; - } - } - } - ensureSufficientValidSignatures(clientStorage.clientState, success); - } - } - - function updateClient(string calldata clientId, UpdateClientMessage.Data calldata message) - public - returns (Height.Data[] memory heights) - { - ClientStorage storage clientStorage = clientStorages[clientId]; - verifySignatures(clientStorage, keccak256(message.proxy_message), message.signatures); - - LCPCommitment.HeaderedProxyMessage memory hm = - abi.decode(message.proxy_message, (LCPCommitment.HeaderedProxyMessage)); - if (hm.header == LCPCommitment.LCP_MESSAGE_HEADER_UPDATE_STATE) { - return updateState(clientStorage, abi.decode(hm.message, (LCPCommitment.UpdateStateProxyMessage))); - } else if (hm.header == LCPCommitment.LCP_MESSAGE_HEADER_MISBEHAVIOUR) { - return submitMisbehaviour(clientStorage, abi.decode(hm.message, (LCPCommitment.MisbehaviourProxyMessage))); - } else { - revert LCPClientUnknownProxyMessageHeader(); - } - } + // --------------------- Internal methods --------------------- function updateState(ClientStorage storage clientStorage, LCPCommitment.UpdateStateProxyMessage memory pmsg) internal @@ -495,59 +378,6 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { return heights; } - function registerEnclaveKey(string calldata clientId, RegisterEnclaveKeyMessage.Data calldata message) - public - returns (Height.Data[] memory heights) - { - ClientStorage storage clientStorage = clientStorages[clientId]; - AVRValidator.ReportExtractedElements memory reElems = AVRValidator.verifyReport( - developmentMode, - verifiedRootCAParams, - verifiedSigningRSAParams, - clientStorage.allowedStatuses, - message.report, - message.signing_cert, - message.signature - ); - - if (bytes32(clientStorage.clientState.mrenclave) != reElems.mrenclave) { - revert LCPClientClientStateUnexpectedMrenclave(); - } - - // if `operator_signature` is empty, the operator address is zero - address operator; - if (message.operator_signature.length != 0) { - operator = verifyECDSASignature( - keccak256(LCPOperator.computeEIP712RegisterEnclaveKey(message.report)), message.operator_signature - ); - } - if (reElems.operator != address(0) && reElems.operator != operator) { - revert LCPClientAVRUnexpectedOperator(operator, reElems.operator); - } - uint64 expiredAt = reElems.attestationTime + clientStorage.clientState.key_expiration; - if (expiredAt <= block.timestamp) { - revert LCPClientAVRAlreadyExpired(); - } - EKInfo storage ekInfo = clientStorage.ekInfos[reElems.enclaveKey]; - if (ekInfo.expiredAt != 0) { - if (ekInfo.operator != operator) { - revert LCPClientEnclaveKeyUnexpectedOperator(ekInfo.operator, operator); - } - if (ekInfo.expiredAt != expiredAt) { - revert LCPClientEnclaveKeyUnexpectedExpiredAt(); - } - // NOTE: if the key already exists, don't update any state - return heights; - } - ekInfo.expiredAt = expiredAt; - ekInfo.operator = operator; - - emit RegisteredEnclaveKey(clientId, reElems.enclaveKey, expiredAt, operator); - - // Note: client and consensus state are not always updated in registerEnclaveKey - return heights; - } - function updateOperators(string calldata clientId, UpdateOperatorsMessage.Data calldata message) public returns (Height.Data[] memory heights) @@ -662,4 +492,72 @@ abstract contract LCPClientBase is ILightClient, ILCPClientErrors { } return ECDSA.recover(commitment, signature); } + + function validateProxyMessage( + ClientStorage storage clientStorage, + LCPCommitment.VerifyMembershipProxyMessage memory message, + Height.Data calldata height, + bytes memory prefix, + bytes memory path + ) internal view { + uint128 messageHeight = message.height.toUint128(); + uint128 heightValue = height.toUint128(); + ConsensusState storage consensusState = clientStorage.consensusStates[messageHeight]; + if (consensusState.stateId == bytes32(0)) { + revert LCPClientConsensusStateNotFound(); + } + if (heightValue != messageHeight) { + revert LCPClientMembershipVerificationInvalidHeight(); + } + if (keccak256(prefix) != keccak256(message.prefix)) { + revert LCPClientMembershipVerificationInvalidPrefix(); + } + if (keccak256(path) != keccak256(message.path)) { + revert LCPClientMembershipVerificationInvalidPath(); + } + if (consensusState.stateId != message.stateId) { + revert LCPClientMembershipVerificationInvalidStateId(); + } + } + + function verifyCommitmentProofs( + ClientStorage storage clientStorage, + LCPCommitment.CommitmentProofs memory commitmentProofs + ) internal view { + bytes32 commitment = keccak256(commitmentProofs.message); + verifySignatures(clientStorage, commitment, commitmentProofs.signatures); + } + + function verifySignatures(ClientStorage storage clientStorage, bytes32 commitment, bytes[] memory signatures) + internal + view + { + uint256 sigNum = signatures.length; + uint256 opNum = clientStorage.clientState.operators.length; + if (opNum == 0) { + if (sigNum != 1) { + revert LCPClientInvalidSignaturesLength(); + } + ensureActiveKey(clientStorage, verifyECDSASignature(commitment, signatures[0])); + } else { + if (sigNum != opNum) { + revert LCPClientInvalidSignaturesLength(); + } + uint256 success = 0; + for (uint256 i = 0; i < sigNum; i++) { + bytes memory sig = signatures[i]; + if (sig.length != 0) { + ensureActiveKey( + clientStorage, + verifyECDSASignature(commitment, sig), + address(bytes20(clientStorage.clientState.operators[i])) + ); + unchecked { + success++; + } + } + } + ensureSufficientValidSignatures(clientStorage.clientState, success); + } + } } diff --git a/contracts/LCPClient.sol b/contracts/LCPClientIAS.sol similarity index 56% rename from contracts/LCPClient.sol rename to contracts/LCPClientIAS.sol index 9270fe2..dea10aa 100644 --- a/contracts/LCPClient.sol +++ b/contracts/LCPClientIAS.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.12; -import {LCPClientBase} from "./LCPClientBase.sol"; +import {LCPClientIASBase} from "./LCPClientIASBase.sol"; -contract LCPClient is LCPClientBase { +contract LCPClientIAS is LCPClientIASBase { constructor(address ibcHandler_, bool developmentMode_, bytes memory rootCACert) - LCPClientBase(ibcHandler_, developmentMode_) + LCPClientIASBase(ibcHandler_, developmentMode_) { initializeRootCACert(rootCACert); } diff --git a/contracts/LCPClientIASBase.sol b/contracts/LCPClientIASBase.sol new file mode 100644 index 0000000..84a46cb --- /dev/null +++ b/contracts/LCPClientIASBase.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +import {IBCHeight} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/02-client/IBCHeight.sol"; +import {Height} from "@hyperledger-labs/yui-ibc-solidity/contracts/proto/Client.sol"; +import { + IbcLightclientsLcpV1ClientState as ProtoClientState, + IbcLightclientsLcpV1RegisterEnclaveKeyMessage as RegisterEnclaveKeyMessage +} from "./proto/ibc/lightclients/lcp/v1/LCP.sol"; +import {LCPOperator} from "./LCPOperator.sol"; +import {LCPProtoMarshaler} from "./LCPProtoMarshaler.sol"; +import {LCPClientBase} from "./LCPClientBase.sol"; +import {AVRValidator} from "./AVRValidator.sol"; + +abstract contract LCPClientIASBase is LCPClientBase { + using IBCHeight for Height.Data; + + /// @dev if developmentMode is true, the client allows the target enclave which is debug mode enabled. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + bool internal immutable developmentMode; + + // --------------------- Constructor --------------------- + + /// @custom:oz-upgrades-unsafe-allow constructor + /// @param ibcHandler_ the address of the IBC handler contract + /// @param developmentMode_ if true, the client allows the enclave debug mode + constructor(address ibcHandler_, bool developmentMode_) LCPClientBase(ibcHandler_) { + developmentMode = developmentMode_; + } + + // --------------------- Events --------------------- + + /// @dev Emitted when an enclave key from IAS report is registered. + event RegisteredEnclaveKey(string clientId, address enclaveKey, uint256 expiredAt, address operator); + + // --------------------- Storage fields --------------------- + + /// @dev RootCA's public key parameters + AVRValidator.RSAParams internal verifiedRootCAParams; + /// @dev keccak256(signingCert) => RSAParams of signing public key + mapping(bytes32 => AVRValidator.RSAParams) internal verifiedSigningRSAParams; + + /// @dev Reserved storage space to allow for layout changes in the future + uint256[50] private __gap; + + // --------------------- Public methods --------------------- + + /// @dev isDevelopmentMode returns true if the client allows the enclave debug mode. + function isDevelopmentMode() public view returns (bool) { + return developmentMode; + } + + /** + * @dev initializeClient initializes a new client with the given state. + * If succeeded, it returns heights at which the consensus state are stored. + * This function is guaranteed by the IBC contract to be called only once for each `clientId`. + * @param clientId the client identifier which is unique within the IBC handler + */ + function initializeClient( + string calldata clientId, + bytes calldata protoClientState, + bytes calldata protoConsensusState + ) public override onlyIBC returns (Height.Data memory height) { + (ProtoClientState.Data memory clientState,) = + _initializeClient(clientStorages[clientId], protoClientState, protoConsensusState); + if (clientState.key_expiration == 0) { + revert LCPClientClientStateInvalidKeyExpiration(); + } + return clientState.latest_height; + } + + /** + * @dev routeUpdateClient returns the calldata to the receiving function of the client message. + * Light client contract may encode a client message as other encoding scheme(e.g. ethereum ABI) + * Check ADR-001 for details. + */ + function routeUpdateClient(string calldata clientId, bytes calldata protoClientMessage) + public + pure + override + returns (bytes4, bytes memory) + { + (bytes32 typeUrlHash, bytes memory args) = LCPProtoMarshaler.routeClientMessage(clientId, protoClientMessage); + if (typeUrlHash == LCPProtoMarshaler.UPDATE_CLIENT_MESSAGE_TYPE_URL_HASH) { + return (this.updateClient.selector, args); + } else if (typeUrlHash == LCPProtoMarshaler.REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL_HASH) { + return (this.registerEnclaveKey.selector, args); + } else if (typeUrlHash == LCPProtoMarshaler.UPDATE_OPERATORS_MESSAGE_TYPE_URL_HASH) { + return (this.updateOperators.selector, args); + } else { + revert LCPClientUnknownProtoTypeUrl(); + } + } + + /** + * @dev registerEnclaveKey validates the IAS report and registers the enclave key from the report data. + */ + function registerEnclaveKey(string calldata clientId, RegisterEnclaveKeyMessage.Data calldata message) + public + returns (Height.Data[] memory heights) + { + ClientStorage storage clientStorage = clientStorages[clientId]; + AVRValidator.ReportExtractedElements memory reElems = AVRValidator.verifyReport( + developmentMode, + verifiedRootCAParams, + verifiedSigningRSAParams, + clientStorage.allowedStatuses, + message.report, + message.signing_cert, + message.signature + ); + + if (bytes32(clientStorage.clientState.mrenclave) != reElems.mrenclave) { + revert LCPClientClientStateUnexpectedMrenclave(); + } + + // if `operator_signature` is empty, the operator address is zero + address operator; + if (message.operator_signature.length != 0) { + operator = verifyECDSASignature( + keccak256(LCPOperator.computeEIP712RegisterEnclaveKey(message.report)), message.operator_signature + ); + } + if (reElems.operator != address(0) && reElems.operator != operator) { + revert LCPClientAVRUnexpectedOperator(operator, reElems.operator); + } + uint64 expiredAt = reElems.attestationTime + clientStorage.clientState.key_expiration; + if (expiredAt <= block.timestamp) { + revert LCPClientAVRAlreadyExpired(); + } + EKInfo storage ekInfo = clientStorage.ekInfos[reElems.enclaveKey]; + if (ekInfo.expiredAt != 0) { + if (ekInfo.operator != operator) { + revert LCPClientEnclaveKeyUnexpectedOperator(ekInfo.operator, operator); + } + if (ekInfo.expiredAt != expiredAt) { + revert LCPClientEnclaveKeyUnexpectedExpiredAt(); + } + // NOTE: if the key already exists, don't update any state + return heights; + } + ekInfo.expiredAt = expiredAt; + ekInfo.operator = operator; + + emit RegisteredEnclaveKey(clientId, reElems.enclaveKey, expiredAt, operator); + + // Note: client and consensus state are not always updated in registerEnclaveKey + return heights; + } + + // --------------------- Internal methods --------------------- + + /// @dev initializeRootCACert initializes the root CA's public key parameters. + /// All contracts that inherit LCPClientIASBase should call this in the constructor or initializer. + function initializeRootCACert(bytes memory rootCACert) internal { + if (verifiedRootCAParams.notAfter != 0) { + revert LCPClientRootCACertAlreadyInitialized(); + } + verifiedRootCAParams = AVRValidator.verifyRootCACert(rootCACert); + } +} diff --git a/contracts/LCPClientOwnableUpgradeable.sol b/contracts/LCPClientIASOwnableUpgradeable.sol similarity index 78% rename from contracts/LCPClientOwnableUpgradeable.sol rename to contracts/LCPClientIASOwnableUpgradeable.sol index c0e8827..5fd92de 100644 --- a/contracts/LCPClientOwnableUpgradeable.sol +++ b/contracts/LCPClientIASOwnableUpgradeable.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.12; -import {LCPClientBase} from "./LCPClientBase.sol"; +import {LCPClientIASBase} from "./LCPClientIASBase.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /// @custom:oz-upgrades-unsafe-allow external-library-linking -contract LCPClientOwnableUpgradeable is LCPClientBase, UUPSUpgradeable, OwnableUpgradeable { +contract LCPClientIASOwnableUpgradeable is LCPClientIASBase, UUPSUpgradeable, OwnableUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address ibcHandler, bool developmentMode) LCPClientBase(ibcHandler, developmentMode) {} + constructor(address ibcHandler, bool developmentMode) LCPClientIASBase(ibcHandler, developmentMode) {} function initialize(bytes memory rootCACert) public initializer { initializeRootCACert(rootCACert); diff --git a/contracts/LCPClientZKDCAP.sol b/contracts/LCPClientZKDCAP.sol new file mode 100644 index 0000000..6b39239 --- /dev/null +++ b/contracts/LCPClientZKDCAP.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +import {LCPClientZKDCAPBase} from "./LCPClientZKDCAPBase.sol"; + +contract LCPClientZKDCAP is LCPClientZKDCAPBase { + constructor(address ibcHandler_, bool developmentMode_, bytes memory intelRootCA, address riscZeroVerifier) + LCPClientZKDCAPBase(ibcHandler_, developmentMode_, intelRootCA, riscZeroVerifier) + {} +} diff --git a/contracts/LCPClientZKDCAPBase.sol b/contracts/LCPClientZKDCAPBase.sol new file mode 100644 index 0000000..c000d69 --- /dev/null +++ b/contracts/LCPClientZKDCAPBase.sol @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +import {IBCHeight} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/02-client/IBCHeight.sol"; +import {Height} from "@hyperledger-labs/yui-ibc-solidity/contracts/proto/Client.sol"; +import {IIBCHandler} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/25-handler/IIBCHandler.sol"; +import {IRiscZeroVerifier} from "risc0-ethereum/contracts/src/IRiscZeroVerifier.sol"; +import { + IbcLightclientsLcpV1ClientState as ProtoClientState, + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage as ZKDCAPRegisterEnclaveKeyMessage +} from "./proto/ibc/lightclients/lcp/v1/LCP.sol"; +import {LCPProtoMarshaler} from "./LCPProtoMarshaler.sol"; +import {LCPClientBase} from "./LCPClientBase.sol"; +import {LCPOperator} from "./LCPOperator.sol"; +import {RemoteAttestation} from "./RemoteAttestation.sol"; +import {DCAPValidator} from "./DCAPValidator.sol"; + +abstract contract LCPClientZKDCAPBase is LCPClientBase { + using IBCHeight for Height.Data; + // --------------------- Constants --------------------- + + uint8 internal constant ZKVM_TYPE_RISC_ZERO = 0x01; + + // --------------------- Events --------------------- + + /// @dev Emitted when an enclave key from zkDCAP quote is registered. + event LCPClientZKDCAPRegisteredEnclaveKey(string clientId, address enclaveKey, uint256 expiredAt, address operator); + + /// @dev Emitted when the current TCB evaluation data number is updated. + event LCPClientZKDCAPUpdateCurrentTcbEvaluationDataNumber(string clientId, uint32 tcbEvaluationDataNumber); + /// @dev Emitted when the next TCB evaluation data number is updated. + /// This event is emitted only when the new next TCB evaluation data number is set. + event LCPClientZKDCAPUpdateNextTcbEvaluationDataNumber(string clientId, uint32 tcbEvaluationDataNumber); + + // --------------------- Immutable fields --------------------- + + /// @dev if developmentMode is true, the client allows the target enclave which is debug mode enabled. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + bool internal immutable developmentMode; + + /// @notice The hash of the root CA's public key certificate. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + bytes32 public immutable intelRootCAHash; + + /// @notice RISC Zero verifier contract address. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IRiscZeroVerifier public immutable riscZeroVerifier; + + // --------------------- Storage fields --------------------- + + /// @dev Reserved storage space to allow for layout changes in the future + uint256[50] private __gap; + + // --------------------- Constructor --------------------- + + /// @custom:oz-upgrades-unsafe-allow constructor + /// @param ibcHandler_ the address of the IBC handler contract + constructor(address ibcHandler_, bool developmentMode_, bytes memory intelRootCA, address riscZeroVerifier_) + LCPClientBase(ibcHandler_) + { + if (intelRootCA.length == 0 || riscZeroVerifier_ == address(0)) { + revert LCPClientZKDCAPInvalidConstructorParams(); + } + intelRootCAHash = keccak256(intelRootCA); + riscZeroVerifier = IRiscZeroVerifier(riscZeroVerifier_); + developmentMode = developmentMode_; + } + + // --------------------- Public methods --------------------- + + /** + * @dev initializeClient initializes a new client with the given state. + * If succeeded, it returns heights at which the consensus state are stored. + * This function is guaranteed by the IBC contract to be called only once for each `clientId`. + * @param clientId the client identifier which is unique within the IBC handler + */ + function initializeClient( + string calldata clientId, + bytes calldata protoClientState, + bytes calldata protoConsensusState + ) public override onlyIBC returns (Height.Data memory height) { + ClientStorage storage clientStorage = clientStorages[clientId]; + (ProtoClientState.Data memory clientState,) = + _initializeClient(clientStorage, protoClientState, protoConsensusState); + if (clientState.current_tcb_evaluation_data_number == 0) { + revert LCPClientZKDCAPCurrentTcbEvaluationDataNumberNotSet(); + } + // check if both next_tcb_evaluation_data_number and next_tcb_evaluation_data_number_update_time are zero or non-zero + if ( + (clientState.next_tcb_evaluation_data_number == 0) + != (clientState.next_tcb_evaluation_data_number_update_time == 0) + ) { + revert LCPClientZKDCAPInvalidNextTcbEvaluationDataNumberInfo(); + } + if ( + clientState.next_tcb_evaluation_data_number != 0 + && clientState.current_tcb_evaluation_data_number >= clientState.next_tcb_evaluation_data_number + ) { + revert LCPClientZKDCAPInvalidNextTcbEvaluationDataNumberInfo(); + } + if (clientState.zkdcap_verifier_infos.length != 1) { + revert LCPClientZKDCAPInvalidVerifierInfos(); + } + // Currently, the client only supports RISC Zero zkVM + clientStorage.zkDCAPRisc0ImageId = parseRiscZeroVerifierInfo(clientState.zkdcap_verifier_infos[0]); + return clientState.latest_height; + } + + /** + * @dev routeUpdateClient returns the calldata to the receiving function of the client message. + * Light client contract may encode a client message as other encoding scheme(e.g. ethereum ABI) + * Check ibc-solidity's ADR-001 for details. + */ + function routeUpdateClient(string calldata clientId, bytes calldata protoClientMessage) + public + pure + override + returns (bytes4, bytes memory) + { + (bytes32 typeUrlHash, bytes memory args) = LCPProtoMarshaler.routeClientMessage(clientId, protoClientMessage); + if (typeUrlHash == LCPProtoMarshaler.UPDATE_CLIENT_MESSAGE_TYPE_URL_HASH) { + return (this.updateClient.selector, args); + } else if (typeUrlHash == LCPProtoMarshaler.ZKDCAP_REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL_HASH) { + return (this.zkDCAPRegisterEnclaveKey.selector, args); + } else if (typeUrlHash == LCPProtoMarshaler.UPDATE_OPERATORS_MESSAGE_TYPE_URL_HASH) { + return (this.updateOperators.selector, args); + } else { + revert LCPClientUnknownProtoTypeUrl(); + } + } + + /** + * @dev zkDCAPRegisterEnclaveKey validates the zkDCAP proof and registers the enclave key from the commit. + * @notice The client only supports RISC Zero zkVM currently. + * @param clientId the client identifier + * @param message the message to register the enclave key with the zkDCAP proof + * @return heights the heights at which the new consensus states are stored. It is always empty because the consensus state is never updated in this function. + */ + function zkDCAPRegisterEnclaveKey(string calldata clientId, ZKDCAPRegisterEnclaveKeyMessage.Data calldata message) + public + returns (Height.Data[] memory heights) + { + // Currently, the client only supports RISC Zero zkVM + if (message.zkvm_type != ZKVM_TYPE_RISC_ZERO) { + revert LCPClientZKDCAPUnsupportedZKVMType(); + } + ClientStorage storage clientStorage = clientStorages[clientId]; + if (clientStorage.zkDCAPRisc0ImageId == bytes32(0)) { + revert LCPClientZKDCAPRisc0ImageIdNotSet(); + } + ProtoClientState.Data storage clientState = clientStorage.clientState; + // NOTE: the client must revert if the proof is invalid + riscZeroVerifier.verify( + message.proof, clientStorage.zkDCAPRisc0ImageId, sha256(message.quote_verification_output) + ); + DCAPValidator.Output memory output = DCAPValidator.parseOutput(message.quote_verification_output); + if (output.sgxIntelRootCAHash != intelRootCAHash) { + revert LCPClientZKDCAPUnexpectedIntelRootCAHash(); + } + if (output.mrenclave != bytes32(clientState.mrenclave)) { + revert LCPClientClientStateUnexpectedMrenclave(); + } + + // Check if the TCB status and advisory IDs are allowed + + // if the TCB status is not up-to-date, the client should check if the status is allowed + if ( + keccak256(bytes(output.tcbStatus)) != DCAPValidator.TCB_STATUS_UP_TO_DATE_KECCAK256_HASH + && clientStorage.allowedStatuses.allowedQuoteStatuses[output.tcbStatus] != RemoteAttestation.FLAG_ALLOWED + ) { + revert LCPClientZKDCAPDisallowedTCBStatus(); + } + + // if the advisory IDs are not empty, the client should check if the advisories are allowed + for (uint256 i = 0; i < output.advisoryIDs.length; i++) { + if ( + clientStorage.allowedStatuses.allowedAdvisories[output.advisoryIDs[i]] != RemoteAttestation.FLAG_ALLOWED + ) { + revert LCPClientZKDCAPDisallowedAdvisoryID(); + } + } + + // check if the validity period of the output is valid at the current block timestamp + if (block.timestamp < output.validityNotBefore || block.timestamp > output.validityNotAfter) { + revert LCPClientZKDCAPOutputNotValid(); + } + + // check if the `output.enclaveDebugEnabled` and `developmentMode` are consistent + if (output.enclaveDebugEnabled != developmentMode) { + revert LCPClientZKDCAPUnexpectedEnclaveDebugMode(); + } + + // calculate the expiration time of the enclave key + uint64 expiredAt; + if (clientState.key_expiration == 0) { + // If the value is 0, the validity period of the EK is `qv_output.validity.not_after`. + expiredAt = output.validityNotAfter; + } else { + // If the value is greater than 0, the validity period of the EK is min(`output.validty.not_before + key_expiration`, `output.validity.not_after`). + expiredAt = output.validityNotBefore + clientState.key_expiration; + if (expiredAt > output.validityNotAfter) { + expiredAt = output.validityNotAfter; + } + if (expiredAt <= block.timestamp) { + revert LCPClientEnclaveKeyExpired(); + } + } + + // check if the TCB evaluation data number is updated + (bool currentUpdated, bool nextUpdated) = + checkAndUpdateTcbEvaluationDataNumber(clientId, output.minTcbEvaluationDataNumber); + if (currentUpdated) { + emit LCPClientZKDCAPUpdateCurrentTcbEvaluationDataNumber( + clientId, clientState.current_tcb_evaluation_data_number + ); + } + if (nextUpdated) { + emit LCPClientZKDCAPUpdateNextTcbEvaluationDataNumber(clientId, clientState.next_tcb_evaluation_data_number); + } + if (currentUpdated || nextUpdated) { + // update the commitment of the client state in the IBC handler + // `heights` is always empty because the consensus state is never updated in this function + IIBCHandler(ibcHandler).updateClientCommitments(clientId, heights); + } + + // if `operator_signature` is empty, the operator address is zero + address operator; + if (message.operator_signature.length != 0) { + operator = verifyECDSASignature( + keccak256( + LCPOperator.computeEIP712ZKDCAPRegisterEnclaveKey( + clientState.zkdcap_verifier_infos[0], keccak256(message.quote_verification_output) + ) + ), + message.operator_signature + ); + } + if (output.operator != address(0) && output.operator != operator) { + revert LCPClientZKDCAPOutputReportUnexpectedOperator(operator, output.operator); + } + + EKInfo storage ekInfo = clientStorage.ekInfos[output.enclaveKey]; + if (ekInfo.expiredAt != 0) { + if (ekInfo.operator != operator) { + revert LCPClientEnclaveKeyUnexpectedOperator(ekInfo.operator, operator); + } + if (ekInfo.expiredAt != expiredAt) { + revert LCPClientEnclaveKeyUnexpectedExpiredAt(); + } + return heights; + } + ekInfo.expiredAt = expiredAt; + ekInfo.operator = operator; + + emit LCPClientZKDCAPRegisteredEnclaveKey(clientId, output.enclaveKey, expiredAt, operator); + + return heights; + } + + // --------------------- Internal methods --------------------- // + + function parseRiscZeroVerifierInfo(bytes memory verifierInfo) internal pure returns (bytes32) { + // The format is as follows: + // - First byte (0): zkVM type identifier. + // - Remaining bytes (1–N): zkVM-specific data. + // + // Currently, only RISC Zero zkVM (type=1) is supported, with the following format: + // + // | Byte(s) | Description | + // |---------|-----------------------------| + // | 0 | zkVM type (fixed as 1) | + // | 1–31 | Reserved (set as zero) | + // | 32–63 | Image ID | + uint256 vlen = verifierInfo.length; + if (vlen == 0) { + revert LCPClientZKDCAPInvalidVerifierInfoLength(); + } + // Currently, the client only supports RISC Zero zkVM + if (uint8(verifierInfo[0]) != ZKVM_TYPE_RISC_ZERO) { + revert LCPClientZKDCAPInvalidVerifierInfoZKVMType(); + } + if (vlen < 64) { + revert LCPClientZKDCAPInvalidVerifierInfoLength(); + } + // 32..64 bytes: image ID + bytes32 imageId; + assembly { + imageId := mload(add(add(verifierInfo, 32), 32)) + } + return imageId; + } + + /// @dev Checks and updates the current and next TCB evaluation data numbers based on the observed `outputTcbEvaluationDataNumber`. + /// + /// The update logic aligns strictly with the proto definition in `LCP.proto`: + /// - If the reserved next number's update time has arrived, it immediately replaces the current number. + /// - Observing a number greater than the current number triggers updates depending on the configured grace period: + /// - Zero grace period: Immediate update; no next number reserved. + /// - Non-zero grace period: + /// - If no next number reserved yet, reserve the observed number. + /// - If a next number is already reserved: + /// - General case: No action required if the observed number matches the reserved number. + /// - Edge case 1 (current < next < observed): Immediate update of current number to reserved number; reserve newly observed number. + /// - Edge case 2 (current < observed < next): Immediate update of current number to observed number; reserved number unchanged. + /// + /// @param clientId Client identifier + /// @param outputTcbEvaluationDataNumber Observed TCB evaluation data number + /// @return currentUpdated True if current number is updated + /// @return nextUpdated True if next number is updated or reserved + function checkAndUpdateTcbEvaluationDataNumber(string calldata clientId, uint32 outputTcbEvaluationDataNumber) + internal + returns (bool currentUpdated, bool nextUpdated) + { + ProtoClientState.Data storage clientState = clientStorages[clientId].clientState; + + // Check if the reserved next TCB number is due for update. + if ( + clientState.next_tcb_evaluation_data_number != 0 + && block.timestamp >= clientState.next_tcb_evaluation_data_number_update_time + ) { + clientState.current_tcb_evaluation_data_number = clientState.next_tcb_evaluation_data_number; + clientState.next_tcb_evaluation_data_number = 0; + clientState.next_tcb_evaluation_data_number_update_time = 0; + currentUpdated = true; + // No new next number reservation here. + } + + if (outputTcbEvaluationDataNumber > clientState.current_tcb_evaluation_data_number) { + if (clientState.tcb_evaluation_data_number_update_grace_period == 0) { + // Immediate update due to zero grace period. + clientState.current_tcb_evaluation_data_number = outputTcbEvaluationDataNumber; + // Sanity check: No next number should be reserved if grace period is zero. + require( + clientState.next_tcb_evaluation_data_number == 0 + && clientState.next_tcb_evaluation_data_number_update_time == 0 + ); + return (true, false); + } else { + uint64 nextUpdateTime = + uint64(block.timestamp) + clientState.tcb_evaluation_data_number_update_grace_period; + + if (clientState.next_tcb_evaluation_data_number == 0) { + // No reserved number yet; reserve now. + clientState.next_tcb_evaluation_data_number = outputTcbEvaluationDataNumber; + clientState.next_tcb_evaluation_data_number_update_time = nextUpdateTime; + return (currentUpdated, true); + } + + if (outputTcbEvaluationDataNumber > clientState.next_tcb_evaluation_data_number) { + // Edge case 1: Immediate update to previously reserved next number. + clientState.current_tcb_evaluation_data_number = clientState.next_tcb_evaluation_data_number; + clientState.next_tcb_evaluation_data_number = outputTcbEvaluationDataNumber; + clientState.next_tcb_evaluation_data_number_update_time = nextUpdateTime; + return (true, true); + } else if (outputTcbEvaluationDataNumber < clientState.next_tcb_evaluation_data_number) { + // Edge case 2: Immediate update to the newly observed number, keep existing reservation. + clientState.current_tcb_evaluation_data_number = outputTcbEvaluationDataNumber; + return (true, false); + } else { + // General case: The observed number is already reserved; no action required. + return (currentUpdated, false); + } + } + } else if (outputTcbEvaluationDataNumber < clientState.current_tcb_evaluation_data_number) { + // Reverting due to invalid backward update. + revert LCPClientZKDCAPUnexpectedTcbEvaluationDataNumber(clientState.current_tcb_evaluation_data_number); + } else { + // Observed number matches current; no updates necessary. + return (currentUpdated, false); + } + } +} diff --git a/contracts/LCPClientZKDCAPOwnableUpgradeable.sol b/contracts/LCPClientZKDCAPOwnableUpgradeable.sol new file mode 100644 index 0000000..a97c80e --- /dev/null +++ b/contracts/LCPClientZKDCAPOwnableUpgradeable.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +import {LCPClientZKDCAPBase} from "./LCPClientZKDCAPBase.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/// @custom:oz-upgrades-unsafe-allow external-library-linking +contract LCPClientZKDCAPOwnableUpgradeable is LCPClientZKDCAPBase, UUPSUpgradeable, OwnableUpgradeable { + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address ibcHandler_, bool developmentMode_, bytes memory intelRootCA, address riscZeroVerifier_) + LCPClientZKDCAPBase(ibcHandler_, developmentMode_, intelRootCA, riscZeroVerifier_) + {} + + function initialize() public initializer { + __UUPSUpgradeable_init(); + __Ownable_init(msg.sender); + } + + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner {} +} diff --git a/contracts/LCPOperator.sol b/contracts/LCPOperator.sol index adca35a..58351be 100644 --- a/contracts/LCPOperator.sol +++ b/contracts/LCPOperator.sol @@ -7,6 +7,8 @@ library LCPOperator { bytes32 internal constant TYPEHASH_DOMAIN_SEPARATOR = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"); bytes32 internal constant TYPEHASH_REGISTER_ENCLAVE_KEY = keccak256("RegisterEnclaveKey(string avr)"); + bytes32 internal constant TYPEHASH_ZKDCAP_REGISTER_ENCLAVE_KEY = + keccak256("ZKDCAPRegisterEnclaveKey(bytes zkDCAPVerifierInfo,bytes32 outputHash)"); bytes32 internal constant TYPEHASH_UPDATE_OPERATORS = keccak256( "UpdateOperators(string clientId,uint64 nonce,address[] newOperators,uint64 thresholdNumerator,uint64 thresholdDenominator)" ); @@ -15,7 +17,7 @@ library LCPOperator { bytes32 internal constant DOMAIN_SEPARATOR_VERSION = keccak256("1"); // domainSeparatorUniversal() - bytes32 internal constant DOMAIN_SEPARATOR_REGISTER_ENCLAVE_KEY = + bytes32 internal constant DOMAIN_SEPARATOR_LCP_CLIENT = 0x7fd21c2453e80741907e7ff11fd62ae1daa34c6fc0c2eced821f1c1d3fe88a4c; ChainType internal constant CHAIN_TYPE_EVM = ChainType.wrap(1); // chainTypeSalt(CHAIN_TYPE_EVM, hex"") @@ -49,10 +51,20 @@ library LCPOperator { } function computeEIP712RegisterEnclaveKey(bytes calldata avr) internal pure returns (bytes memory) { + return abi.encodePacked( + hex"1901", DOMAIN_SEPARATOR_LCP_CLIENT, keccak256(abi.encode(TYPEHASH_REGISTER_ENCLAVE_KEY, keccak256(avr))) + ); + } + + function computeEIP712ZKDCAPRegisterEnclaveKey(bytes memory zkdcapVerifierInfo, bytes32 outputHash) + internal + pure + returns (bytes memory) + { return abi.encodePacked( hex"1901", - DOMAIN_SEPARATOR_REGISTER_ENCLAVE_KEY, - keccak256(abi.encode(TYPEHASH_REGISTER_ENCLAVE_KEY, keccak256(avr))) + DOMAIN_SEPARATOR_LCP_CLIENT, + keccak256(abi.encode(TYPEHASH_ZKDCAP_REGISTER_ENCLAVE_KEY, keccak256(zkdcapVerifierInfo), outputHash)) ); } diff --git a/contracts/LCPProtoMarshaler.sol b/contracts/LCPProtoMarshaler.sol index 4f45066..8e5fa1e 100644 --- a/contracts/LCPProtoMarshaler.sol +++ b/contracts/LCPProtoMarshaler.sol @@ -5,6 +5,7 @@ import { IbcLightclientsLcpV1ClientState as ClientState, IbcLightclientsLcpV1ConsensusState as ConsensusState, IbcLightclientsLcpV1RegisterEnclaveKeyMessage as RegisterEnclaveKeyMessage, + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage as ZKDCAPRegisterEnclaveKeyMessage, IbcLightclientsLcpV1UpdateClientMessage as UpdateClientMessage, IbcLightclientsLcpV1UpdateOperatorsMessage as UpdateOperatorsMessage } from "./proto/ibc/lightclients/lcp/v1/LCP.sol"; @@ -13,6 +14,8 @@ import {GoogleProtobufAny as Any} from "@hyperledger-labs/yui-ibc-solidity/contr library LCPProtoMarshaler { string constant UPDATE_CLIENT_MESSAGE_TYPE_URL = "/ibc.lightclients.lcp.v1.UpdateClientMessage"; string constant REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL = "/ibc.lightclients.lcp.v1.RegisterEnclaveKeyMessage"; + string constant ZKDCAP_REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL = + "/ibc.lightclients.lcp.v1.ZKDCAPRegisterEnclaveKeyMessage"; string constant UPDATE_OPERATORS_MESSAGE_TYPE_URL = "/ibc.lightclients.lcp.v1.UpdateOperatorsMessage"; string constant CLIENT_STATE_TYPE_URL = "/ibc.lightclients.lcp.v1.ClientState"; string constant CONSENSUS_STATE_TYPE_URL = "/ibc.lightclients.lcp.v1.ConsensusState"; @@ -20,6 +23,8 @@ library LCPProtoMarshaler { bytes32 constant UPDATE_CLIENT_MESSAGE_TYPE_URL_HASH = keccak256(abi.encodePacked(UPDATE_CLIENT_MESSAGE_TYPE_URL)); bytes32 constant REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL_HASH = keccak256(abi.encodePacked(REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL)); + bytes32 constant ZKDCAP_REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL_HASH = + keccak256(abi.encodePacked(ZKDCAP_REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL)); bytes32 constant UPDATE_OPERATORS_MESSAGE_TYPE_URL_HASH = keccak256(abi.encodePacked(UPDATE_OPERATORS_MESSAGE_TYPE_URL)); bytes32 constant CLIENT_STATE_TYPE_URL_HASH = keccak256(abi.encodePacked(CLIENT_STATE_TYPE_URL)); @@ -47,6 +52,13 @@ library LCPProtoMarshaler { return Any.encode(any); } + function marshal(ZKDCAPRegisterEnclaveKeyMessage.Data calldata message) public pure returns (bytes memory) { + Any.Data memory any; + any.type_url = ZKDCAP_REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL; + any.value = ZKDCAPRegisterEnclaveKeyMessage.encode(message); + return Any.encode(any); + } + function marshal(ClientState.Data calldata clientState) public pure returns (bytes memory) { Any.Data memory anyClientState; anyClientState.type_url = CLIENT_STATE_TYPE_URL; @@ -74,6 +86,10 @@ library LCPProtoMarshaler { } else if (typeUrlHash == REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL_HASH) { RegisterEnclaveKeyMessage.Data memory message = RegisterEnclaveKeyMessage.decode(anyClientMessage.value); return (typeUrlHash, abi.encode(clientId, message)); + } else if (typeUrlHash == ZKDCAP_REGISTER_ENCLAVE_KEY_MESSAGE_TYPE_URL_HASH) { + ZKDCAPRegisterEnclaveKeyMessage.Data memory message = + ZKDCAPRegisterEnclaveKeyMessage.decode(anyClientMessage.value); + return (typeUrlHash, abi.encode(clientId, message)); } else if (typeUrlHash == UPDATE_OPERATORS_MESSAGE_TYPE_URL_HASH) { UpdateOperatorsMessage.Data memory message = UpdateOperatorsMessage.decode(anyClientMessage.value); return (typeUrlHash, abi.encode(clientId, message)); diff --git a/contracts/RemoteAttestation.sol b/contracts/RemoteAttestation.sol new file mode 100644 index 0000000..858b16d --- /dev/null +++ b/contracts/RemoteAttestation.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +library RemoteAttestation { + // FLAG_DISALLOWED indicates that the advisory or quote status is not allowed. + uint256 internal constant FLAG_DISALLOWED = 0; + // FLAG_ALLOWED indicates that the advisory or quote status is allowed. + uint256 internal constant FLAG_ALLOWED = 1; + + struct ReportAllowedStatus { + // quote status => flag(0: not allowed, 1: allowed) + mapping(string => uint256) allowedQuoteStatuses; + // advisory id => flag(0: not allowed, 1: allowed) + mapping(string => uint256) allowedAdvisories; + } +} diff --git a/contracts/proto/ibc/lightclients/lcp/v1/LCP.sol b/contracts/proto/ibc/lightclients/lcp/v1/LCP.sol index daa492c..f663873 100644 --- a/contracts/proto/ibc/lightclients/lcp/v1/LCP.sol +++ b/contracts/proto/ibc/lightclients/lcp/v1/LCP.sol @@ -641,6 +641,336 @@ library IbcLightclientsLcpV1RegisterEnclaveKeyMessage { } //library IbcLightclientsLcpV1RegisterEnclaveKeyMessage +library IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage { + + + //struct definition + struct Data { + uint32 zkvm_type; + bytes quote_verification_output; + bytes proof; + bytes operator_signature; + } + + // Decoder section + + /** + * @dev The main decoder for memory + * @param bs The bytes array to be decoded + * @return The decoded struct + */ + function decode(bytes memory bs) internal pure returns (Data memory) { + (Data memory x, ) = _decode(32, bs, bs.length); + return x; + } + + /** + * @dev The main decoder for storage + * @param self The in-storage struct + * @param bs The bytes array to be decoded + */ + function decode(Data storage self, bytes memory bs) internal { + (Data memory x, ) = _decode(32, bs, bs.length); + store(x, self); + } + // inner decoder + + /** + * @dev The decoder for internal usage + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param sz The number of bytes expected + * @return The decoded struct + * @return The number of bytes decoded + */ + function _decode(uint256 p, bytes memory bs, uint256 sz) + internal + pure + returns (Data memory, uint) + { + Data memory r; + uint256 fieldId; + ProtoBufRuntime.WireType wireType; + uint256 bytesRead; + uint256 offset = p; + uint256 pointer = p; + while (pointer < offset + sz) { + (fieldId, wireType, bytesRead) = ProtoBufRuntime._decode_key(pointer, bs); + pointer += bytesRead; + if (fieldId == 1) { + pointer += _read_zkvm_type(pointer, bs, r); + } else + if (fieldId == 2) { + pointer += _read_quote_verification_output(pointer, bs, r); + } else + if (fieldId == 3) { + pointer += _read_proof(pointer, bs, r); + } else + if (fieldId == 4) { + pointer += _read_operator_signature(pointer, bs, r); + } else + { + pointer += ProtoBufRuntime._skip_field_decode(wireType, pointer, bs); + } + + } + return (r, sz); + } + + // field readers + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_zkvm_type( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (uint32 x, uint256 sz) = ProtoBufRuntime._decode_uint32(p, bs); + r.zkvm_type = x; + return sz; + } + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_quote_verification_output( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (bytes memory x, uint256 sz) = ProtoBufRuntime._decode_bytes(p, bs); + r.quote_verification_output = x; + return sz; + } + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_proof( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (bytes memory x, uint256 sz) = ProtoBufRuntime._decode_bytes(p, bs); + r.proof = x; + return sz; + } + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_operator_signature( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (bytes memory x, uint256 sz) = ProtoBufRuntime._decode_bytes(p, bs); + r.operator_signature = x; + return sz; + } + + + // Encoder section + + /** + * @dev The main encoder for memory + * @param r The struct to be encoded + * @return The encoded byte array + */ + function encode(Data memory r) internal pure returns (bytes memory) { + bytes memory bs = new bytes(_estimate(r)); + uint256 sz = _encode(r, 32, bs); + assembly { + mstore(bs, sz) + } + return bs; + } + // inner encoder + + /** + * @dev The encoder for internal usage + * @param r The struct to be encoded + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @return The number of bytes encoded + */ + function _encode(Data memory r, uint256 p, bytes memory bs) + internal + pure + returns (uint) + { + uint256 offset = p; + uint256 pointer = p; + + if (r.zkvm_type != 0) { + pointer += ProtoBufRuntime._encode_key( + 1, + ProtoBufRuntime.WireType.Varint, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_uint32(r.zkvm_type, pointer, bs); + } + if (r.quote_verification_output.length != 0) { + pointer += ProtoBufRuntime._encode_key( + 2, + ProtoBufRuntime.WireType.LengthDelim, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_bytes(r.quote_verification_output, pointer, bs); + } + if (r.proof.length != 0) { + pointer += ProtoBufRuntime._encode_key( + 3, + ProtoBufRuntime.WireType.LengthDelim, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_bytes(r.proof, pointer, bs); + } + if (r.operator_signature.length != 0) { + pointer += ProtoBufRuntime._encode_key( + 4, + ProtoBufRuntime.WireType.LengthDelim, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_bytes(r.operator_signature, pointer, bs); + } + return pointer - offset; + } + // nested encoder + + /** + * @dev The encoder for inner struct + * @param r The struct to be encoded + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @return The number of bytes encoded + */ + function _encode_nested(Data memory r, uint256 p, bytes memory bs) + internal + pure + returns (uint) + { + /** + * First encoded `r` into a temporary array, and encode the actual size used. + * Then copy the temporary array into `bs`. + */ + uint256 offset = p; + uint256 pointer = p; + bytes memory tmp = new bytes(_estimate(r)); + uint256 tmpAddr = ProtoBufRuntime.getMemoryAddress(tmp); + uint256 bsAddr = ProtoBufRuntime.getMemoryAddress(bs); + uint256 size = _encode(r, 32, tmp); + pointer += ProtoBufRuntime._encode_varint(size, pointer, bs); + ProtoBufRuntime.copyBytes(tmpAddr + 32, bsAddr + pointer, size); + pointer += size; + delete tmp; + return pointer - offset; + } + // estimator + + /** + * @dev The estimator for a struct + * @param r The struct to be encoded + * @return The number of bytes encoded in estimation + */ + function _estimate( + Data memory r + ) internal pure returns (uint) { + uint256 e; + e += 1 + ProtoBufRuntime._sz_uint32(r.zkvm_type); + e += 1 + ProtoBufRuntime._sz_lendelim(r.quote_verification_output.length); + e += 1 + ProtoBufRuntime._sz_lendelim(r.proof.length); + e += 1 + ProtoBufRuntime._sz_lendelim(r.operator_signature.length); + return e; + } + // empty checker + + function _empty( + Data memory r + ) internal pure returns (bool) { + + if (r.zkvm_type != 0) { + return false; + } + + if (r.quote_verification_output.length != 0) { + return false; + } + + if (r.proof.length != 0) { + return false; + } + + if (r.operator_signature.length != 0) { + return false; + } + + return true; + } + + + //store function + /** + * @dev Store in-memory struct to storage + * @param input The in-memory struct + * @param output The in-storage struct + */ + function store(Data memory input, Data storage output) internal { + output.zkvm_type = input.zkvm_type; + output.quote_verification_output = input.quote_verification_output; + output.proof = input.proof; + output.operator_signature = input.operator_signature; + + } + + + + //utility functions + /** + * @dev Return an empty struct + * @return r The empty struct + */ + function nil() internal pure returns (Data memory r) { + assembly { + r := 0 + } + } + + /** + * @dev Test whether a struct is empty + * @param x The struct to be tested + * @return r True if it is empty + */ + function isNil(Data memory x) internal pure returns (bool r) { + assembly { + r := iszero(x) + } + } +} +//library IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage + library IbcLightclientsLcpV1UpdateOperatorsMessage { @@ -1110,6 +1440,11 @@ library IbcLightclientsLcpV1ClientState { uint64 operators_nonce; uint64 operators_threshold_numerator; uint64 operators_threshold_denominator; + uint32 current_tcb_evaluation_data_number; + uint32 tcb_evaluation_data_number_update_grace_period; + uint32 next_tcb_evaluation_data_number; + uint64 next_tcb_evaluation_data_number_update_time; + bytes[] zkdcap_verifier_infos; } // Decoder section @@ -1149,7 +1484,7 @@ library IbcLightclientsLcpV1ClientState { returns (Data memory, uint) { Data memory r; - uint[11] memory counters; + uint[16] memory counters; uint256 fieldId; ProtoBufRuntime.WireType wireType; uint256 bytesRead; @@ -1188,6 +1523,21 @@ library IbcLightclientsLcpV1ClientState { if (fieldId == 10) { pointer += _read_operators_threshold_denominator(pointer, bs, r); } else + if (fieldId == 11) { + pointer += _read_current_tcb_evaluation_data_number(pointer, bs, r); + } else + if (fieldId == 12) { + pointer += _read_tcb_evaluation_data_number_update_grace_period(pointer, bs, r); + } else + if (fieldId == 13) { + pointer += _read_next_tcb_evaluation_data_number(pointer, bs, r); + } else + if (fieldId == 14) { + pointer += _read_next_tcb_evaluation_data_number_update_time(pointer, bs, r); + } else + if (fieldId == 15) { + pointer += _read_unpacked_repeated_zkdcap_verifier_infos(pointer, bs, nil(), counters); + } else { pointer += ProtoBufRuntime._skip_field_decode(wireType, pointer, bs); } @@ -1206,6 +1556,10 @@ library IbcLightclientsLcpV1ClientState { require(r.operators.length == 0); r.operators = new bytes[](counters[7]); } + if (counters[15] > 0) { + require(r.zkdcap_verifier_infos.length == 0); + r.zkdcap_verifier_infos = new bytes[](counters[15]); + } while (pointer < offset + sz) { (fieldId, wireType, bytesRead) = ProtoBufRuntime._decode_key(pointer, bs); @@ -1219,6 +1573,9 @@ library IbcLightclientsLcpV1ClientState { if (fieldId == 7) { pointer += _read_unpacked_repeated_operators(pointer, bs, r, counters); } else + if (fieldId == 15) { + pointer += _read_unpacked_repeated_zkdcap_verifier_infos(pointer, bs, r, counters); + } else { pointer += ProtoBufRuntime._skip_field_decode(wireType, pointer, bs); } @@ -1308,7 +1665,7 @@ library IbcLightclientsLcpV1ClientState { uint256 p, bytes memory bs, Data memory r, - uint[11] memory counters + uint[16] memory counters ) internal pure returns (uint) { /** * if `r` is NULL, then only counting the number of fields. @@ -1335,7 +1692,7 @@ library IbcLightclientsLcpV1ClientState { uint256 p, bytes memory bs, Data memory r, - uint[11] memory counters + uint[16] memory counters ) internal pure returns (uint) { /** * if `r` is NULL, then only counting the number of fields. @@ -1362,7 +1719,7 @@ library IbcLightclientsLcpV1ClientState { uint256 p, bytes memory bs, Data memory r, - uint[11] memory counters + uint[16] memory counters ) internal pure returns (uint) { /** * if `r` is NULL, then only counting the number of fields. @@ -1428,6 +1785,101 @@ library IbcLightclientsLcpV1ClientState { return sz; } + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_current_tcb_evaluation_data_number( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (uint32 x, uint256 sz) = ProtoBufRuntime._decode_uint32(p, bs); + r.current_tcb_evaluation_data_number = x; + return sz; + } + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_tcb_evaluation_data_number_update_grace_period( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (uint32 x, uint256 sz) = ProtoBufRuntime._decode_uint32(p, bs); + r.tcb_evaluation_data_number_update_grace_period = x; + return sz; + } + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_next_tcb_evaluation_data_number( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (uint32 x, uint256 sz) = ProtoBufRuntime._decode_uint32(p, bs); + r.next_tcb_evaluation_data_number = x; + return sz; + } + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @return The number of bytes decoded + */ + function _read_next_tcb_evaluation_data_number_update_time( + uint256 p, + bytes memory bs, + Data memory r + ) internal pure returns (uint) { + (uint64 x, uint256 sz) = ProtoBufRuntime._decode_uint64(p, bs); + r.next_tcb_evaluation_data_number_update_time = x; + return sz; + } + + /** + * @dev The decoder for reading a field + * @param p The offset of bytes array to start decode + * @param bs The bytes array to be decoded + * @param r The in-memory struct + * @param counters The counters for repeated fields + * @return The number of bytes decoded + */ + function _read_unpacked_repeated_zkdcap_verifier_infos( + uint256 p, + bytes memory bs, + Data memory r, + uint[16] memory counters + ) internal pure returns (uint) { + /** + * if `r` is NULL, then only counting the number of fields. + */ + (bytes memory x, uint256 sz) = ProtoBufRuntime._decode_bytes(p, bs); + if (isNil(r)) { + counters[15] += 1; + } else { + r.zkdcap_verifier_infos[r.zkdcap_verifier_infos.length - counters[15]] = x; + counters[15] -= 1; + } + return sz; + } + // struct decoder /** * @dev The decoder for reading a inner struct field @@ -1577,6 +2029,53 @@ library IbcLightclientsLcpV1ClientState { ); pointer += ProtoBufRuntime._encode_uint64(r.operators_threshold_denominator, pointer, bs); } + if (r.current_tcb_evaluation_data_number != 0) { + pointer += ProtoBufRuntime._encode_key( + 11, + ProtoBufRuntime.WireType.Varint, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_uint32(r.current_tcb_evaluation_data_number, pointer, bs); + } + if (r.tcb_evaluation_data_number_update_grace_period != 0) { + pointer += ProtoBufRuntime._encode_key( + 12, + ProtoBufRuntime.WireType.Varint, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_uint32(r.tcb_evaluation_data_number_update_grace_period, pointer, bs); + } + if (r.next_tcb_evaluation_data_number != 0) { + pointer += ProtoBufRuntime._encode_key( + 13, + ProtoBufRuntime.WireType.Varint, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_uint32(r.next_tcb_evaluation_data_number, pointer, bs); + } + if (r.next_tcb_evaluation_data_number_update_time != 0) { + pointer += ProtoBufRuntime._encode_key( + 14, + ProtoBufRuntime.WireType.Varint, + pointer, + bs + ); + pointer += ProtoBufRuntime._encode_uint64(r.next_tcb_evaluation_data_number_update_time, pointer, bs); + } + if (r.zkdcap_verifier_infos.length != 0) { + for(i = 0; i < r.zkdcap_verifier_infos.length; i++) { + pointer += ProtoBufRuntime._encode_key( + 15, + ProtoBufRuntime.WireType.LengthDelim, + pointer, + bs) + ; + pointer += ProtoBufRuntime._encode_bytes(r.zkdcap_verifier_infos[i], pointer, bs); + } + } return pointer - offset; } // nested encoder @@ -1636,6 +2135,13 @@ library IbcLightclientsLcpV1ClientState { e += 1 + ProtoBufRuntime._sz_uint64(r.operators_nonce); e += 1 + ProtoBufRuntime._sz_uint64(r.operators_threshold_numerator); e += 1 + ProtoBufRuntime._sz_uint64(r.operators_threshold_denominator); + e += 1 + ProtoBufRuntime._sz_uint32(r.current_tcb_evaluation_data_number); + e += 1 + ProtoBufRuntime._sz_uint32(r.tcb_evaluation_data_number_update_grace_period); + e += 1 + ProtoBufRuntime._sz_uint32(r.next_tcb_evaluation_data_number); + e += 1 + ProtoBufRuntime._sz_uint64(r.next_tcb_evaluation_data_number_update_time); + for(i = 0; i < r.zkdcap_verifier_infos.length; i++) { + e += 1 + ProtoBufRuntime._sz_lendelim(r.zkdcap_verifier_infos[i].length); + } return e; } // empty checker @@ -1680,6 +2186,26 @@ library IbcLightclientsLcpV1ClientState { return false; } + if (r.current_tcb_evaluation_data_number != 0) { + return false; + } + + if (r.tcb_evaluation_data_number_update_grace_period != 0) { + return false; + } + + if (r.next_tcb_evaluation_data_number != 0) { + return false; + } + + if (r.next_tcb_evaluation_data_number_update_time != 0) { + return false; + } + + if (r.zkdcap_verifier_infos.length != 0) { + return false; + } + return true; } @@ -1701,6 +2227,11 @@ library IbcLightclientsLcpV1ClientState { output.operators_nonce = input.operators_nonce; output.operators_threshold_numerator = input.operators_threshold_numerator; output.operators_threshold_denominator = input.operators_threshold_denominator; + output.current_tcb_evaluation_data_number = input.current_tcb_evaluation_data_number; + output.tcb_evaluation_data_number_update_grace_period = input.tcb_evaluation_data_number_update_grace_period; + output.next_tcb_evaluation_data_number = input.next_tcb_evaluation_data_number; + output.next_tcb_evaluation_data_number_update_time = input.next_tcb_evaluation_data_number_update_time; + output.zkdcap_verifier_infos = input.zkdcap_verifier_infos; } @@ -1759,6 +2290,24 @@ library IbcLightclientsLcpV1ClientState { self.operators = tmp; } + //array helpers for ZkdcapVerifierInfos + /** + * @dev Add value to an array + * @param self The in-memory struct + * @param value The value to add + */ + function addZkdcapVerifierInfos(Data memory self, bytes memory value) internal pure { + /** + * First resize the array. Then add the new element to the end. + */ + bytes[] memory tmp = new bytes[](self.zkdcap_verifier_infos.length + 1); + for (uint256 i = 0; i < self.zkdcap_verifier_infos.length; i++) { + tmp[i] = self.zkdcap_verifier_infos[i]; + } + tmp[self.zkdcap_verifier_infos.length] = value; + self.zkdcap_verifier_infos = tmp; + } + //utility functions /** diff --git a/foundry.toml b/foundry.toml index c360c4b..af81449 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = 'contracts' out = 'out' libs = ['lib', 'node_modules'] optimizer = true -optimizer_runs = 9_999_999 +optimizer_runs = 8000 via-ir = false ffi = true ast = true diff --git a/lib/risc0-ethereum b/lib/risc0-ethereum new file mode 160000 index 0000000..b9b22c3 --- /dev/null +++ b/lib/risc0-ethereum @@ -0,0 +1 @@ +Subproject commit b9b22c396a0d5ef97bf02702da9415d5bb79a85a diff --git a/proto/ibc/lightclients/lcp/v1/LCP.proto b/proto/ibc/lightclients/lcp/v1/LCP.proto index 2d3e9ad..253ceb6 100644 --- a/proto/ibc/lightclients/lcp/v1/LCP.proto +++ b/proto/ibc/lightclients/lcp/v1/LCP.proto @@ -3,45 +3,190 @@ package ibc.lightclients.lcp.v1; import "@hyperledger-labs/yui-ibc-solidity/proto/core/02-client/Client.proto"; -option go_package = "github.com/datachainlab/lcp/go/light-clients/lcp/types"; - +// A message containing information required to update the client. message UpdateClientMessage { + // A proxy message generated by the LCP node running on the target platform bytes proxy_message = 1; + // Signatures of the proxy message by the LCP node repeated bytes signatures = 2; } +// A message to verify IAS report and signature for the enclave key registration message RegisterEnclaveKeyMessage { + // IAS report bytes report = 1; + // A signature of the IAS report by the IAS signing key bytes signature = 2; + // A certificate of the IAS signing key bytes signing_cert = 3; + // An operator's signature of the EIP-712 message `RegisterEnclaveKey` + bytes operator_signature = 4; +} + +// A message to verify zkDCAP's output and proof for the enclave key registration +message ZKDCAPRegisterEnclaveKeyMessage { + // A type of zkVM generated the `quote_verification_output` and `proof` + uint32 zkvm_type = 1; + // An output of the zkDCAP program that verifies the quote + bytes quote_verification_output = 2; + // A proof of the zkVM generated the `quote_verification_output` + bytes proof = 3; + // An operator's signature of the EIP-712 message `ZKDCAPRegisterEnclaveKey` bytes operator_signature = 4; } message UpdateOperatorsMessage { + // A nonce for this operators update uint64 nonce = 1; + // A list of new operators repeated bytes new_operators = 2; + // A numerator of the threshold of signatures required for new operators uint64 new_operators_threshold_numerator = 3; + // A denominator of the threshold of signatures required for new operators uint64 new_operators_threshold_denominator = 4; + // Signatures of the EIP-712 message `UpdateOperators` by the current operators repeated bytes signatures = 5; } message ClientState { + // This value strictly identifies the allowed enclave. bytes mrenclave = 1; + + // The `key_expiration` is used to determine the validity period of the EK. + // + // The logic for calculating EK validity periods slightly differs between IAS and DCAP: + // + // IAS: + // - This value must be greater than 0. + // - The EK validity ends at `ias_report.timestamp + key_expiration`. + // + // DCAP: + // - If the value is 0, the EK validity ends at `output.validity.not_after`. + // - If the value is greater than 0, the EK validity ends at: + // min(`qv_output.validity.not_before` + key_expiration, `output.validity.not_after`) + // + // Considerations: + // - Operators should fetch the latest collateral from Intel Provisioning Certification Service (PCS) to ensure the EK validity starts close to the current time. + // - When the EK expires and the TCB evaluation data number has been updated, operators might not be immediately ready + // to operate with the newly accepted TCB status, resulting in availability risks. + // To mitigate this risk, operators should set an appropriate `tcb_evaluation_data_number_update_grace_period`. uint64 key_expiration = 2; + + // Indicates whether the client is frozen. bool frozen = 3; + + // The height of the latest consensus state that the client has tracked Height latest_height = 4; - // e.g. SW_HARDENING_NEEDED, CONFIGURATION_AND_SW_HARDENING_NEEDED (except "OK") + + // Determines which SGX enclave quote statuses are acceptable. + // + // Operators must configure this carefully based on their operational + // security posture and environment-specific considerations. + // + // e.g. IAS: SW_HARDENING_NEEDED, CONFIGURATION_AND_SW_HARDENING_NEEDED + // DCAP: SWHardeningNeeded, ConfigurationAndSWHardeningNeeded repeated string allowed_quote_statuses = 5; - // e.g. INTEL-SA-XXXXX + + // Specifies Security Advisory IDs that operators explicitly allow. + // + // Operators must carefully consider the security implications of allowing specific advisories. + // + // e.g. INTEL-SA-00001, INTEL-SA-00002 repeated string allowed_advisory_ids = 6; + + // A list of LCP operator addresses (ethereum format) associated with this client. + // + // If this field is empty, operator signatures are not required, allowing any entity to act as an operator. + // + // Operational assumptions: + // - At least one operator (including entities not listed in the `operators` field) is expected to promptly reference and report the latest TCB evaluation data number. + // - If no operator promptly reports the latest TCB number, the client continues accepting attestations based on outdated collateral for up to 12 months. + // - Not all operators may immediately prepare an SGX environment compatible with the latest TCB level. + // - The `tcb_evaluation_data_number_update_grace_period` ensures that all operators have a guaranteed minimum period to update their SGX environments, maintaining overall availability. repeated bytes operators = 7; + + // The current nonce used in operator updates. uint64 operators_nonce = 8; + + // The numerator of the signature threshold for operator updates. uint64 operators_threshold_numerator = 9; + + // The denominator of the signature threshold for operator updates. uint64 operators_threshold_denominator = 10; + + // The current TCB evaluation data number + // + // The client only accepts the zkDCAP output generated using collateral with a TCB evaluation data number equal to or greater than this number. + uint32 current_tcb_evaluation_data_number = 11; + + // The grace period (in seconds) for operators to update their SGX environments to support a newly observed TCB evaluation data number. + // + // Notes: + // - A shorter grace period could increase availability risk if operators are not given sufficient time + // to prepare the new SGX environment compatible with the updated TCB level. + // - Conversely, a longer grace period could delay the adoption of the latest TCB level, potentially increasing security risks. + // - Operators must carefully consider their operational preparation needs and security posture when configuring this value. + // + // When a new TCB evaluation data number greater than the current number is observed: + // + // - If the grace period is zero: + // - The current number is updated immediately. + // + // - If the grace period is non-zero: + // - The new number is reserved as `next_tcb_evaluation_data_number`. + // - `next_tcb_evaluation_data_number_update_time` is set to current timestamp plus the grace period. + // + // Edge cases: + // + // - Edge case 1 (current < next < newly observed number): + // - Immediate activation of reserved next number, bypassing the remaining grace period. + // - Newly observed number is reserved as the next number. + // + // - Edge case 2 (current < newly observed number < next): + // - Immediate activation of newly observed number, preserving the reserved next number. + // + // These edge cases can occur due to excessively long grace periods or frequent TCB Recovery Events occurring within shorter intervals than the typical 6-month update frequency. + // Note that we assume operators can maintain an appropriate TCB status based on previous TCB collateral. Therefore, we expect that immediate updates in these edge cases do not cause operational issues. + // Additionally, with a well-configured grace period aligned with typical TCB update intervals, the client will never skip the configured grace period for any TCB number update. + uint32 tcb_evaluation_data_number_update_grace_period = 12; + + // Next TCB evaluation data number scheduled to be updated + // + // Notes: + // - Must be zero if and only if `next_tcb_evaluation_data_number_update_time` is zero. + // - When `tcb_evaluation_data_number_update_grace_period` is zero, this field must always be zero. + // - If this is non-zero, this number must be always greater than the `current_tcb_evaluation_data_number`. + uint32 next_tcb_evaluation_data_number = 13; + + // Scheduled update time of the next TCB evaluation data number (UNIX time seconds) + // + // Notes: + // - Must be zero if and only if `next_tcb_evaluation_data_number` is zero. + // - When `tcb_evaluation_data_number_update_grace_period` is zero, this field must always be zero. + uint64 next_tcb_evaluation_data_number_update_time = 14; + + // Contains verifier-specific information for zkDCAP proofs. + // + // Data format: + // - First byte (0): zkVM type identifier. + // - Remaining bytes (1–N): zkVM-specific data. + // + // Currently, only RISC Zero zkVM (type=1) is supported, with the following format: + // + // | Byte(s) | Description | + // |---------|-----------------------------| + // | 0 | zkVM type (fixed as 1) | + // | 1–31 | Reserved (set as zero) | + // | 32–63 | Image ID | + repeated bytes zkdcap_verifier_infos = 15; } message ConsensusState { + // An identifier that uniquely indicates the ELC state at a specific height + // + // Please check the state ID details: bytes state_id = 1; - // unix timestamp in seconds + // The timestamp of the target chain's block corresponding to the consensus height, + // expressed in UNIX time (seconds). uint64 timestamp = 2; } diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..321f9ec --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +openzeppelin/=lib/risc0-ethereum/lib/openzeppelin-contracts/ diff --git a/slither.config.json b/slither.config.json index 80ba075..9fa09c9 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,4 +1,4 @@ { "detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,deprecated-standards,erc20-indexed,function-init-state,pragma,reentrancy-unlimited-gas,immutable-states,var-read-using-this,dead-code", - "filter_paths": "(test/|node_modules/|contracts/proto/)" + "filter_paths": "(test/|node_modules/|lib/|contracts/proto/)" } \ No newline at end of file diff --git a/test/BytesLib.sol b/test/BytesLib.sol new file mode 100644 index 0000000..3991b90 --- /dev/null +++ b/test/BytesLib.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: Unlicense +// This is free and unencumbered software released into the public domain. + +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. + +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +// For more information, please refer to +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity >=0.8.0 <0.9.0; + +library BytesLib { + function concat(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bytes memory) { + bytes memory tempBytes; + + assembly { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // Store the length of the first bytes array at the beginning of + // the memory for tempBytes. + let length := mload(_preBytes) + mstore(tempBytes, length) + + // Maintain a memory counter for the current write location in the + // temp bytes array by adding the 32 bytes for the array length to + // the starting location. + let mc := add(tempBytes, 0x20) + // Stop copying when the memory counter reaches the length of the + // first bytes array. + let end := add(mc, length) + + for { + // Initialize a copy counter to the start of the _preBytes data, + // 32 bytes into its memory. + let cc := add(_preBytes, 0x20) + } lt(mc, end) { + // Increase both counters by 32 bytes each iteration. + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // Write the _preBytes data into the tempBytes memory 32 bytes + // at a time. + mstore(mc, mload(cc)) + } + + // Add the length of _postBytes to the current length of tempBytes + // and store it as the new length in the first 32 bytes of the + // tempBytes memory. + length := mload(_postBytes) + mstore(tempBytes, add(length, mload(tempBytes))) + + // Move the memory counter back from a multiple of 0x20 to the + // actual end of the _preBytes data. + mc := end + // Stop copying when the memory counter reaches the new combined + // length of the arrays. + end := add(mc, length) + + for { let cc := add(_postBytes, 0x20) } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { mstore(mc, mload(cc)) } + + // Update the free-memory pointer by padding our last write location + // to 32 bytes: add 31 bytes to the end of tempBytes to move to the + // next 32 byte block, then round down to the nearest multiple of + // 32. If the sum of the length of the two arrays is zero then add + // one before rounding down to leave a blank 32 bytes (the length block with 0). + mstore( + 0x40, + and( + add(add(end, iszero(add(length, mload(_preBytes)))), 31), + not(31) // Round down to the nearest 32 bytes. + ) + ) + } + + return tempBytes; + } + + function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal { + assembly { + // Read the first 32 bytes of _preBytes storage, which is the length + // of the array. (We don't need to use the offset into the slot + // because arrays use the entire slot.) + let fslot := sload(_preBytes.slot) + // Arrays of 31 bytes or less have an even value in their slot, + // while longer arrays have an odd value. The actual length is + // the slot divided by two for odd values, and the lowest order + // byte divided by two for even values. + // If the slot is even, bitwise and the slot with 255 and divide by + // two to get the length. If the slot is odd, bitwise and the slot + // with -1 and divide by two. + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + let newlength := add(slength, mlength) + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + switch add(lt(slength, 32), lt(newlength, 32)) + case 2 { + // Since the new array still fits in the slot, we just need to + // update the contents of the slot. + // uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length + sstore( + _preBytes.slot, + // all the modifications to the slot are inside this + // next block + add( + // we can just add to the slot contents because the + // bytes we want to change are the LSBs + fslot, + add( + mul( + div( + // load the bytes from memory + mload(add(_postBytes, 0x20)), + // zero all bytes to the right + exp(0x100, sub(32, mlength)) + ), + // and now shift left the number of bytes to + // leave space for the length in the slot + exp(0x100, sub(32, newlength)) + ), + // increase length by the double of the memory + // bytes length + mul(mlength, 2) + ) + ) + ) + } + case 1 { + // The stored value fits in the slot, but the combined value + // will exceed it. + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // The contents of the _postBytes array start 32 bytes into + // the structure. Our first read should obtain the `submod` + // bytes that can fit into the unused space in the last word + // of the stored array. To get this, we read 32 bytes starting + // from `submod`, so the data we read overlaps with the array + // contents by `submod` bytes. Masking the lowest-order + // `submod` bytes allows us to add that value directly to the + // stored value. + + let submod := sub(32, slength) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore( + sc, + add( + and(fslot, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00), + and(mload(mc), mask) + ) + ) + + for { + mc := add(mc, 0x20) + sc := add(sc, 1) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { sstore(sc, mload(mc)) } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + default { + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + // Start copying to the last used word of the stored array. + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // Copy over the first `submod` bytes of the new data as in + // case 1 above. + let slengthmod := mod(slength, 32) + let mlengthmod := mod(mlength, 32) + let submod := sub(32, slengthmod) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore(sc, add(sload(sc), and(mload(mc), mask))) + + for { + sc := add(sc, 1) + mc := add(mc, 0x20) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { sstore(sc, mload(mc)) } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + } + } + + function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) { + require(_length + 31 >= _length, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { mstore(mc, mload(cc)) } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) { + require(_bytes.length >= _start + 1, "toUint8_outOfBounds"); + uint8 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x1), _start)) + } + + return tempUint; + } + + function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) { + require(_bytes.length >= _start + 2, "toUint16_outOfBounds"); + uint16 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x2), _start)) + } + + return tempUint; + } + + function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) { + require(_bytes.length >= _start + 4, "toUint32_outOfBounds"); + uint32 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x4), _start)) + } + + return tempUint; + } + + function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) { + require(_bytes.length >= _start + 8, "toUint64_outOfBounds"); + uint64 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x8), _start)) + } + + return tempUint; + } + + function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) { + require(_bytes.length >= _start + 12, "toUint96_outOfBounds"); + uint96 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0xc), _start)) + } + + return tempUint; + } + + function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) { + require(_bytes.length >= _start + 16, "toUint128_outOfBounds"); + uint128 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x10), _start)) + } + + return tempUint; + } + + function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) { + require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); + uint256 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x20), _start)) + } + + return tempUint; + } + + function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) { + require(_bytes.length >= _start + 32, "toBytes32_outOfBounds"); + bytes32 tempBytes32; + + assembly { + tempBytes32 := mload(add(add(_bytes, 0x20), _start)) + } + + return tempBytes32; + } + + function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { + bool success = true; + + assembly { + let length := mload(_preBytes) + + // if lengths don't match the arrays are not equal + switch eq(length, mload(_postBytes)) + case 1 { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + let mc := add(_preBytes, 0x20) + let end := add(mc, length) + + for { let cc := add(_postBytes, 0x20) } + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + eq(add(lt(mc, end), cb), 2) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // if any of these checks fails then arrays are not equal + if iszero(eq(mload(mc), mload(cc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } + + function equalStorage(bytes storage _preBytes, bytes memory _postBytes) internal view returns (bool) { + bool success = true; + + assembly { + // we know _preBytes_offset is 0 + let fslot := sload(_preBytes.slot) + // Decode the length of the stored array like in concatStorage(). + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + + // if lengths don't match the arrays are not equal + switch eq(slength, mlength) + case 1 { + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + if iszero(iszero(slength)) { + switch lt(slength, 32) + case 1 { + // blank the last byte which is the length + fslot := mul(div(fslot, 0x100), 0x100) + + if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) { + // unsuccess: + success := 0 + } + } + default { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := keccak256(0x0, 0x20) + + let mc := add(_postBytes, 0x20) + let end := add(mc, mlength) + + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + for {} eq(add(lt(mc, end), cb), 2) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + if iszero(eq(sload(sc), mload(mc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } +} diff --git a/test/ContractUpgrade.t.sol b/test/ContractUpgrade.t.sol index cd755c6..366592e 100644 --- a/test/ContractUpgrade.t.sol +++ b/test/ContractUpgrade.t.sol @@ -4,21 +4,23 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; import {Options} from "openzeppelin-foundry-upgrades/Options.sol"; -import {LCPClientOwnableUpgradeable} from "../contracts/LCPClientOwnableUpgradeable.sol"; contract ContractUpgrade is Test { - string internal constant rootCAFile = "test/data/certs/Intel_SGX_Attestation_RootCA.der"; - - function testUpgrade() public { + function testUpgradeIAS() public { if (!vm.envOr("TEST_UPGRADEABLE", false)) { return; } Options memory opts; opts.constructorData = abi.encode(address(0x01), true); - bytes memory rootCACert = vm.readFileBinary(rootCAFile); - vm.warp(2524607999); - Upgrades.deployUUPSProxy( - "LCPClientOwnableUpgradeable.sol", abi.encodeCall(LCPClientOwnableUpgradeable.initialize, rootCACert), opts - ); + Upgrades.validateImplementation("LCPClientIASOwnableUpgradeable.sol", opts); + } + + function testUpgradeZKDCAP() public { + if (!vm.envOr("TEST_UPGRADEABLE", false)) { + return; + } + Options memory opts; + opts.constructorData = abi.encode(address(0x01), true, 0x01, address(0x01)); + Upgrades.validateImplementation("LCPClientZKDCAPOwnableUpgradeable.sol", opts); } } diff --git a/test/LCPClientBenchmark.t.sol b/test/LCPClientBenchmark.t.sol index a4f3017..6564688 100644 --- a/test/LCPClientBenchmark.t.sol +++ b/test/LCPClientBenchmark.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/utils/Strings.sol"; import "./TestHelper.t.sol"; -import "../contracts/LCPClient.sol"; +import "../contracts/LCPClientIAS.sol"; import { IbcLightclientsLcpV1ClientState as ClientState, IbcLightclientsLcpV1ConsensusState as ConsensusState, @@ -12,7 +12,6 @@ import { } from "../contracts/proto/ibc/lightclients/lcp/v1/LCP.sol"; import {LCPProtoMarshaler} from "../contracts/LCPProtoMarshaler.sol"; import {IBCHeight} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/02-client/IBCHeight.sol"; -import {LCPOperator} from "../contracts/LCPOperator.sol"; abstract contract BaseLCPClientBenchmark is BasicTest { string internal constant commandAvrFile = "test/data/client/02/001-avr"; @@ -88,9 +87,9 @@ abstract contract BaseLCPClientBenchmark is BasicTest { } } -contract BLCPClient is LCPClient { +contract BLCPClient is LCPClientIAS { constructor(address ibcHandler_, bool developmentMode_, bytes memory rootCACert) - LCPClient(ibcHandler_, developmentMode_, rootCACert) + LCPClientIAS(ibcHandler_, developmentMode_, rootCACert) {} function setSigningRSAParams(bytes32 signingCertHash, AVRValidator.RSAParams calldata params) public { diff --git a/test/LCPClientOperator.t.sol b/test/LCPClientOperator.t.sol index 9dddfd8..2fe822c 100644 --- a/test/LCPClientOperator.t.sol +++ b/test/LCPClientOperator.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.12; import {IBCHeight} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/02-client/IBCHeight.sol"; import "./TestHelper.t.sol"; -import "../contracts/LCPClient.sol"; +import "../contracts/LCPClientIAS.sol"; import {LCPProtoMarshaler} from "../contracts/LCPProtoMarshaler.sol"; import {ILCPClientErrors} from "../contracts/ILCPClientErrors.sol"; import { @@ -20,17 +20,17 @@ contract LCPClientOperatorTest is BasicTest { string internal constant commandResultSuffix = "_result"; - LCPClient lc; + LCPClientIAS lc; function setUp() public { vm.warp(1718465726); - lc = new LCPClient(address(this), true, vm.readFileBinary("./test/data/certs/simulation_rootca.der")); + lc = new LCPClientIAS(address(this), true, vm.readFileBinary("./test/data/certs/simulation_rootca.der")); } // ---------------------------- Test Cases ---------------------------- function testPreComputationValues() public pure { - assertEq(LCPOperator.domainSeparatorUniversal(), LCPOperator.DOMAIN_SEPARATOR_REGISTER_ENCLAVE_KEY); + assertEq(LCPOperator.domainSeparatorUniversal(), LCPOperator.DOMAIN_SEPARATOR_LCP_CLIENT); assertEq(LCPOperator.chainTypeSalt(LCPOperator.CHAIN_TYPE_EVM, hex""), LCPOperator.CHAIN_TYPE_EVM_SALT); } diff --git a/test/LCPClientTest.t.sol b/test/LCPClientTest.t.sol index 9752963..f0a8356 100644 --- a/test/LCPClientTest.t.sol +++ b/test/LCPClientTest.t.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.12; import "@openzeppelin/contracts/utils/Strings.sol"; import "@ensdomains/ens-contracts/contracts/dnssec-oracle/BytesUtils.sol"; import {IBCHeight} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/02-client/IBCHeight.sol"; -import "../contracts/LCPClient.sol"; -import "../contracts/LCPClientBase.sol"; +import "../contracts/LCPClientIAS.sol"; +import "../contracts/LCPClientIASBase.sol"; import { IbcLightclientsLcpV1ClientState as ClientState, IbcLightclientsLcpV1ConsensusState as ConsensusState, @@ -22,8 +22,8 @@ contract LCPClientTest is BasicTest { using IBCHeight for Height.Data; TestContext testContext; - LCPClient iasLC; - LCPClient simulationLC; + LCPClientIAS iasLC; + LCPClientIAS simulationLC; string internal constant baseDir = "test/data/client"; uint256 internal constant commandNumberPrefixLength = 4; // "000-" @@ -33,7 +33,7 @@ contract LCPClientTest is BasicTest { struct TestContext { string dir; - LCPClient lc; + LCPClientIAS lc; Vm.Wallet opWallet; } @@ -43,10 +43,12 @@ contract LCPClientTest is BasicTest { function setUp() public { vm.warp(1692703263); - iasLC = - new LCPClient(address(this), true, vm.readFileBinary("./test/data/certs/Intel_SGX_Attestation_RootCA.der")); + iasLC = new LCPClientIAS( + address(this), true, vm.readFileBinary("./test/data/certs/Intel_SGX_Attestation_RootCA.der") + ); require(iasLC.isDevelopmentMode() == true, "developmentMode must be true"); - simulationLC = new LCPClient(address(this), true, vm.readFileBinary("./test/data/certs/simulation_rootca.der")); + simulationLC = + new LCPClientIAS(address(this), true, vm.readFileBinary("./test/data/certs/simulation_rootca.der")); } function testIASClientPermissioned() public { @@ -81,7 +83,7 @@ contract LCPClientTest is BasicTest { string memory commandAvrFile, uint256 commandStartNumber ) internal { - LCPClient lc = testContext.lc; + LCPClientIAS lc = testContext.lc; { ClientState.Data memory clientState; address[] memory opWallets; @@ -112,7 +114,7 @@ contract LCPClientTest is BasicTest { createRegisterEnclaveKeyMessage(commandAvrFile, testContext.opWallet); // the following staticcall is expected to succeed because registerEnclaveKey does not update the state if the message contains an enclave key already registered (bool success,) = address(lc).staticcall( - abi.encodeWithSelector(LCPClientBase.registerEnclaveKey.selector, clientId, message) + abi.encodeWithSelector(LCPClientIASBase.registerEnclaveKey.selector, clientId, message) ); require(success, "failed to register duplicated enclave key"); } diff --git a/test/LCPClientZKDCAPTest.t.sol b/test/LCPClientZKDCAPTest.t.sol new file mode 100644 index 0000000..b0d319c --- /dev/null +++ b/test/LCPClientZKDCAPTest.t.sol @@ -0,0 +1,916 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +import "./TestHelper.t.sol"; +import { + IbcLightclientsLcpV1ClientState, + IbcLightclientsLcpV1ConsensusState, + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage +} from "../contracts/proto/ibc/lightclients/lcp/v1/LCP.sol"; +import {LCPClientZKDCAP} from "../contracts/LCPClientZKDCAP.sol"; +import {LCPClientZKDCAPBase} from "../contracts/LCPClientZKDCAPBase.sol"; +import {LCPProtoMarshaler} from "../contracts/LCPProtoMarshaler.sol"; +import {IRiscZeroVerifier, Receipt} from "risc0-ethereum/contracts/src/test/RiscZeroMockVerifier.sol"; +import {DCAPValidator} from "../contracts/DCAPValidator.sol"; +import {BytesLib} from "./BytesLib.sol"; +import {ILCPClientErrors} from "../contracts/ILCPClientErrors.sol"; +import {LCPOperator} from "../contracts/LCPOperator.sol"; + +contract TestLCPClientZKDCAPExtended is LCPClientZKDCAP { + constructor(address ibcHandler_, bool developmentMode_, bytes memory intelRootCA, address riscZeroVerifier) + LCPClientZKDCAP(ibcHandler_, developmentMode_, intelRootCA, riscZeroVerifier) + {} + + function getDecodedClientState(string memory clientId) + public + view + returns (IbcLightclientsLcpV1ClientState.Data memory) + { + return clientStorages[clientId].clientState; + } + + function getEKInfo(string memory clientId, address ekAddr) public view returns (EKInfo memory) { + return clientStorages[clientId].ekInfos[ekAddr]; + } +} + +contract LCPClientZKDCAPTest is BasicTest { + using BytesLib for bytes; + + function testRegisterEnclaveKeyQvOutputValidity() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + // warp to the time of `output.validityNotBefore` + vm.warp(output.validityNotBefore); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).expiredAt, output.validityNotAfter); + + // if `validityNotBefore` is in the future, it should fail + output = ZKDCAPTestHelper.qvOutput(); + output.validityNotBefore = output.validityNotBefore + 1; + output.enclaveKey = address(2); + vm.expectRevert(); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + + // if `validityNotAfter` is in the past, it should fail + output = ZKDCAPTestHelper.qvOutput(); + output.validityNotAfter = uint64(block.timestamp) - 1; + output.enclaveKey = address(2); + vm.expectRevert(); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + + // if `validityNotAfter` equals to `block.timestamp`, it should succeed + output = ZKDCAPTestHelper.qvOutput(); + output.validityNotAfter = uint64(block.timestamp); + output.enclaveKey = address(2); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).expiredAt, output.validityNotAfter); + } + + function testRegisterEnclaveKeyNotSetGracePeriod() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + + DCAPValidator.Output memory output; + + // if `minTcbEvaluationDataNumber` equals to `clientState.current_tcb_evaluation_data_number`, it should succeed + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 1; + vm.warp(output.validityNotBefore); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + + // if `minTcbEvaluationDataNumber` is less than `clientState.current_tcb_evaluation_data_number`, it should fail + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 1; + output.enclaveKey = address(2); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + + // if `minTcbEvaluationDataNumber` is greater than `clientState.current_tcb_evaluation_data_number`, it should succeed + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 2; + output.enclaveKey = address(3); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + + // if `minTcbEvaluationDataNumber` is less than `clientState.current_tcb_evaluation_data_number`, it should fail + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 1; + output.enclaveKey = address(4); + vm.expectRevert(); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + + // if `minTcbEvaluationDataNumber` is greater than `clientState.current_tcb_evaluation_data_number`, it should succeed + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 5; + output.enclaveKey = address(4); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + + function testRegisterEnclaveKeySetGracePeriod() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.tcb_evaluation_data_number_update_grace_period = 2; + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + + { + // if `minTcbEvaluationDataNumber` equals to `clientState.current_tcb_evaluation_data_number`, it should succeed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 1; + vm.warp(output.validityNotBefore); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 1); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 0); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, 0); + } + + uint64 nextUpdateTime; + { + // if `minTcbEvaluationDataNumber` greater than `clientState.current_tcb_evaluation_data_number`, it should succeed + // and the `current_tcb_evaluation_data_number` should not be updated and the `next_tcb_evaluation_data_number` should be set + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 2; + output.enclaveKey = address(2); + // warp to the time of `output.validityNotBefore` + vm.warp(output.validityNotBefore); + // Note: block.timestamp == output.validityNotBefore + nextUpdateTime = uint64(block.timestamp) + clientState.tcb_evaluation_data_number_update_grace_period; + vm.expectEmit(); + emit LCPClientZKDCAPBase.LCPClientZKDCAPUpdateNextTcbEvaluationDataNumber(clientId, 2); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 1); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 2); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, nextUpdateTime); + } + { + // if current time is within the grace period, it + // should succeed and the `current_tcb_evaluation_data_number` should be not updated + vm.warp(nextUpdateTime - 1); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 2; + output.enclaveKey = address(3); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 1); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 2); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, nextUpdateTime); + } + { + // warp to the time of `nextUpdateTime` + vm.warp(nextUpdateTime); + // if `minTcbEvaluationDataNumber` is equal to `clientState.current_tcb_evaluation_data_number`, it should fail + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 1; + output.enclaveKey = address(3); + vm.expectRevert( + abi.encodeWithSelector(ILCPClientErrors.LCPClientZKDCAPUnexpectedTcbEvaluationDataNumber.selector, 2) + ); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + // save the snapshot that includes the current state (current_tcb_evaluation_data_number = 1, next_tcb_evaluation_data_number = 2) + uint256 sid = vm.snapshot(); + { + // warp to the time of `nextUpdateTime` + vm.warp(nextUpdateTime); + // if `minTcbEvaluationDataNumber` equals to `clientState.current_tcb_evaluation_data_number`, it should succeed + // and the `current_tcb_evaluation_data_number` should be updated + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 2; + output.enclaveKey = address(3); + vm.expectEmit(); + emit LCPClientZKDCAPBase.LCPClientZKDCAPUpdateCurrentTcbEvaluationDataNumber(clientId, 2); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 2); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 0); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, 0); + + // if `minTcbEvaluationDataNumber` equals to `clientState.current_tcb_evaluation_data_number`, it should succeed + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 2; + output.enclaveKey = address(4); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 2); + + // if `minTcbEvaluationDataNumber` is less than `clientState.current_tcb_evaluation_data_number`, it should fail + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 1; + output.enclaveKey = address(5); + vm.expectRevert(); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + // revert to the state (current_tcb_evaluation_data_number = 1, next_tcb_evaluation_data_number = 2) + vm.revertTo(sid); + { + // Edge case 1 (current < next < newly observed number) + // warp to the time of `nextUpdateTime`-1 + vm.warp(nextUpdateTime - 1); + // if `minTcbEvaluationDataNumber` is greater than `clientState.next_tcb_evaluation_data_number`, it should succeed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 3; + output.enclaveKey = address(3); + vm.expectEmit(); + emit LCPClientZKDCAPBase.LCPClientZKDCAPUpdateCurrentTcbEvaluationDataNumber(clientId, 2); + emit LCPClientZKDCAPBase.LCPClientZKDCAPUpdateNextTcbEvaluationDataNumber(clientId, 3); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + // check if the current and next tcb evaluation data numbers are updated + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 2); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 3); + // check if the next update time is rescheduled + assertNotEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, nextUpdateTime); + assertEq( + lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, + block.timestamp + clientState.tcb_evaluation_data_number_update_grace_period + ); + } + // revert to the state (current_tcb_evaluation_data_number = 1, next_tcb_evaluation_data_number = 2) + vm.revertTo(sid); + { + // warp to the time of `nextUpdateTime` + vm.warp(nextUpdateTime); + // if `minTcbEvaluationDataNumber` equals to `clientState.current_tcb_evaluation_data_number`, it should succeed + // and the `current_tcb_evaluation_data_number` should be updated + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 2; + output.enclaveKey = address(3); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 2); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 0); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, 0); + + { + // Edge case 2 (current < newly observed number < next) + + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 4; + output.enclaveKey = address(4); + vm.expectEmit(); + emit LCPClientZKDCAPBase.LCPClientZKDCAPUpdateNextTcbEvaluationDataNumber(clientId, 4); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + uint256 nextUpdateTime2 = block.timestamp + clientState.tcb_evaluation_data_number_update_grace_period; + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 2); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 4); + assertEq( + lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, nextUpdateTime2 + ); + + output = ZKDCAPTestHelper.qvOutput(); + output.minTcbEvaluationDataNumber = 3; + output.enclaveKey = address(5); + emit LCPClientZKDCAPBase.LCPClientZKDCAPUpdateCurrentTcbEvaluationDataNumber(clientId, 3); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + // the current tcb evaluation data number should be updated + assertEq(lc.getDecodedClientState(clientId).current_tcb_evaluation_data_number, 3); + assertEq(lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number, 4); + // the next update time should not be updated + assertEq( + lc.getDecodedClientState(clientId).next_tcb_evaluation_data_number_update_time, nextUpdateTime2 + ); + } + } + } + + function testRegisterEnclaveKeySetKeyNotSetKeyExpiration() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + // 1 sec from `output.validityNotBefore` + clientState.key_expiration = 0; + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + + DCAPValidator.Output memory output; + + // if `key_expiration` is 0 and the current time is within the validity period, it should succeed + // and the key expiration should be set to `validityNotAfter` + output = ZKDCAPTestHelper.qvOutput(); + vm.warp(output.validityNotBefore); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).expiredAt, output.validityNotAfter); + } + + function testRegisterEnclaveKeySetKeyExpiration() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + // 1 sec from `output.validityNotBefore` + clientState.key_expiration = 2; + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + + DCAPValidator.Output memory output; + + // if `validityNotBefore` + `key_expiration` is in the future, it should succeed + output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(1); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq( + lc.getEKInfo(clientId, output.enclaveKey).expiredAt, output.validityNotBefore + clientState.key_expiration + ); + + // if `validityNotBefore` + `key_expiration` is in the past, it should fail + + // warp to the time of `output.validityNotBefore` + `clientState.key_expiration` + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP + clientState.key_expiration); + output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(2); + vm.expectRevert(ILCPClientErrors.LCPClientEnclaveKeyExpired.selector); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + + // if `validityNotBefore` + `key_expiration` equals to `validityNotAfter`, it should succeed + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(2); + output.validityNotAfter = output.validityNotBefore + clientState.key_expiration; + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq( + lc.getEKInfo(clientId, output.enclaveKey).expiredAt, output.validityNotBefore + clientState.key_expiration + ); + + // if `validityNotBefore` + `key_expiration` is greater than `validityNotAfter`, it should succeed + // and the key expiration should be set to `validityNotAfter` + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(3); + output.validityNotAfter = output.validityNotBefore + clientState.key_expiration - 1; + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).expiredAt, output.validityNotAfter); + } + + function testRegisterEnclaveKeyInvalidZkvmType() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory msgData = registerEnclaveKeyMessage(output); + msgData.zkvm_type = 0x02; + vm.expectRevert(abi.encodeWithSelector(ILCPClientErrors.LCPClientZKDCAPUnsupportedZKVMType.selector)); + lc.zkDCAPRegisterEnclaveKey(clientId, msgData); + } + + function testRegisterEnclaveKeyEnclaveDebugMismatch() public { + string memory clientId = "lcp-zkdcap"; + // developmentMode=false but output.enclaveDebugEnabled is set to true + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveDebugEnabled = true; + vm.expectRevert(abi.encodeWithSelector(ILCPClientErrors.LCPClientZKDCAPUnexpectedEnclaveDebugMode.selector)); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + + function testRegisterEnclaveKeyMrenclaveMismatch() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.mrenclave = keccak256(abi.encodePacked("different mrenclave")); + vm.expectRevert(abi.encodeWithSelector(ILCPClientErrors.LCPClientClientStateUnexpectedMrenclave.selector)); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + + function testRegisterEnclaveKeyIntelRootCAMismatch() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.sgxIntelRootCAHash = keccak256(abi.encodePacked("different root cert")); + vm.expectRevert(abi.encodeWithSelector(ILCPClientErrors.LCPClientZKDCAPUnexpectedIntelRootCAHash.selector)); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + + function testRegisterEnclaveKeyTCBStatus() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.allowed_quote_statuses = new string[](1); + clientState.allowed_quote_statuses[0] = DCAPValidator.TCB_STATUS_SW_HARDENING_NEEDED_STRING; + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + { + // OutOfDate status is not allowed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.tcbStatus = DCAPValidator.TCB_STATUS_OUT_OF_DATE_STRING; + vm.expectRevert(abi.encodeWithSelector(ILCPClientErrors.LCPClientZKDCAPDisallowedTCBStatus.selector)); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + { + // SwHardeningNeeded status is allowed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(1); + output.tcbStatus = DCAPValidator.TCB_STATUS_SW_HARDENING_NEEDED_STRING; + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + { + // UpToDate status is allowed by default + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(2); + output.tcbStatus = DCAPValidator.TCB_STATUS_UP_TO_DATE_STRING; + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + } + + function testRegisterEnclaveKeyAllowedAdvisoryID() public { + string memory clientId = "lcp-zkdcap"; + { + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.allowed_advisory_ids = new string[](0); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.advisoryIDs = new string[](1); + output.advisoryIDs[0] = "INTEL-SA-00001"; + vm.expectRevert(abi.encodeWithSelector(ILCPClientErrors.LCPClientZKDCAPDisallowedAdvisoryID.selector)); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + { + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.allowed_advisory_ids = new string[](1); + clientState.allowed_advisory_ids[0] = "INTEL-SA-00001"; + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.advisoryIDs = new string[](1); + output.advisoryIDs[0] = "INTEL-SA-00002"; + vm.expectRevert(abi.encodeWithSelector(ILCPClientErrors.LCPClientZKDCAPDisallowedAdvisoryID.selector)); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + { + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.allowed_advisory_ids = new string[](1); + clientState.allowed_advisory_ids[0] = "INTEL-SA-00001"; + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.advisoryIDs = new string[](1); + output.advisoryIDs[0] = "INTEL-SA-00001"; + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + } + } + + function testRegisterEnclaveKeyOperatorsSet() public { + Vm.Wallet memory op1 = vm.createWallet("op1"); + Vm.Wallet memory op2 = vm.createWallet("op2"); + Vm.Wallet memory op3 = vm.createWallet("op3"); + + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.operators_threshold_denominator = 1; + clientState.operators_threshold_numerator = 1; + clientState.operators = new bytes[](2); + clientState.operators[0] = abi.encodePacked(op3.addr); + clientState.operators[1] = abi.encodePacked(op1.addr); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + + { + // if the operator is set and operator signature is not set, it should fail + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.operator = op1.addr; + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory message = registerEnclaveKeyMessage(output); + vm.expectRevert( + abi.encodeWithSelector( + ILCPClientErrors.LCPClientZKDCAPOutputReportUnexpectedOperator.selector, address(0), output.operator + ) + ); + lc.zkDCAPRegisterEnclaveKey(clientId, message); + } + + { + // if the operator signature is set by a different operator, it should fail + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.operator = op1.addr; + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory message = registerEnclaveKeyMessage(output); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + op2, + keccak256( + LCPOperator.computeEIP712ZKDCAPRegisterEnclaveKey( + clientState.zkdcap_verifier_infos[0], keccak256(ZKDCAPTestHelper.toBytes(output)) + ) + ) + ); + message.operator_signature = abi.encodePacked(r, s, v); + vm.expectRevert( + abi.encodeWithSelector( + ILCPClientErrors.LCPClientZKDCAPOutputReportUnexpectedOperator.selector, op2.addr, op1.addr + ) + ); + lc.zkDCAPRegisterEnclaveKey(clientId, message); + } + + { + // if the operator signature is set by the correct operator, it should succeed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(1); + output.operator = op1.addr; + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory message = registerEnclaveKeyMessage(output); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + op1, + keccak256( + LCPOperator.computeEIP712ZKDCAPRegisterEnclaveKey( + clientState.zkdcap_verifier_infos[0], keccak256(ZKDCAPTestHelper.toBytes(output)) + ) + ) + ); + message.operator_signature = abi.encodePacked(r, s, v); + lc.zkDCAPRegisterEnclaveKey(clientId, message); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).operator, op1.addr); + } + + { + // if both operator and operator signature are not set, it should succeed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(2); + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory message = registerEnclaveKeyMessage(output); + lc.zkDCAPRegisterEnclaveKey(clientId, message); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).operator, address(0)); + } + + { + // if re-registering the same enclave key with a different operator, it should fail + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(1); + output.operator = op1.addr; + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory message = registerEnclaveKeyMessage(output); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + op3, + keccak256( + LCPOperator.computeEIP712ZKDCAPRegisterEnclaveKey( + clientState.zkdcap_verifier_infos[0], keccak256(ZKDCAPTestHelper.toBytes(output)) + ) + ) + ); + message.operator_signature = abi.encodePacked(r, s, v); + vm.expectRevert( + abi.encodeWithSelector( + ILCPClientErrors.LCPClientZKDCAPOutputReportUnexpectedOperator.selector, op3.addr, op1.addr + ) + ); + lc.zkDCAPRegisterEnclaveKey(clientId, message); + } + } + + function testRegisterEnclaveKeyOperatorsNotSet() public { + Vm.Wallet memory op1 = vm.createWallet("op1"); + + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + lc.initializeClient( + clientId, LCPProtoMarshaler.marshal(clientState), LCPProtoMarshaler.marshal(defaultConsensusState()) + ); + + { + // if the operator signature is set, it should succeed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(1); + output.operator = op1.addr; + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + + IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory message = registerEnclaveKeyMessage(output); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + op1, + keccak256( + LCPOperator.computeEIP712ZKDCAPRegisterEnclaveKey( + clientState.zkdcap_verifier_infos[0], keccak256(ZKDCAPTestHelper.toBytes(output)) + ) + ) + ); + message.operator_signature = abi.encodePacked(r, s, v); + lc.zkDCAPRegisterEnclaveKey(clientId, message); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).operator, op1.addr); + } + + { + // if the operator signature is not set, it also should succeed + DCAPValidator.Output memory output = ZKDCAPTestHelper.qvOutput(); + output.enclaveKey = address(2); + lc.zkDCAPRegisterEnclaveKey(clientId, registerEnclaveKeyMessage(output)); + assertEq(lc.getEKInfo(clientId, output.enclaveKey).operator, address(0)); + } + } + + function testInitializeClientInvalidVerifierInfos() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + bytes memory consensusStateBytes = LCPProtoMarshaler.marshal(defaultConsensusState()); + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.zkdcap_verifier_infos[0] = new bytes(0); + bytes memory clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + + clientState.zkdcap_verifier_infos[0] = new bytes(1); + clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + + clientState.zkdcap_verifier_infos = new bytes[](0); + clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + + clientState.zkdcap_verifier_infos = new bytes[](2); + clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + } + + function testInitializeClientInvalidValues() public { + string memory clientId = "lcp-zkdcap"; + TestLCPClientZKDCAPExtended lc = new TestLCPClientZKDCAPExtended( + address(this), false, ZKDCAPTestHelper.dummyIntelRootCACert(), address(new NopRiscZeroVerifier()) + ); + vm.warp(ZKDCAPTestHelper.TEST_TIMESTAMP); + bytes memory consensusStateBytes = LCPProtoMarshaler.marshal(defaultConsensusState()); + + { + // `current_tcb_evaluation_data_number` is not set + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.current_tcb_evaluation_data_number = 0; + bytes memory clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(ILCPClientErrors.LCPClientZKDCAPCurrentTcbEvaluationDataNumberNotSet.selector); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + } + { + // if `next_tcb_evaluation_data_number` is set, `next_tcb_evaluation_data_number_update_time` should be set + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.next_tcb_evaluation_data_number = 1; + clientState.next_tcb_evaluation_data_number_update_time = 0; + bytes memory clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(ILCPClientErrors.LCPClientZKDCAPInvalidNextTcbEvaluationDataNumberInfo.selector); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + } + { + // if `next_tcb_evaluation_data_number_update_time` is set, `next_tcb_evaluation_data_number` should be set + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.next_tcb_evaluation_data_number = 0; + clientState.next_tcb_evaluation_data_number_update_time = uint64(block.timestamp) + 1; + bytes memory clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(ILCPClientErrors.LCPClientZKDCAPInvalidNextTcbEvaluationDataNumberInfo.selector); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + } + { + // if `next_tcb_evaluation_data_number` is set, the value should not be equal to `current_tcb_evaluation_data_number` + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.current_tcb_evaluation_data_number = 1; + clientState.next_tcb_evaluation_data_number = 1; + clientState.next_tcb_evaluation_data_number_update_time = uint64(block.timestamp) + 1; + bytes memory clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(ILCPClientErrors.LCPClientZKDCAPInvalidNextTcbEvaluationDataNumberInfo.selector); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + } + { + // if `next_tcb_evaluation_data_number` is set, the value should be greater than `current_tcb_evaluation_data_number` + IbcLightclientsLcpV1ClientState.Data memory clientState = defaultClientState(); + clientState.current_tcb_evaluation_data_number = 2; + clientState.next_tcb_evaluation_data_number = 1; + clientState.next_tcb_evaluation_data_number_update_time = uint64(block.timestamp) + 1; + bytes memory clientStateBytes = LCPProtoMarshaler.marshal(clientState); + vm.expectRevert(ILCPClientErrors.LCPClientZKDCAPInvalidNextTcbEvaluationDataNumberInfo.selector); + lc.initializeClient(clientId, clientStateBytes, consensusStateBytes); + } + } + + // --- helper functions --- + + function registerEnclaveKeyMessage(DCAPValidator.Output memory output) + internal + returns (IbcLightclientsLcpV1ZKDCAPRegisterEnclaveKeyMessage.Data memory message) + { + message.zkvm_type = 0x01; + message.quote_verification_output = ZKDCAPTestHelper.toBytes(output); + message.proof = bytes(hex"00000000").concat(hex"01"); + return message; + } + + function defaultClientState() internal returns (IbcLightclientsLcpV1ClientState.Data memory clientState) { + clientState.mrenclave = abi.encodePacked(ZKDCAPTestHelper.TEST_MRENCLAVE); + clientState.key_expiration = 0; + clientState.current_tcb_evaluation_data_number = 1; + clientState.zkdcap_verifier_infos = + ZKDCAPTestHelper.buildRiscZeroVerifierInfos(ZKDCAPTestHelper.TEST_RISC_ZERO_IMAGE_ID); + return clientState; + } + + function defaultConsensusState() + internal + pure + returns (IbcLightclientsLcpV1ConsensusState.Data memory consensusState) + { + // The initial consensus state is empty + return consensusState; + } + + // Called by the client when registering an enclave key + function updateClientCommitments(string calldata clientId, Height.Data[] calldata heights) external {} +} + +library ZKDCAPTestHelper { + bytes32 constant TEST_INTEL_ROOT_CA_HASH = keccak256(abi.encodePacked("sgx intel root ca hash")); + bytes32 constant TEST_MRENCLAVE = keccak256(abi.encodePacked("mrenclave")); + bytes32 constant TEST_RISC_ZERO_IMAGE_ID = keccak256(abi.encodePacked("image_id")); + + uint64 constant TEST_TIMESTAMP = 1740842723; + + function qvOutput() internal pure returns (DCAPValidator.Output memory output) { + output.tcbStatus = DCAPValidator.TCB_STATUS_UP_TO_DATE_STRING; + output.minTcbEvaluationDataNumber = 1; + output.sgxIntelRootCAHash = ZKDCAPTestHelper.TEST_INTEL_ROOT_CA_HASH; + output.validityNotBefore = TEST_TIMESTAMP; + output.validityNotAfter = output.validityNotBefore + 30 days; + output.enclaveDebugEnabled = false; + output.mrenclave = ZKDCAPTestHelper.TEST_MRENCLAVE; + output.enclaveKey = address(1); + output.operator = address(0); + return output; + } + + function dummyIntelRootCACert() internal pure returns (bytes memory) { + return abi.encodePacked("sgx intel root ca hash"); + } + + function buildRiscZeroVerifierInfos(bytes32 imageId) internal returns (bytes[] memory infos) { + // The format is as follows: + // 0: zkVM type + // 1-N: arbitrary data for each zkVM type + // + // The format of the risc0 zkVM is as follows: + // | 0 | 1 - 31 | 32 - 64 | + // |---|----------|-----------| + // | 1 | reserved | image id | + bytes memory verifierInfo = new bytes(64); + verifierInfo[0] = 0x01; + assembly { + mstore(add(add(verifierInfo, 32), 32), imageId) + } + infos = new bytes[](1); + infos[0] = verifierInfo; + return infos; + } + + function toBytes(DCAPValidator.Output memory output) internal pure returns (bytes memory) { + bytes memory result = hex"0000"; // 0..2: version (2 bytes) + result = BytesLib.concat(result, hex"0003"); // 2..4: quote version (2 bytes) + result = BytesLib.concat(result, hex"00000000"); // 4..8: tee type (4 bytes) + + // 8: tcb status (1 byte) + result = BytesLib.concat(result, abi.encodePacked(_tcbStatusFromString(output.tcbStatus))); + + // 9..13: min tcb evaluation data number (4 bytes) + result = BytesLib.concat(result, abi.encodePacked(output.minTcbEvaluationDataNumber)); + + // 13..19: 6 bytes zero padding + result = BytesLib.concat(result, new bytes(6)); + + // 19..51: sgx intel root ca hash (32 bytes) + result = BytesLib.concat(result, abi.encodePacked(output.sgxIntelRootCAHash)); + + // 51..59: validity not before (8 bytes) + result = BytesLib.concat(result, abi.encodePacked(output.validityNotBefore)); + + // 59..67: validity not after (8 bytes) + result = BytesLib.concat(result, abi.encodePacked(output.validityNotAfter)); + + // 67..115: 48 bytes zero padding + result = BytesLib.concat(result, new bytes(48)); + + // 115: attributes (1 byte: 0x00 for enclave debug disabled, 0x02 for enclave debug enabled) + bytes1 attributesByte = output.enclaveDebugEnabled ? bytes1(0x02) : bytes1(0x00); + result = BytesLib.concat(result, abi.encodePacked(attributesByte)); + + // 116..131: 15 bytes zero padding + result = BytesLib.concat(result, new bytes(15)); + + // 131..163: mrenclave (32 bytes) + result = BytesLib.concat(result, abi.encodePacked(output.mrenclave)); + + // 163..387: 224 bytes zero padding + result = BytesLib.concat(result, new bytes(224)); + + // 387..451: report data (64 bytes) + // Format: 1 byte report data version (0x01) + // 20 bytes enclaveKey, 20 bytes operator, 23 bytes zero padding + result = BytesLib.concat( + result, abi.encodePacked(uint8(0x01), bytes20(output.enclaveKey), bytes20(output.operator), new bytes(23)) + ); + + // 451..end: advisory IDs + result = BytesLib.concat(result, abi.encode(output.advisoryIDs)); + + return result; + } + + function _tcbStatusFromString(string memory tcbStatus) internal pure returns (uint8) { + if (keccak256(bytes(tcbStatus)) == keccak256(bytes(DCAPValidator.TCB_STATUS_UP_TO_DATE_STRING))) { + return DCAPValidator.TCB_STATUS_UP_TO_DATE; + } else if (keccak256(bytes(tcbStatus)) == keccak256(bytes(DCAPValidator.TCB_STATUS_OUT_OF_DATE_STRING))) { + return DCAPValidator.TCB_STATUS_OUT_OF_DATE; + } else if (keccak256(bytes(tcbStatus)) == keccak256(bytes(DCAPValidator.TCB_STATUS_REVOKED_STRING))) { + return DCAPValidator.TCB_STATUS_REVOKED; + } else if ( + keccak256(bytes(tcbStatus)) == keccak256(bytes(DCAPValidator.TCB_STATUS_CONFIGURATION_NEEDED_STRING)) + ) { + return DCAPValidator.TCB_STATUS_CONFIGURATION_NEEDED; + } else if ( + keccak256(bytes(tcbStatus)) + == keccak256(bytes(DCAPValidator.TCB_STATUS_OUT_OF_DATE_CONFIGURATION_NEEDED_STRING)) + ) { + return DCAPValidator.TCB_STATUS_OUT_OF_DATE_CONFIGURATION_NEEDED; + } else if (keccak256(bytes(tcbStatus)) == keccak256(bytes(DCAPValidator.TCB_STATUS_SW_HARDENING_NEEDED_STRING))) + { + return DCAPValidator.TCB_STATUS_SW_HARDENING_NEEDED; + } else if ( + keccak256(bytes(tcbStatus)) + == keccak256(bytes(DCAPValidator.TCB_STATUS_CONFIGURATION_AND_SW_HARDENING_NEEDED_STRING)) + ) { + return DCAPValidator.TCB_STATUS_CONFIGURATION_AND_SW_HARDENING_NEEDED; + } else { + revert("unexpected TCB status"); + } + } +} + +contract NopRiscZeroVerifier is IRiscZeroVerifier { + function verify(bytes calldata seal, bytes32 imageId, bytes32) public view override { + require(seal.length == 5, "unexpected seal length"); + require(imageId == ZKDCAPTestHelper.TEST_RISC_ZERO_IMAGE_ID, "unexpected image id"); + } + + function verifyIntegrity(Receipt calldata) external view {} +} diff --git a/test/ReportTest.t.sol b/test/ReportTest.t.sol index 7fbbf46..8d84d72 100644 --- a/test/ReportTest.t.sol +++ b/test/ReportTest.t.sol @@ -2,8 +2,9 @@ pragma solidity ^0.8.12; import "forge-std/console.sol"; -import "../contracts/LCPClient.sol"; +import "../contracts/LCPClientIAS.sol"; import "../contracts/AVRValidator.sol"; +import "../contracts/RemoteAttestation.sol"; import "./TestHelper.t.sol"; contract ReportTest is BasicTest { @@ -14,7 +15,7 @@ contract ReportTest is BasicTest { address operator; } - AVRValidator.ReportAllowedStatus internal allowedStatuses; + RemoteAttestation.ReportAllowedStatus internal allowedStatuses; mapping(string => uint256) internal allowedAdvisoriesForValidateAdvisories; @@ -102,8 +103,8 @@ contract ReportTest is BasicTest { function testValidateAdvisories() public { uint256 offset; - allowedAdvisoriesForValidateAdvisories["INTEL-SA-00000"] = AVRValidator.FLAG_ALLOWED; - allowedAdvisoriesForValidateAdvisories["INTEL-SA-00001"] = AVRValidator.FLAG_ALLOWED; + allowedAdvisoriesForValidateAdvisories["INTEL-SA-00000"] = RemoteAttestation.FLAG_ALLOWED; + allowedAdvisoriesForValidateAdvisories["INTEL-SA-00001"] = RemoteAttestation.FLAG_ALLOWED; { offset = TestAVRValidator.validateAdvisories(bytes("[]"), 0, allowedAdvisoriesForValidateAdvisories); @@ -126,9 +127,9 @@ contract ReportTest is BasicTest { } function initAllowedStatusAdvisories(string memory quoteStatus, string[] memory advisories) internal { - allowedStatuses.allowedQuoteStatuses[quoteStatus] = AVRValidator.FLAG_ALLOWED; + allowedStatuses.allowedQuoteStatuses[quoteStatus] = RemoteAttestation.FLAG_ALLOWED; for (uint256 i = 0; i < advisories.length; i++) { - allowedStatuses.allowedAdvisories[advisories[i]] = AVRValidator.FLAG_ALLOWED; + allowedStatuses.allowedAdvisories[advisories[i]] = RemoteAttestation.FLAG_ALLOWED; } } diff --git a/test/ZKDCAPVerifier.t.sol b/test/ZKDCAPVerifier.t.sol new file mode 100644 index 0000000..4cdc6db --- /dev/null +++ b/test/ZKDCAPVerifier.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.12; + +import {BasicTest} from "./TestHelper.t.sol"; +import {IRiscZeroVerifier} from "risc0-ethereum/contracts/src/IRiscZeroVerifier.sol"; +import {RiscZeroGroth16Verifier} from "risc0-ethereum/contracts/src/groth16/RiscZeroGroth16Verifier.sol"; +import {DCAPValidator} from "../contracts/DCAPValidator.sol"; + +contract ZKDCAPVerifierTest is BasicTest { + IRiscZeroVerifier verifier_rz11; + IRiscZeroVerifier verifier_rz12; + + function setUp() public { + // ref. https://github.com/risc0/risc0-ethereum/blob/4fa7de055d461b7fa948eb56107b7a172459e8fc/contracts/src/groth16/ControlID.sol#L22 + verifier_rz11 = new RiscZeroGroth16Verifier( + hex"8b6dcf11d463ac455361b41fb3ed053febb817491bdea00fdb340e45013b852e", + hex"05a022e1db38457fb510bc347b30eb8f8cf3eda95587653d0eac19e1f10d164e" + ); + // ref. https://github.com/risc0/risc0-ethereum/blob/b9b22c396a0d5ef97bf02702da9415d5bb79a85a/contracts/src/groth16/ControlID.sol#L22 + verifier_rz12 = new RiscZeroGroth16Verifier( + hex"8cdad9242664be3112aba377c5425a4df735eb1c6966472b561d2855932c0469", + hex"04446e66d300eb7fb45c9726bb53c793dda407a62e9601618bb43c5c14657ac0" + ); + } + + /* + "zkp":{"Risc0":{ + "image_id":"200d1f40b5733d31e4f3bfb1f106351e878aea304b5c9e73690b9e18e2e77bb6", + "seal":"50bd1769039405f7898272862594bc436f08dab3e5c1200e0c44b542b462fe09bb2655b700ae0374f99c5b30f8580605267de7b5d121758ae405b5b378b588deca3042a70e22eefd598a4638d7c039ffb737f45dbee0152559f25c4dc0854ab7fd7cc2f52704dc9991d84e00c48d155b0d7fbfe2d307d1fb1a573c38083d86bea39e60191110517565779f631fc0028b82ff9b224bcc627bd6fd3ee1c1ffcea67e9e921c281abaaeacabff9fb74232c9ca2914ce6fe3ec9a4584c68a888339f2e6865c9f2bb57d5a38759c09ecb3f9f9f80fd582f23b7fb47495f783a8cc2eedb99ca18b0337102df4a2e325367d954ea5154f11c03f459551f849763d75ccad9d6665c5", + "commit":"00000003000000000500906ed50000a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d700900000000679885950000000067c00a9c15150b07ff800e000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000700000000000000813c146e403f203f2784fa222b3edeac70727dee21c0b08f74883aa189e7b0ed000000000000000000000000000000000000000000000000000000000000000083d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017dcf7408c72ebe9076aebbb208d2c85e62050db4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030333334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030363135000000000000000000000000000000000000" + }} + */ + function testVerify1() public view { + verifier_rz11.verify( + hex"50bd1769039405f7898272862594bc436f08dab3e5c1200e0c44b542b462fe09bb2655b700ae0374f99c5b30f8580605267de7b5d121758ae405b5b378b588deca3042a70e22eefd598a4638d7c039ffb737f45dbee0152559f25c4dc0854ab7fd7cc2f52704dc9991d84e00c48d155b0d7fbfe2d307d1fb1a573c38083d86bea39e60191110517565779f631fc0028b82ff9b224bcc627bd6fd3ee1c1ffcea67e9e921c281abaaeacabff9fb74232c9ca2914ce6fe3ec9a4584c68a888339f2e6865c9f2bb57d5a38759c09ecb3f9f9f80fd582f23b7fb47495f783a8cc2eedb99ca18b0337102df4a2e325367d954ea5154f11c03f459551f849763d75ccad9d6665c5", + hex"200d1f40b5733d31e4f3bfb1f106351e878aea304b5c9e73690b9e18e2e77bb6", + sha256( + hex"00000003000000000500906ed50000a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d700900000000679885950000000067c00a9c15150b07ff800e000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000700000000000000813c146e403f203f2784fa222b3edeac70727dee21c0b08f74883aa189e7b0ed000000000000000000000000000000000000000000000000000000000000000083d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017dcf7408c72ebe9076aebbb208d2c85e62050db4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030333334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030363135000000000000000000000000000000000000" + ) + ); + } + + /* + { + "Risc0":{ + "image_id":"a7b6d5b9a35f9c4e9364e69728c87a4818fe56b361b5f5ff25bab7494074b50b", + "seal":"c101b42b2178a72a844649d14328eb6b7a650d6fe1640c1ef2fa887bcc755597229713fb2360369704b1c06c610e6a83dbdc31bb04e1cb5a8def9c7354fc34be7e74844301db3e4f8c2cef2a91df93e191e3138ac27dac03d1a2d2bc2e18330f712e65021d34cf95c2ff4134df2a04a726de5a8d8e535f10225f00046295f8b3d3b939a626e424d04c48c94aef79267457e375ce4670ef5eb5af2c3e8fea40c4905228e617130a7a0c1ac16ed030103b02f94c89f6f5935d8a581198df573c050ea63e331b6c494238c02ff85040e720621dd81fa6d1191098cf40ac05923398e58b213c197e62e6a4394b5b5134f9770ba31805e57c2c8635e921472936bab23a56c824", + "commit":"00000003000000000500906ed50000a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d700900000000679ec00c0000000067c6487615150b07ff800e00000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000070000000000000026ae825ffce1cf9dcdf682614f4d36704e7bca087bbb5264aca9301d7824cec8000000000000000000000000000000000000000000000000000000000000000083d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d81e4baf174a00844eea767972c7433e19ff7c28000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030333334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030363135000000000000000000000000000000000000" + } + } + */ + function testVerify2() public view { + verifier_rz12.verify( + hex"c101b42b2178a72a844649d14328eb6b7a650d6fe1640c1ef2fa887bcc755597229713fb2360369704b1c06c610e6a83dbdc31bb04e1cb5a8def9c7354fc34be7e74844301db3e4f8c2cef2a91df93e191e3138ac27dac03d1a2d2bc2e18330f712e65021d34cf95c2ff4134df2a04a726de5a8d8e535f10225f00046295f8b3d3b939a626e424d04c48c94aef79267457e375ce4670ef5eb5af2c3e8fea40c4905228e617130a7a0c1ac16ed030103b02f94c89f6f5935d8a581198df573c050ea63e331b6c494238c02ff85040e720621dd81fa6d1191098cf40ac05923398e58b213c197e62e6a4394b5b5134f9770ba31805e57c2c8635e921472936bab23a56c824", + hex"a7b6d5b9a35f9c4e9364e69728c87a4818fe56b361b5f5ff25bab7494074b50b", + sha256( + hex"00000003000000000500906ed50000a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d700900000000679ec00c0000000067c6487615150b07ff800e00000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000070000000000000026ae825ffce1cf9dcdf682614f4d36704e7bca087bbb5264aca9301d7824cec8000000000000000000000000000000000000000000000000000000000000000083d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d81e4baf174a00844eea767972c7433e19ff7c28000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030333334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030363135000000000000000000000000000000000000" + ) + ); + } + + function testParseOutputSWHardeningNeeded() public pure { + bytes memory outputBytes = + hex"0000000300000000050000001200906ed50000a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d70090000000067b3f1fa0000000067db736115150b07ff800e00000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000070000000000000026ae825ffce1cf9dcdf682614f4d36704e7bca087bbb5264aca9301d7824cec8000000000000000000000000000000000000000000000000000000000000000083d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c170f98628b3a01b15654fbfaad1aaf3419b2c3c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030333334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030363135000000000000000000000000000000000000"; + DCAPValidator.Output memory output = DCAPValidatorTestHelper.parseOutput(outputBytes); + assertEq( + output.sgxIntelRootCAHash, bytes32(hex"a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d7009") + ); + assertEq(output.tcbStatus, DCAPValidator.TCB_STATUS_SW_HARDENING_NEEDED_STRING); + assertFalse(output.enclaveDebugEnabled); + assertEq(output.mrenclave, bytes32(hex"26ae825ffce1cf9dcdf682614f4d36704e7bca087bbb5264aca9301d7824cec8")); + assertEq(output.enclaveKey, address(bytes20(hex"c170f98628b3a01b15654fbfaad1aaf3419b2c3c"))); + assertEq(output.operator, address(0)); + assertEq(output.advisoryIDs.length, 2); + assertEq(output.advisoryIDs[0], "INTEL-SA-00334"); + assertEq(output.advisoryIDs[1], "INTEL-SA-00615"); + } + + function testParseOutputSimulationUpToDate() public pure { + bytes memory outputBytes = + hex"0000000300000000000000000000606a000000d61f4e3d30011899d16131d4c940ef1f75ec53d7f9a70cbb3aab1f5ab0235b2b000000000000000100000000ffffffff4820f3376ae6b2f2034d3b7a4b48a7780000000000000000000000000000000000000000000000000000000000000000070000000000000007000000000000003a354bf808b89267b19c6b390ee484d1bee8d301d0058fac511a900d5d0a6f68000000000000000000000000000000000000000000000000000000000000000083d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000019c69756b02dd84ad5d7a11758025ae4a7edf938d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + DCAPValidator.Output memory output = DCAPValidatorTestHelper.parseOutput(outputBytes); + assertEq( + output.sgxIntelRootCAHash, bytes32(hex"d61f4e3d30011899d16131d4c940ef1f75ec53d7f9a70cbb3aab1f5ab0235b2b") + ); + assertEq(output.tcbStatus, DCAPValidator.TCB_STATUS_UP_TO_DATE_STRING); + assertTrue(output.enclaveDebugEnabled); + assertEq(output.mrenclave, bytes32(hex"3a354bf808b89267b19c6b390ee484d1bee8d301d0058fac511a900d5d0a6f68")); + assertEq(output.enclaveKey, address(bytes20(hex"9c69756b02dd84ad5d7a11758025ae4a7edf938d"))); + assertEq(output.operator, address(0)); + assertEq(output.advisoryIDs.length, 0); + } + + function testParseCommitEnclaveDebugEnabled() public pure { + bytes memory outputBytes = + hex"0000000300000000050000001200906ed50000a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d70090000000067b42fdd0000000067dbb9ea15150b07ff800e00000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000070000000000000026ae825ffce1cf9dcdf682614f4d36704e7bca087bbb5264aca9301d7824cec8000000000000000000000000000000000000000000000000000000000000000083d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001988143c0a5645a38c7900b3102859d136f3bcc2c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030333334000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e494e54454c2d53412d3030363135000000000000000000000000000000000000"; + DCAPValidator.Output memory output = DCAPValidatorTestHelper.parseOutput(outputBytes); + assertEq( + output.sgxIntelRootCAHash, bytes32(hex"a1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d7009") + ); + assertEq(output.tcbStatus, DCAPValidator.TCB_STATUS_SW_HARDENING_NEEDED_STRING); + assertTrue(output.enclaveDebugEnabled); + assertEq(output.mrenclave, bytes32(hex"26ae825ffce1cf9dcdf682614f4d36704e7bca087bbb5264aca9301d7824cec8")); + assertEq(output.enclaveKey, address(bytes20(hex"988143c0a5645a38c7900b3102859d136f3bcc2c"))); + assertEq(output.operator, address(0)); + assertEq(output.advisoryIDs.length, 2); + assertEq(output.advisoryIDs[0], "INTEL-SA-00334"); + assertEq(output.advisoryIDs[1], "INTEL-SA-00615"); + } +} + +library DCAPValidatorTestHelper { + function parseOutput(bytes calldata outputBytes) public pure returns (DCAPValidator.Output memory) { + return DCAPValidator.parseOutput(outputBytes); + } +}