Skip to content

Commit

Permalink
Add nitro validator (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
chunter-cb authored Oct 22, 2024
1 parent 76642b3 commit cfd4951
Show file tree
Hide file tree
Showing 20 changed files with 400 additions and 10 deletions.
11 changes: 11 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@
[submodule "lib/optimism"]
path = lib/optimism
url = https://github.com/ethereum-optimism/optimism

# Nitro Validator
[submodule "nitro-validator/lib/NitroProver"]
path = nitro-validator/lib/NitroProver
url = https://github.com/marlinprotocol/NitroProver
[submodule "nitro-validator/lib/solidity-cbor"]
path = nitro-validator/lib/solidity-cbor
url = https://github.com/marlinprotocol/solidity-cbor
[submodule "nitro-validator/lib/forge-std"]
path = nitro-validator/lib/forge-std
url = https://github.com/foundry-rs/forge-std
5 changes: 4 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ evm_version = "cancun"

fs_permissions = [
{ access='read', path='./deploy-config/' },
{ access='read-write', path='./deployments/' }
{ access='read-write', path='./deployments/' },
{ access='read', path='./nitro-validator/' }
]

exclude = ['nitro-validator/**']
8 changes: 8 additions & 0 deletions nitro-validator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/.idea/
.DS_Store
/out/
/cache/
/testnet/data/
/testnet/.env
/deployments/*-*-*.json
/bin/
45 changes: 45 additions & 0 deletions nitro-validator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Nitro Validator

This directory is used to build the nitro validator. It currently needs both solidity ^0.8.24 and to be compiled with via-ir and therefore cant be in the same src directory.

This should be a singleton deployed on the L1.


### Build

```shell
$ forge build
```

### Test

```shell
$ forge test
```

### Format

```shell
$ forge fmt
```

### Deploy

```shell
$ forge script script/DeployNitroValidator.s.sol:DeployNitroValidator --rpc-url <your_rpc_url>
```


### Cast

```shell
$ cast <subcommand>
```

### Help

```shell
$ forge --help
$ anvil --help
$ cast --help
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"address": "0x20823F43A70eEE2F92A6f44cC378566C6c7d4b0E"}
13 changes: 13 additions & 0 deletions nitro-validator/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

fs_permissions = [
{ access = "read", path = "./test/nitro-attestation"},
{ access='read-write', path='./deployments/' },
{ access = "read-write", path = "./nitro-validator/deployments/" }
]


# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions nitro-validator/lib/NitroProver
Submodule NitroProver added at 8923d3
1 change: 1 addition & 0 deletions nitro-validator/lib/forge-std
Submodule forge-std added at 1de6ee
1 change: 1 addition & 0 deletions nitro-validator/lib/solidity-cbor
Submodule solidity-cbor added at 6b8683
3 changes: 3 additions & 0 deletions nitro-validator/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@marlinprotocol/=lib/NitroProver/src/
@solidity-cbor/=lib/solidity-cbor/packages/solidity-cbor/contracts/
forge-std/=lib/forge-std/src/
29 changes: 29 additions & 0 deletions nitro-validator/script/DeployNitroValidator.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Script} from "forge-std/Script.sol";
import {console2 as console} from "forge-std/console2.sol";
import {NitroValidator} from "src/NitroValidator.sol";

/// @notice will deploy the singleton NitroValidatorContract to a deterministic address
contract DeployNitroValidator is Script {
bytes32 constant SALT = bytes32(uint256(0x4E313752304F5)); // todo

function run() public returns (address addr_) {
vm.startBroadcast();

addr_ = address(new NitroValidator{salt: SALT}());

console.log("NitroValidator to be deployed at:", addr_);

// Save the address to the deployment file
string memory deploymentJson = string.concat("{", '"address": "', vm.toString(addr_), '"}');

vm.writeFile(
string.concat(vm.projectRoot(), "/deployments/", vm.toString(block.chainid), "-nitro-validator-deploy.json"),
deploymentJson
);

vm.stopBroadcast();
}
}
13 changes: 13 additions & 0 deletions nitro-validator/src/INitroValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

interface INitroValidator {
/// @notice Verifies an AWS Nitro attestation
/// @param attestation The attestation document
/// @param maxAge Maximum age of the attestation in seconds
/// @return enclavePubKey The enclave's public key
/// @return pcr0 User data included in the attestation
function validateAttestation(bytes memory attestation, uint256 maxAge)
external view
returns (bytes memory enclavePubKey, bytes memory pcr0);
}
160 changes: 160 additions & 0 deletions nitro-validator/src/NitroValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {CBORDecoding} from "@solidity-cbor/CBORDecoding.sol";
import {CBOR} from "@solidity-cbor/CBOREncoding.sol";
import {ByteParser} from "@solidity-cbor/ByteParser.sol";
import {INitroValidator} from "./INitroValidator.sol";
import {NitroProver} from "@marlinprotocol/NitroProver.sol";

contract NitroValidator is NitroProver, INitroValidator {
/// @notice based off of NitroProver's verifyAttestation. Will validate an attestation and return the public key and PRC0 used
function validateAttestation(bytes memory attestation, uint256 maxAge)
external view
returns (bytes memory, bytes memory)
{
/*
https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md#31-cose-and-cbor
Attestation document is an array of 4 elements
[
protected: Header,
unprotected: Header,
payload: This field contains the serialized content to be signed,
signature: This field contains the computed signature value.
]
*/

// GAS: Attestation decode gas ~62k
bytes[] memory attestationDecoded = CBORDecoding.decodeArray(attestation);

// TODO: confirm that the attestation is untagged CBOR structure
// https://datatracker.ietf.org/doc/html/rfc8152#section-3.1
// Protected header for COSE_Sign1
bytes[2][] memory protectedHeader = CBORDecoding.decodeMapping(attestationDecoded[0]);
// Protected header should have algorithm flag which is specified by 1
require(ByteParser.bytesToUint64(protectedHeader[0][0]) == 1, "Not algo flag");
// Algorithm should be ECDSA w/ SHA-384
require(ByteParser.bytesToNegativeInt128(protectedHeader[0][1]) == -35, "Incorrect algorithm");
// Protected header should just have sig algo flag
require(protectedHeader.length == 1, "Only algo flag should be present");

// Unprotected header for COSE_Sign1
bytes[2][] memory unprotectedHeader = CBORDecoding.decodeMapping(attestationDecoded[1]);
// Unprotected header should be empty
require(unprotectedHeader.length == 0, "Unprotected header should be empty");

bytes memory payload = attestationDecoded[2];
(bytes memory certPubKey, bytes memory enclavePubKey, bytes memory pcr0) =
_getAttestationDocKeysAndPCR0(payload, maxAge);

// verify COSE signature as per https://www.rfc-editor.org/rfc/rfc9052.html#section-4.4
bytes memory attestationSig = attestationDecoded[3];

// create COSE structure
// GAS: COSE structure creation gas ~42.7k
// TODO: set CBOR length appropriately
CBOR.CBORBuffer memory buf = CBOR.create(payload.length * 2);
CBOR.startFixedArray(buf, 4);
// context to be written as Signature1 as COSE_Sign1 is used https://www.rfc-editor.org/rfc/rfc9052.html#section-4.4-2.1.1
CBOR.writeString(buf, "Signature1");
// Protected headers to be added https://www.rfc-editor.org/rfc/rfc9052.html#section-4.4-2.2
CBOR.writeBytes(buf, attestationDecoded[0]);
// externally supplied data is empty https://www.rfc-editor.org/rfc/rfc9052.html#section-4.4-2.4
CBOR.writeBytes(buf, "");
// Payload to be added https://www.rfc-editor.org/rfc/rfc9052.html#section-4.4-2.5
CBOR.writeBytes(buf, payload);

_processSignature(attestationSig, certPubKey, buf.buf.buf);
return (enclavePubKey, pcr0);
}

/// @notice validates the attestation payload and returns the used public key, enclave public key and pcr0 used
/// @return certPubKey certificate public key
/// @return enclavePubKey enclave public key
/// @return pcr0 platform configuration register 0
function _getAttestationDocKeysAndPCR0(bytes memory attestationPayload, uint256 maxAge)
internal
view
returns (bytes memory certPubKey, bytes memory enclavePubKey, bytes memory pcr0)
{
// TODO: validate if this check is expected? https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md?plain=1#L168
require(attestationPayload.length <= 2 ** 15, "Attestation too long");

// validations as per https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md#32-syntactical-validation
// issuing Nitro hypervisor module ID
// GAS: decoding takes ~173.5k gas
bytes[2][] memory attestationStructure = CBORDecoding.decodeMapping(attestationPayload);
bytes memory moduleId;
bytes memory rawTimestamp;
bytes memory digest;
bytes memory rawPcrs;
bytes memory certificate;
bytes memory cabundle;
bytes memory userData;

for (uint256 i = 0; i < attestationStructure.length; i++) {
bytes32 keyHash = keccak256(attestationStructure[i][0]);
if (keyHash == keccak256(bytes("module_id"))) {
moduleId = attestationStructure[i][1];
continue;
}
if (keyHash == keccak256(bytes("timestamp"))) {
rawTimestamp = attestationStructure[i][1];
continue;
}
if (keyHash == keccak256(bytes("digest"))) {
digest = attestationStructure[i][1];
continue;
}
if (keyHash == keccak256(bytes("pcrs"))) {
rawPcrs = attestationStructure[i][1];
continue;
}
if (keyHash == keccak256(bytes("certificate"))) {
certificate = attestationStructure[i][1];
continue;
}
if (keyHash == keccak256(bytes("cabundle"))) {
cabundle = attestationStructure[i][1];
continue;
}
if (keyHash == keccak256(bytes("public_key"))) {
enclavePubKey = attestationStructure[i][1];
continue;
}
if (keyHash == keccak256(bytes("user_data"))) {
userData = attestationStructure[i][1];
continue;
}
}

require(moduleId.length != 0, "Invalid module id");

uint64 timestamp = ByteParser.bytesToUint64(rawTimestamp);
require(timestamp != 0, "invalid timestamp");
require(timestamp + maxAge > block.timestamp, "attestation too old");

require(bytes32(digest) == bytes32("SHA384"), "invalid digest algo");

bytes[2][] memory pcrs = CBORDecoding.decodeMapping(rawPcrs);
pcr0 = _getPCR0(pcrs);

certPubKey = _verifyCerts(certificate, cabundle);

return (certPubKey, enclavePubKey, pcr0);
}

/// @notice PCR0 should be first in the array but this ensures it will find it otherwise
function _getPCR0(bytes[2][] memory pcrs) internal pure returns (bytes memory) {
require(pcrs.length != 0, "no pcr specified");
require(pcrs.length <= 32, "only 32 pcrs allowed");

for (uint256 i = 0; i < pcrs.length; i++) {
if (uint8(bytes1(pcrs[i][0])) == uint8(0)) {
return pcrs[i][1];
}
}

revert("failed to find pcr0");
}
}
34 changes: 34 additions & 0 deletions nitro-validator/test/NitroValidator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import {Test, console} from "forge-std/Test.sol";
import "../src/INitroValidator.sol";
import "../src/NitroValidator.sol";

contract NitroValidatorTest is Test {
INitroValidator validator;

function setUp() public {
vm.warp(1708930774);
validator = new NitroValidator();
}

function test_validateAttestation() public {
bytes memory attestation = vm.readFileBinary("./test/nitro-attestation/sample_attestation.bin");

(bytes memory enclavePubKey, bytes memory pcr0) = validator.validateAttestation(attestation, 365 days);

assertEq(enclavePubKey, hex"d239fd059dd0e0a01e280bec44903bb8143bae7e578b9844c6df5fd6351eddc0");
assertEq(pcr0, hex"17BF8F048519797BE90497001A7559A3D555395937117D76F8BAAEDF56CA6D97952DE79479BC0C76E5D176D20F663790");
}

function test_validateAttestation_RevertOnExpiredTime() public {
bytes memory attestation = vm.readFileBinary("./test/nitro-attestation/sample_attestation.bin");

// Warp time to 366 days in the future
vm.warp(block.timestamp + 366 days);

vm.expectRevert("certificate not valid anymore");
validator.validateAttestation(attestation, 365 days);
}
}
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@lib-keccak/=lib/optimism/packages/contracts-bedrock/lib/lib-keccak/contracts/lib/
@openzeppelin/contracts-upgradeable/=lib/optimism/packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable/contracts/
@rari-capital/solmate/=lib/optimism/packages/contracts-bedrock/lib/solmate/
@nitro-validator/=nitro-validator/
src/cannon/interfaces/=lib/optimism/packages/contracts-bedrock/src/cannon/interfaces/
src/L1/=lib/optimism/packages/contracts-bedrock/src/L1/
src/L2/=lib/optimism/packages/contracts-bedrock/src/L2/
Expand Down
16 changes: 15 additions & 1 deletion script/DeploySystem.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { DeployChain } from "src/DeployChain.sol";
import { Constants } from "@eth-optimism-bedrock/src/libraries/Constants.sol";
import { ResourceMetering } from "@eth-optimism-bedrock/src/L1/ResourceMetering.sol";
import { IResourceMetering } from "@eth-optimism-bedrock/src/L1/interfaces/IResourceMetering.sol";
import "../nitro-validator/src/INitroValidator.sol";

import { console2 as console } from "forge-std/console2.sol";

Expand Down Expand Up @@ -134,8 +135,21 @@ contract DeploySystem is Deploy {
}

function deploySystemConfigGlobal() public broadcast returns (address addr_) {
string memory filePath = string(abi.encodePacked(
"nitro-validator/deployments/",
vm.toString(block.chainid),
"-nitro-validator-deploy.json"
));

if (!vm.exists(filePath)) {
revert("NitroValidator.json not found. Please deploy nitro-validator first.");
}

address nitroValidatorAddress = vm.parseJsonAddress(filePath, ".address");
INitroValidator nitroValidator = INitroValidator(nitroValidatorAddress);

console.log("Deploying SystemConfigGlobal implementation");
addr_ = address(new SystemConfigGlobal{ salt: _implSalt() }());
addr_ = address(new SystemConfigGlobal{ salt: _implSalt() }(nitroValidator));
save("SystemConfigGlobal", addr_);
console.log("SystemConfigGlobal deployed at %s", addr_);
}
Expand Down
Loading

0 comments on commit cfd4951

Please sign in to comment.