Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seed recovery process to survive kettle restarts #38

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion scripts/bootstrap_kettle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function main() {
console.log("Key manager already bootstrapped with "+keyManagerPub);
return
}

// 1st. Bootstrap the key manager
await kettle_advance(kettle);
const bootstrapTxData = await KM.offchain_Bootstrap.populateTransaction();
let resp = await kettle_execute(kettle, bootstrapTxData.to, bootstrapTxData.data);
Expand All @@ -37,6 +37,37 @@ async function main() {

const onchainBootstrapTx = await (await KM.onchain_Bootstrap(offchainBootstrapResult._xPub, offchainBootstrapResult.att)).wait();
console.log("bootstrapped "+offchainBootstrapResult._xPub+" in "+onchainBootstrapTx.hash);

// 2nd. Register the key manager
await kettle_advance(kettle);
const registerTxData = await KM.offchain_Register.populateTransaction();
resp = await kettle_execute(kettle, registerTxData.to, registerTxData.data);

const registerResult = JSON.parse(resp);
if (registerResult.Success === undefined) {
throw("registration did not succeed: "+JSON.stringify(resp));
}

const offchainRegisterResult = KM.interface.decodeFunctionResult(KM.offchain_Register.fragment, registerResult.Success.output.Call).toObject();

const onchainRegisterTx = await (await KM.onchain_Register(offchainRegisterResult.addr, offchainRegisterResult.myPub, offchainRegisterResult.att)).wait();
console.log("registered "+offchainRegisterResult.addr+" with the pubkey "+offchainRegisterResult.myPub+" in "+onchainRegisterTx.hash);

// 3rd onboard the key manager
await kettle_advance(kettle);
const onboardTxData = await KM.offchain_Onboard.populateTransaction(offchainRegisterResult.addr);
resp = await kettle_execute(kettle, onboardTxData.to, onboardTxData.data);

const onboardResult = JSON.parse(resp);
if (onboardResult.Success === undefined) {
throw("onboarding did not succeed: "+JSON.stringify(resp));
}

const offchainOnboardResult = KM.interface.decodeFunctionResult(KM.offchain_Onboard.fragment, onboardResult.Success.output.Call).toObject();

const onchainOnboardTx = await (await KM.onchain_Onboard(offchainRegisterResult.addr, offchainOnboardResult.ciphertext)).wait();
console.log("onboarded "+offchainRegisterResult.addr+" in "+onchainOnboardTx.hash);

}

main().catch((error) => {
Expand Down
1 change: 0 additions & 1 deletion scripts/test_examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ async function deploy() {

const SealedAuction = await deploy_artifact_direct(LocalConfig.SEALED_AUCTION_ARTIFACT, wallet, KM.target, 5);
await kettle_advance(kettle);

await derive_key(await SealedAuction.getAddress(), kettle, KM);
await testSA(SealedAuction, kettle);
}
Expand Down
5 changes: 2 additions & 3 deletions src/Andromeda.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ contract Andromeda is IAndromeda, DcapDemo {
require(success);
}

function volatileGet(bytes32 key) external override returns (bytes32) {
function volatileGet(bytes32 key) public view returns (bytes memory) {
(bool success, bytes memory value) = VOLATILEGET_ADDR.staticcall(abi.encodePacked((key)));
require(success);
require(value.length == 32);
return abi.decode(value, (bytes32));
return abi.decode(value, (bytes));
}

function attestSgx(bytes32 appData) external override returns (bytes memory) {
Expand Down
6 changes: 3 additions & 3 deletions src/AndromedaForge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {IAndromeda} from "src/IAndromeda.sol";
interface Vm {
function ffi(string[] calldata commandInput) external view returns (bytes memory result);
function setEnv(string calldata name, string calldata value) external;
function envOr(string calldata key, bytes32 defaultValue) external returns (bytes32 value);
function envOr(string calldata key, bytes memory defaultValue) external returns (bytes memory value);
}

contract AndromedaForge is IAndromeda {
Expand Down Expand Up @@ -57,10 +57,10 @@ contract AndromedaForge is IAndromeda {
vm.setEnv(env, iToHex(abi.encodePacked(value)));
}

function volatileGet(bytes32 tag) public returns (bytes32) {
function volatileGet(bytes32 tag) public returns (bytes memory) {
address caller = msg.sender;
string memory env = toEnv(activeHost, caller, tag);
return vm.envOr(env, bytes32(""));
return vm.envOr(env, bytes(""));
}

// Currently active host
Expand Down
6 changes: 3 additions & 3 deletions src/AndromedaRemote.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {V3Parser} from "automata-dcap-v3-attestation/lib/QuoteV3Auth/V3Parser.so
interface Vm {
function ffi(string[] calldata commandInput) external view returns (bytes memory result);
function setEnv(string calldata name, string calldata value) external;
function envOr(string calldata key, bytes32 defaultValue) external returns (bytes32 value);
function envOr(string calldata key, bytes memory defaultValue) external returns (bytes memory value);
function readFile(string calldata path) external view returns (string memory data);
function prank(address caller) external;
function parseJson(string memory json, string memory key) external view;
Expand Down Expand Up @@ -139,10 +139,10 @@ contract AndromedaRemote is IAndromeda, DcapDemo {
vm.setEnv(env, iToHex(abi.encodePacked(value)));
}

function volatileGet(bytes32 tag) public returns (bytes32) {
function volatileGet(bytes32 tag) public returns (bytes memory) {
address caller = msg.sender;
string memory env = toEnv(activeHost, caller, tag);
return vm.envOr(env, bytes32(""));
return vm.envOr(env, bytes(""));
}

function doHTTPRequest(IAndromeda.HttpRequest memory) external pure returns (bytes memory) {
Expand Down
2 changes: 1 addition & 1 deletion src/IAndromeda.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface IAndromeda is IHash {
function attestSgx(bytes32 appData) external returns (bytes memory);
function verifySgx(address caller, bytes32 appData, bytes memory att) external view returns (bool);
function volatileSet(bytes32 tag, bytes32 value) external;
function volatileGet(bytes32 tag) external returns (bytes32);
function volatileGet(bytes32 tag) external returns (bytes memory);
function localRandom() external view returns (bytes32);
function sealingKey(bytes32 tag) external view returns (bytes32);

Expand Down
50 changes: 45 additions & 5 deletions src/KeyManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,30 @@ contract KeyManager_v0 is KeyManagerBase {
return _derivedPriv(msg.sender);
}

function getSeed() private returns (bytes32) {
return Suave.volatileGet("seed");
function getSeed() internal returns (bytes32) {
bytes memory seed = Suave.volatileGet("seed");
// check if a seed was lost due a kettle restart and restore it
if(seed.length > 0) {
require(seed.length == 32, "Seed length is not 32 bytes");
return bytes32(seed);
}
return recoverSeed();
}

function setSeed(bytes32 seed) private {
function recoverSeed() internal returns (bytes32) {
bytes32 myPriv = Suave.sealingKey("myPriv");
address addr = address(Secp256k1.deriveAddress(uint256(myPriv)));
// make sure that the kettle was onboarded
require(keccak256(ciphertexts[addr]) != keccak256(bytes("")));
/* TODO: followup: discover ways to restore the seed in case the sealing key is revoked or rotated
In this case, a need seed should be generated and everything that was encrypted or keys derived from the old seed should be re-encrypted or re-derived
Problem: the decryption here would fail and the execution will be interrupted due to wrong(new) private key */
bytes32 seed = abi.decode(PKE.decrypt(myPriv, ciphertexts[addr]), (bytes32));
setSeed(seed);
return seed;
}

function setSeed(bytes32 seed) internal {
Suave.volatileSet("seed", seed);
}

Expand Down Expand Up @@ -122,13 +141,14 @@ contract KeyManager_v0 is KeyManagerBase {
// 2. New node register phase
// Mapping to nonzero indicates valid Kettle
mapping(address => bytes) registry;
// Mapping kettles to their ciphertexts
mapping(address => bytes) public ciphertexts;

function offchain_Register() public returns (address addr, bytes memory myPub, bytes memory att) {
require(keccak256(registry[addr]) == keccak256(bytes("")));

bytes32 myPriv = Suave.sealingKey("myPriv");
myPub = PKE.derivePubKey(myPriv);
addr = address(Secp256k1.deriveAddress(uint256(myPriv)));
require(keccak256(registry[addr]) == keccak256(bytes("")));
att = Suave.attestSgx(keccak256(abi.encodePacked("myPub", myPub, addr)));
return (addr, myPub, att);
}
Expand All @@ -150,6 +170,7 @@ contract KeyManager_v0 is KeyManagerBase {

function onchain_Onboard(address addr, bytes memory ciphertext) public {
// Note: nothing guarantees all ciphertexts on chain are valid
ciphertexts[addr] = ciphertext;
emit Onboard(addr, ciphertext);
}

Expand All @@ -161,3 +182,22 @@ contract KeyManager_v0 is KeyManagerBase {
setSeed(seed);
}
}

/* This contract is used for testing purposes and should never be used in production
It is used to expose the seed and restart and recovery functions for testing purposes
*/
contract TestRecoverableKeyManagerWrapper is KeyManager_v0 {
constructor(address _Suave) KeyManager_v0(_Suave) {}

function getRecoveredSeed() public returns (bytes32) {
return super.recoverSeed();
}

function getCurrentSeed() public returns (bytes32) {
return bytes32(Suave.volatileGet("seed"));
}

function restartKettle() public {
super.setSeed(bytes32(0));
}
}
5 changes: 4 additions & 1 deletion src/examples/SpeedrunAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ contract KeyManager {

// Private key will only be accessible in confidential mode
function xPriv() internal returns (bytes32) {
return Suave.volatileGet("xPriv");
bytes memory _xPriv = Suave.volatileGet("xPriv");
require(_xPriv.length == 32, "Seed length is not 32 bytes");
return bytes32(_xPriv);

}

// To initialize the key, some kettle must call this...
Expand Down
6 changes: 3 additions & 3 deletions test/AndromedaForge.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ contract AndromedaForgeTest is Test {
bytes32 value = keccak256(abi.encodePacked("hi"));

// Initially it is 0
bytes32 value2 = andromeda.volatileGet(bytes32("test"));
bytes32 value2 = bytes32(andromeda.volatileGet(bytes32("test")));
assertEq(value2, "");

// After setting it is hash("hi")
andromeda.volatileSet(bytes32("test"), value);
bytes32 value3 = andromeda.volatileGet(bytes32("test"));
bytes32 value3 = bytes32(andromeda.volatileGet(bytes32("test")));
assertEq(value3, value);

// Setting again overwrites
bytes32 v2 = keccak256("asdf");
andromeda.volatileSet(bytes32("test"), v2);
bytes32 v2check = andromeda.volatileGet(bytes32("test"));
bytes32 v2check = bytes32(andromeda.volatileGet(bytes32("test")));
assertEq(v2check, v2);
}
}
33 changes: 32 additions & 1 deletion test/KeyManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import {KeyManager_v0} from "../src/KeyManager.sol";
import {KeyManager_v0, TestRecoverableKeyManagerWrapper} from "../src/KeyManager.sol";
import {PKE} from "../src/crypto/encryption.sol";
import {AndromedaForge} from "src/AndromedaForge.sol";
import "forge-std/Vm.sol";

contract KeyManager_v0_Test is Test {
AndromedaForge andromeda;
KeyManager_v0 keymgr;
TestRecoverableKeyManagerWrapper rckeymgr;

Vm.Wallet alice;
Vm.Wallet bob;
Expand All @@ -19,6 +20,7 @@ contract KeyManager_v0_Test is Test {
andromeda = new AndromedaForge();
vm.prank(vm.addr(uint(keccak256("KeyManager.t.sol"))));
keymgr = new KeyManager_v0(address(andromeda));
rckeymgr = new TestRecoverableKeyManagerWrapper(address(andromeda));

alice = vm.createWallet("alice");
bob = vm.createWallet("bob");
Expand Down Expand Up @@ -83,4 +85,33 @@ contract KeyManager_v0_Test is Test {
bytes32 dPriv = keymgr.derivedPriv();
assertEq(PKE.derivePubKey(dPriv), keymgr.derivedPub(address(this)));
}

function testSeedRecovery() public {
andromeda.switchHost("carol");
// Do the bootstrap
(address xPub, bytes memory att) = rckeymgr.offchain_Bootstrap();
rckeymgr.onchain_Bootstrap(xPub, att);

// do the registration
(address my_kettle, bytes memory myPub, bytes memory myAtt) = rckeymgr
.offchain_Register();
rckeymgr.onchain_Register(my_kettle, myPub, myAtt);

// do the onboarding
bytes memory ciphertext = rckeymgr.offchain_Onboard(my_kettle);
rckeymgr.onchain_Onboard(my_kettle, ciphertext);

// get the seed
bytes32 seed = rckeymgr.getCurrentSeed();

// simulate a new kettle restart by setting the seed to 0
rckeymgr.restartKettle();
bytes32 resettedSeed = rckeymgr.getCurrentSeed();
assertEq(resettedSeed, bytes32(0));

// recover the seed
bytes32 recoveredSeed = rckeymgr.getRecoveredSeed();

assertEq(seed, recoveredSeed);
}
}