LCP Client
LCP Clientは、on-chainでELCにより生成されたcommitmentを検証するICS-02を実装するLight Clientである。LCPが生成するVerifiable Quoteを検証し、そこに含まれるEnclave Keyを取り出し保持する。最初はVerifiable Quoteとして、IASのAVRをサポートする。
Clientは状態をClientStateとConsensusStateで表現する。各定義は次のようになる:
message ClientState {
ibc.core.client.v1.Height latest_height = 1;
bytes mrenclave = 2;
uint64 key_expiration = 3;
repeated bytes attested_keys = 4;
}
message ConsensusState {
bytes state_id = 1;
uint64 timestamp = 2;
}
ClientStateは、LCP EnclaveのMRENCLAVEとELCのproofを検証するpublic Enclave Keyのリスト、Enclave Keyの有効期限から構成される。Enclave Keyのリストは、後述するRegisterEnclaveKey commandにより検証されたうえで追加される。
ConsensusStateは、Upstreamの高さごとのELCの状態である。高さに対応するELCのClientのStateIDとその高さに関連づくupstreamのtimestampを追跡する。
LCP Clientは、最初にCreateClient、RegisterEnclaveKey、UpdateClientの3つのコマンドにより構成されるセットアップを行う必要がある。セットアップ後、Clientの状態に基づく状態検証が可能になる。セットアップのためのワークフローは次の図のようになる。
CreateClient
CreateClientは、LCP EnclaveのMRENCLAVEとEnclave Keyの有効期限を指定しLCP Clientの状態を初期化するためのコマンドである。MRENCLAVEはRegisterEnclaveKeyでEnclave Keyを登録するために必要なAVRの検証時に用いられる。有効期限は秒単位で指定される。
RegisterEnclaveKey
RegisterEnclaveKeyは、LCP Enclaveにより生成されたEnclave Keyをclientに登録するためのコマンドである。コマンドには、Attestation Serviceにより得られたVerifiable Quote(IASにおいてはAVRとその署名)が含まれる。コマンドの定義とコマンドに対応する関数は次のようになる。
struct RegisterEnclaveKeyCommand {
pub avr_body: Vec<u8>,
pub signature: Vec<u8>,
}
fn register_enclave_key(
&self,
ctx: &dyn ClientReader,
client_id: ClientId,
client_state: ClientState,
command: RegisterEnclaveKeyCommand,
) -> Result<(ClientState, ConsensusState), Ics02Error> {
let vctx = self.validation_context(ctx);
let avr = parse_avr(command.avr_body);
assert!(verify_report(&vctx, &client_state, &avr, &header.signature));
let key = read_enclave_key_from_report(&avr).unwrap();
let expiration = avr.quote.timestamp + client_state.key_expiration;
let any_consensus_state = ctx
.consensus_state(&client_id, client_state.latest_height)
.unwrap();
let consensus_state = ConsensusState::try_from(any_consensus_state)?;
let new_client_state = client_state.with_new_key((key, expiration));
Ok((new_client_state, consensus_state))
}
LCP EnclaveのAVRを検証し、そのReportに含まれるpublic Enclave Keyを承認された鍵として有効期限付きでClientStateにセットする。
検証は、利用するAttestation Serviceごとに異なる。IASの場合、次の2つを検証する。
- AVRに対するIntelの署名の検証
- EnclaveおよびCPUのTCB(Trusted Computing Base)の有効性の確認
1.に関して、AVRはIASのReport Signing Keyによる署名がされており、それを以下のステップで検証する。 (以下は https://api.trustedservices.intel.com/documents/sgx-attestation-api-spec.pdf からの引用)
- Decode and verify the Report Signing Certificate Chain that was sent together with the report (see Report Signing Certificate Chain for details). Verify that the chain is rooted in a trusted Attestation Report Signing CA Certificate (available to download upon successful registration to IAS).
- Optionally, verify that the certificates in the chain have not been revoked (using CRLs indicated in the certificates).
- Verify the signature over the report using Attestation Report Signing Certificate.
2.に関して、AVRに含まれるisvEnclaveQuoteStatusやadvisoryIDの値を元に検証者は鍵の登録を拒絶できる。このバリデーションのルールはLCPのセキュリティ仮定に基づきLCP Clientに実装される。
また、検証された鍵はAVRのtimestampにClientStateのkey_expirationを加算した値を有効期限とする。これにより、IASにより更新された各ステータスの有効性はkey_expiration以内に確認されることを保証している。
UpdateClient
UpdateClient commandは、Clientの状態を更新するためのコマンドである。コマンドには、ELCのUpdateClient CommitmentとEnclave Keyによる署名およびpublic Enclave Keyを含める。コマンドの定義とコマンドに対応する関数は次のようになる。
pub struct UpdateClientCommand {
pub commitment_bytes: Vec<u8>,
pub signer: Vec<u8>,
pub signature: Vec<u8>,
pub commitment: UpdateClientCommitment,
}
fn update_client(
&self,
ctx: &dyn ClientReader,
client_id: ClientId,
client_state: ClientState,
command: UpdateClientCommand,
) -> Result<(ClientState, ConsensusState), Ics02Error> {
if client_state.latest_height.is_zero() {
// if the client is not initialized, `prev_height` and `prev_state_id` must be empty
assert!(command.prev_height().is_none() && command.prev_state_id().is_none());
} else {
// if the client is initialized, `prev_height` and `prev_state_id` must not be empty.
assert!(command.prev_height().is_some() && command.prev_state_id().is_some());
}
// check if the proxy's trusted consensus state exists in the store
let prev_consensus_state: ConsensusState = ctx
.consensus_state(&client_id, command.prev_height().unwrap())?
.try_into()?;
assert!(prev_consensus_state.state_id == command.prev_state_id().unwrap());
// check if the specified signer exists in the client state
assert!(client_state.contains(&command.signer()));
// check if the specified signer is not expired
assert!(client_state.is_available_key(ctx, &header.signer()));
// check if the `command.signer` matches the commitment prover
let signer = verify_signature(&command.commitment_bytes, &command.signature).unwrap();
assert!(command.signer() == signer);
// check if LCP's validation context matches our context
let vctx = self.validation_context(ctx);
assert!(validation_predicate(&vctx, command.validation_params()).unwrap());
// create a new state
let new_client_state = client_state.with_command(&command);
let new_consensus_state = ConsensusState {
state_id: command.state_id(),
timestamp: command.timestamp_as_u128(),
};
Ok((new_client_state, new_consensus_state))
}
UpdateClient commitmentと署名は、指定されたClientStateのattested_keys内の鍵により検証される。CreateClient後の最初に実行されるUpdateClient commandに含まれるcommitmentは、ELCのClientによるinitializeClientにより生成されたcommitmentである必要がある。
検証は、次のstepsで行われる
- 指定されたpublic Enclave KeyがClientStateに存在することを確認
- keyの有効期限が切れていないことを確認
- commitment proofがkeyによる署名であることを検証
- commitmentに含まれるELCの検証結果の有効性のためのpredicateを評価する
検証に成功した場合、このcommitmentに含まれるstate_idとそれに関連づくUpstreamの時刻、高さを元にConsensusStateを作成する。
最後に、PacketRelayのフローは以下の図のようになる:
PacketRelay
PacketRelay commandは、LCP ClientによるPacketのcommitmentの検証を行うコマンドである。コマンドには、ELCにより生成されたUpstreamのPacket commitmentの検証結果を示すState Commitmentを含む。コマンドに対応する関数は次のようになる。
pub fn verify_packet_data(
&self,
ctx: &dyn ChannelReader,
client_state: &ClientState,
height: Height,
connection_end: &ConnectionEnd,
proof: &CommitmentProofBytes,
root: &CommitmentRoot,
port_id: &PortId,
channel_id: &ChannelId,
sequence: Sequence,
packet_commitment: String,
) -> Result<(), Ics02Error> {
// convert `proof` to StateCommitmentProof
let commitment_proof = Self::convert_to_state_commitment_proof(proof).unwrap();
let commitment = commitment_proof.commitment();
assert!(height == commitment.height);
// check if `.path` matches expected path
assert!(
commitment.path
== CommitmentsPath {
port_id: port_id.clone(),
channel_id: channel_id.clone(),
sequence: sequence,
}
.into()
);
// check if the specified signer exists in the client state
assert!(client_state.contains(&commitment_proof.signer));
// check if the specified signer is not expired
assert!(client_state.is_available_key(ctx, &commitment_proof.signer));
// verify the signature with `commitment_proof.signer`
let signer = verify_signature(
&commitment_proof.commitment_bytes,
&commitment_proof.signature,
)
.unwrap();
assert!(Address::from(&commitment_proof.signer as &[u8]) == signer);
let mut packet_commitment_bytes = Vec::new();
packet_commitment
.encode(&mut packet_commitment_bytes)
.expect("buffer size too small");
// check if `.value` matches expected state
assert!(packet_commitment_bytes == commitment.value);
let channel_end = ctx.channel_end(&(port_id.clone(), channel_id.clone())).unwrap();
let connection_end = ctx.connection_end(&channel_end.connection_hops[0]).unwrap();
// check if `.state_id` matches the corresponding stored consensus state's state_id
let consensus_state = ConsensusState::try_from(ctx.client_consensus_state(connection_end.client_id(), height).unwrap())?;
assert!(consensus_state.state_id == commitment.state_id);
Ok(())
}
Packetの検証結果を示すState Commitmentの妥当性は、次のように確認される:
proof
のsignerがClientStateに含まれることを確認する- signerが有効期限切れでないことを確認する
- signerによりcommitment.signatureが検証される
- commitmentの
state_id
がConsensusStateのstate_id
と一致する