メインコンテンツまでスキップ

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 EnclaveMRENCLAVEと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つを検証する。

  1. AVRに対するIntelの署名の検証
  2. EnclaveおよびCPUのTCB(Trusted Computing Base)の有効性の確認

1.に関して、AVRはIASのReport Signing Keyによる署名がされており、それを以下のステップで検証する。 (以下は https://api.trustedservices.intel.com/documents/sgx-attestation-api-spec.pdf からの引用)

  1. 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).
  2. Optionally, verify that the certificates in the chain have not been revoked (using CRLs indicated in the certificates).
  3. 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で行われる

  1. 指定されたpublic Enclave KeyがClientStateに存在することを確認
  2. keyの有効期限が切れていないことを確認
  3. commitment proofがkeyによる署名であることを検証
  4. 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の妥当性は、次のように確認される:

  1. proofのsignerがClientStateに含まれることを確認する
  2. signerが有効期限切れでないことを確認する
  3. signerによりcommitment.signatureが検証される
  4. commitmentのstate_idがConsensusStateのstate_idと一致する