diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dd9157bc..7a2e26ff 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,5 +28,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - - uses: cachix/install-nix-action@v27 + - uses: cachix/install-nix-action@v30 - run: nix run nixpkgs#taplo -- fmt --check diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 20d90478..53bd89bc 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - - uses: cachix/install-nix-action@v27 + - uses: cachix/install-nix-action@v30 with: extra_nix_config: | access-tokens = github.com=${{ github.token }} @@ -37,7 +37,7 @@ jobs: runs-on: [ matterlabs-default-infra-runners ] steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - - uses: cachix/install-nix-action@v27 + - uses: cachix/install-nix-action@v30 with: extra_nix_config: | access-tokens = github.com=${{ github.token }} @@ -75,7 +75,7 @@ jobs: - { nixpackage: 'container-verify-era-proof-attestation-sgx' } steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@v27 + - uses: cachix/install-nix-action@v30 with: extra_nix_config: | access-tokens = github.com=${{ github.token }} diff --git a/Cargo.lock b/Cargo.lock index 4ceefab8..b770552e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5217,7 +5217,6 @@ dependencies = [ "hex", "rand", "secp256k1 0.29.1", - "sha3", "teepot", "tracing", "tracing-log 0.2.0", @@ -5326,10 +5325,12 @@ dependencies = [ "rand", "rsa", "rustls", + "secp256k1 0.29.1", "serde", "serde_json", "serde_with 3.11.0", "sha2", + "sha3", "signature 2.2.0", "tdx-attest-rs", "teepot-tee-quote-verification-rs", @@ -5343,6 +5344,7 @@ dependencies = [ "webpki-roots", "x509-cert", "zeroize", + "zksync_basic_types", ] [[package]] @@ -6033,6 +6035,7 @@ dependencies = [ "clap 4.5.23", "hex", "secp256k1 0.29.1", + "sha3", "teepot", "zksync_basic_types", ] diff --git a/README.md b/README.md index b1b53190..8c18eb59 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,5 @@ # teepot -Key Value store in a TEE with Remote Attestation for Authentication - -## Introduction - -This project is a key-value store that runs in a Trusted Execution Environment (TEE) and uses Remote Attestation for -Authentication. -The key-value store is implemented using Hashicorp Vault running in an Intel SGX enclave via the Gramine runtime. - ## Parts of this project - `teepot`: The main rust crate that abstracts TEEs and key-value stores. @@ -22,6 +14,18 @@ The key-value store is implemented using Hashicorp Vault running in an Intel SGX - `verify-attestation`: A client utility that verifies the attestation of an enclave. - `tee-key-preexec`: A pre-exec utility that generates a p256 secret key and passes it as an environment variable to the enclave along with the attestation quote containing the hash of the public key. +- `tdx_google`: A base VM running on Google Cloud TDX. It receives a container URL via the instance metadata, + measures the sha384 of the URL to RTMR3 and launches the container. +- `tdx-extend`: A utility to extend an RTMR register with a hash value. +- `rtmr-calc`: A utility to calculate RTMR1 and RTMR2 from a GPT disk, the linux kernel, the linux initrd + and a UKI (unified kernel image). +- `sha384-extend`: A utility to calculate RTMR registers after extending them with a digest. + +## Vault + +Part of this project is a key-value store that runs in a Trusted Execution Environment (TEE) and uses Remote Attestation +for Authentication. The key-value store is implemented using Hashicorp Vault running in an Intel SGX enclave via the +Gramine runtime. ## Development @@ -96,3 +100,9 @@ Attributes: isv_svn: 0 debug_enclave: False ``` + +### TDX VM testing + +```shell +nixos-rebuild -L --flake .#tdxtest build-vm && ./result/bin/run-tdxtest-vm +``` diff --git a/assets/gcloud-deploy.sh b/assets/gcloud-deploy.sh new file mode 100755 index 00000000..b2f8a226 --- /dev/null +++ b/assets/gcloud-deploy.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2025 Matter Labs +# + +set -ex + +NO=${NO:-1} + +nix build -L .#tdx_google + +gsutil cp result/tdx_base_1.vmdk gs://tdx_vms/ + +gcloud migration vms image-imports create \ + --location=us-central1 \ + --target-project=tdx-pilot \ + --project=tdx-pilot \ + --skip-os-adaptation \ + --source-file=gs://tdx_vms/tdx_base_1.vmdk \ + tdx-img-pre-"${NO}" + +gcloud compute instances stop tdx-pilot --zone us-central1-c --project tdx-pilot || : +gcloud compute instances delete tdx-pilot --zone us-central1-c --project tdx-pilot || : + +while gcloud migration vms image-imports list --location=us-central1 --project=tdx-pilot | grep -F RUNNING; do + sleep 1 +done + +gcloud compute images create \ + --project tdx-pilot \ + --guest-os-features=UEFI_COMPATIBLE,TDX_CAPABLE,GVNIC,VIRTIO_SCSI_MULTIQUEUE \ + --storage-location=us-central1 \ + --source-image=tdx-img-pre-"${NO}" \ + tdx-img-f-"${NO}" + +gcloud compute instances create tdx-pilot \ + --machine-type c3-standard-4 --zone us-central1-c \ + --confidential-compute-type=TDX \ + --maintenance-policy=TERMINATE \ + --image-project=tdx-pilot \ + --project tdx-pilot \ + --metadata=container_hub="docker.io",container_image="amd64/hello-world@sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57" \ + --image tdx-img-f-"${NO}" diff --git a/bin/tee-key-preexec/Cargo.toml b/bin/tee-key-preexec/Cargo.toml index 7732d68c..20d3e372 100644 --- a/bin/tee-key-preexec/Cargo.toml +++ b/bin/tee-key-preexec/Cargo.toml @@ -15,7 +15,6 @@ clap.workspace = true hex.workspace = true rand.workspace = true secp256k1.workspace = true -sha3.workspace = true teepot.workspace = true tracing.workspace = true tracing-log.workspace = true diff --git a/bin/tee-key-preexec/src/main.rs b/bin/tee-key-preexec/src/main.rs index 2d614787..ac384328 100644 --- a/bin/tee-key-preexec/src/main.rs +++ b/bin/tee-key-preexec/src/main.rs @@ -8,9 +8,9 @@ use anyhow::{Context, Result}; use clap::Parser; -use secp256k1::{rand, PublicKey, Secp256k1, SecretKey}; -use sha3::{Digest, Keccak256}; +use secp256k1::{rand, Secp256k1}; use std::{ffi::OsString, os::unix::process::CommandExt, process::Command}; +use teepot::ethereum::public_key_to_ethereum_address; use teepot::quote::get_quote; use tracing::error; use tracing_log::LogTracer; @@ -29,19 +29,6 @@ struct Args { cmd_args: Vec, } -/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256. -pub fn public_key_to_address(public: &PublicKey) -> [u8; 20] { - let public_key_bytes = public.serialize_uncompressed(); - - // Skip the first byte (0x04) which indicates uncompressed key - let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into(); - - // Take the last 20 bytes of the hash to get the Ethereum address - let mut address = [0u8; 20]; - address.copy_from_slice(&hash[12..]); - address -} - fn main_with_error() -> Result<()> { LogTracer::init().context("Failed to set logger")?; @@ -54,7 +41,7 @@ fn main_with_error() -> Result<()> { let mut rng = rand::thread_rng(); let secp = Secp256k1::new(); let (signing_key, verifying_key) = secp.generate_keypair(&mut rng); - let ethereum_address = public_key_to_address(&verifying_key); + let ethereum_address = public_key_to_ethereum_address(&verifying_key); let tee_type = match get_quote(ethereum_address.as_ref()) { Ok((tee_type, quote)) => { // save quote to file @@ -99,6 +86,8 @@ fn main() -> Result<()> { #[cfg(test)] mod tests { + use secp256k1::{PublicKey, Secp256k1, SecretKey}; + use super::*; #[test] @@ -110,7 +99,7 @@ mod tests { let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap(); let public_key = PublicKey::from_secret_key(&secp, &secret_key); let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap(); - let address = public_key_to_address(&public_key); + let address = public_key_to_ethereum_address(&public_key); assert_eq!(address, expected_address.as_slice()); } diff --git a/bin/verify-attestation/Cargo.toml b/bin/verify-attestation/Cargo.toml index ff473d9a..5a7c2762 100644 --- a/bin/verify-attestation/Cargo.toml +++ b/bin/verify-attestation/Cargo.toml @@ -12,5 +12,6 @@ anyhow.workspace = true clap.workspace = true hex.workspace = true secp256k1.workspace = true +sha3.workspace = true teepot.workspace = true zksync_basic_types.workspace = true diff --git a/bin/verify-attestation/src/main.rs b/bin/verify-attestation/src/main.rs index d7994626..8ba0c2b7 100644 --- a/bin/verify-attestation/src/main.rs +++ b/bin/verify-attestation/src/main.rs @@ -5,10 +5,13 @@ use anyhow::{Context, Result}; use clap::{Args, Parser, Subcommand}; -use secp256k1::{ecdsa::Signature, Message, PublicKey}; +use core::convert::TryInto; +use hex::encode; +use secp256k1::{Message, PublicKey}; use std::{fs, io::Read, path::PathBuf, str::FromStr, time::UNIX_EPOCH}; use teepot::{ client::TcbLevel, + ethereum::recover_signer, quote::{error, tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult}, }; use zksync_basic_types::H256; @@ -87,14 +90,25 @@ fn verify_signature( let reportdata = "e_verification_result.quote.get_report_data(); let public_key = PublicKey::from_slice(reportdata)?; println!("Public key from attestation quote: {}", public_key); - let signature_bytes = fs::read(&signature_args.signature_file)?; - let signature = Signature::from_compact(&signature_bytes)?; - let root_hash_msg = Message::from_digest_slice(&signature_args.root_hash.0)?; - if signature.verify(&root_hash_msg, &public_key).is_ok() { - println!("Signature verified successfully"); - } else { - println!("Failed to verify signature"); - } + let signature_bytes: &[u8] = &fs::read(&signature_args.signature_file)?; + let ethereum_address_from_quote = "e_verification_result.quote.get_report_data()[..20]; + let root_hash_bytes = signature_args.root_hash.as_bytes(); + let root_hash_msg = Message::from_digest_slice(root_hash_bytes)?; + let ethereum_address_from_signature = + recover_signer(&signature_bytes.try_into()?, &root_hash_msg)?; + let verification_successful = ethereum_address_from_signature == ethereum_address_from_quote; + + println!( + "Signature '{}' {}. Ethereum address from attestation quote: {}. Ethereum address from signature: {}.", + encode(signature_bytes), + if verification_successful { + "verified successfully" + } else { + "verification failed" + }, + encode(ethereum_address_from_quote), + encode(ethereum_address_from_signature) + ); Ok(()) } diff --git a/bin/verify-era-proof-attestation/src/verification.rs b/bin/verify-era-proof-attestation/src/verification.rs index b3895003..4c99bf3b 100644 --- a/bin/verify-era-proof-attestation/src/verification.rs +++ b/bin/verify-era-proof-attestation/src/verification.rs @@ -4,16 +4,17 @@ use crate::{args::AttestationPolicyArgs, client::JsonRpcClient}; use anyhow::{Context, Result}; use hex::encode; -use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey}; +use secp256k1::Message; use teepot::{ client::TcbLevel, + ethereum::recover_signer, quote::{ error::QuoteContext, tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult, Report, }, }; use tracing::{debug, info, warn}; -use zksync_basic_types::{L1BatchNumber, H256}; +use zksync_basic_types::L1BatchNumber; pub async fn verify_batch_proof( quote_verification_result: &QuoteVerificationResult, @@ -27,22 +28,38 @@ pub async fn verify_batch_proof( } let batch_no = batch_number.0; - - let public_key = PublicKey::from_slice( - "e_verification_result.quote.get_report_data()[..PUBLIC_KEY_SIZE], - )?; - debug!(batch_no, "public key: {}", public_key); - let root_hash = node_client.get_root_hash(batch_number).await?; - debug!(batch_no, "root hash: {}", root_hash); + let ethereum_address_from_quote = "e_verification_result.quote.get_report_data()[..20]; + let signature_bytes: &[u8; 65] = signature.try_into()?; + let root_hash_bytes = root_hash.as_bytes(); + let root_hash_msg = Message::from_digest_slice(root_hash_bytes)?; + let ethereum_address_from_signature = recover_signer(signature_bytes, &root_hash_msg)?; + let verification_successful = ethereum_address_from_signature == ethereum_address_from_quote; + debug!( + batch_no, + "Root hash: {}. Ethereum address from the attestation quote: {}. Ethereum address from the signature: {}.", + root_hash, + encode(ethereum_address_from_quote), + encode(ethereum_address_from_signature), + ); - let is_verified = verify_signature(signature, public_key, root_hash)?; - if is_verified { - info!(batch_no, signature = %encode(signature), "Signature verified successfully."); + if verification_successful { + info!( + batch_no, + signature = encode(signature), + ethereum_address = encode(ethereum_address_from_quote), + "Signature verified successfully." + ); } else { - warn!(batch_no, signature = %encode(signature), "Failed to verify signature!"); + warn!( + batch_no, + signature = encode(signature), + ethereum_address_from_signature = encode(ethereum_address_from_signature), + ethereum_address_from_quote = encode(ethereum_address_from_quote), + "Failed to verify signature!" + ); } - Ok(is_verified) + Ok(verification_successful) } pub fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result { @@ -85,12 +102,6 @@ pub fn log_quote_verification_summary(quote_verification_result: &QuoteVerificat ); } -fn verify_signature(signature: &[u8], public_key: PublicKey, root_hash: H256) -> Result { - let signature = Signature::from_compact(signature)?; - let root_hash_msg = Message::from_digest_slice(&root_hash.0)?; - Ok(signature.verify(&root_hash_msg, &public_key).is_ok()) -} - fn is_quote_matching_policy( attestation_policy: &AttestationPolicyArgs, quote_verification_result: &QuoteVerificationResult, diff --git a/crates/teepot-tee-quote-verification-rs/src/lib.rs b/crates/teepot-tee-quote-verification-rs/src/lib.rs index d337924a..c19b2926 100644 --- a/crates/teepot-tee-quote-verification-rs/src/lib.rs +++ b/crates/teepot-tee-quote-verification-rs/src/lib.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright (c) 2024 Matter Labs +// Copyright (c) 2024-2025 Matter Labs // SPDX-License-Identifier: BSD-3-Clause /* @@ -39,22 +39,14 @@ //! This is a safe wrapper for **sgx-dcap-quoteverify-sys**. use serde::{Deserialize, Serialize}; -use std::marker::PhantomData; -use std::mem; -use std::ops::Deref; -use std::slice; +use std::{marker::PhantomData, ops::Deref, slice}; use intel_tee_quote_verification_sys as qvl_sys; - -pub use qvl_sys::quote3_error_t; -pub use qvl_sys::sgx_ql_qe_report_info_t; -pub use qvl_sys::sgx_ql_qv_result_t; -pub use qvl_sys::sgx_ql_qv_supplemental_t; -pub use qvl_sys::sgx_ql_qve_collateral_t; -pub use qvl_sys::sgx_ql_request_policy_t; -pub use qvl_sys::sgx_qv_path_type_t; -pub use qvl_sys::tdx_ql_qve_collateral_t; -pub use qvl_sys::tee_supp_data_descriptor_t; +pub use qvl_sys::{ + quote3_error_t, sgx_ql_qe_report_info_t, sgx_ql_qv_result_t, sgx_ql_qv_supplemental_t, + sgx_ql_qve_collateral_t, sgx_ql_request_policy_t, sgx_qv_path_type_t, tdx_ql_qve_collateral_t, + tee_qv_free_collateral, tee_supp_data_descriptor_t, +}; /// When the Quoting Verification Library is linked to a process, it needs to know the proper enclave loading policy. /// The library may be linked with a long lived process, such as a service, where it can load the enclaves and leave @@ -328,43 +320,6 @@ pub struct Collateral { pub qe_identity: Box<[u8]>, } -impl TryFrom<&sgx_ql_qve_collateral_t> for Collateral { - type Error = (); - - fn try_from(value: &sgx_ql_qve_collateral_t) -> Result { - fn to_boxed_slice(p: *mut ::std::os::raw::c_char, size: u32) -> Result, ()> { - if p.is_null() { - return Err(()); - } - Ok(Box::from(unsafe { - slice::from_raw_parts(p as _, size as _) - })) - } - - Ok(Collateral { - major_version: unsafe { value.__bindgen_anon_1.__bindgen_anon_1.major_version }, - minor_version: unsafe { value.__bindgen_anon_1.__bindgen_anon_1.minor_version }, - tee_type: value.tee_type, - pck_crl_issuer_chain: to_boxed_slice( - value.pck_crl_issuer_chain, - value.pck_crl_issuer_chain_size, - )?, - root_ca_crl: to_boxed_slice(value.root_ca_crl, value.root_ca_crl_size)?, - pck_crl: to_boxed_slice(value.pck_crl, value.pck_crl_size)?, - tcb_info_issuer_chain: to_boxed_slice( - value.tcb_info_issuer_chain, - value.tcb_info_issuer_chain_size, - )?, - tcb_info: to_boxed_slice(value.tcb_info, value.tcb_info_size)?, - qe_identity_issuer_chain: to_boxed_slice( - value.qe_identity_issuer_chain, - value.qe_identity_issuer_chain_size, - )?, - qe_identity: to_boxed_slice(value.qe_identity, value.qe_identity_size)?, - }) - } -} - // referential struct struct SgxQlQveCollateralT<'a> { inner: sgx_ql_qve_collateral_t, @@ -432,6 +387,55 @@ impl Deref for SgxQlQveCollateralT<'_> { /// - *SGX_QL_ERROR_UNEXPECTED* /// pub fn tee_qv_get_collateral(quote: &[u8]) -> Result { + fn try_into_collateral( + buf: *const sgx_ql_qve_collateral_t, + buf_len: u32, + ) -> Result { + fn try_into_boxed_slice( + p: *mut ::std::os::raw::c_char, + size: u32, + ) -> Result, quote3_error_t> { + if p.is_null() || !p.is_aligned() { + return Err(quote3_error_t::SGX_QL_ERROR_MAX); + } + Ok(Box::from(unsafe { + slice::from_raw_parts(p as _, size as _) + })) + } + + if buf.is_null() + || (buf_len as usize) < size_of::() + || !buf.is_aligned() + { + return Err(quote3_error_t::SGX_QL_ERROR_MAX); + } + + // SAFETY: buf is not null, buf_len is not zero, and buf is aligned. + let collateral = unsafe { *buf }; + + Ok(Collateral { + major_version: unsafe { collateral.__bindgen_anon_1.__bindgen_anon_1.major_version }, + minor_version: unsafe { collateral.__bindgen_anon_1.__bindgen_anon_1.minor_version }, + tee_type: collateral.tee_type, + pck_crl_issuer_chain: try_into_boxed_slice( + collateral.pck_crl_issuer_chain, + collateral.pck_crl_issuer_chain_size, + )?, + root_ca_crl: try_into_boxed_slice(collateral.root_ca_crl, collateral.root_ca_crl_size)?, + pck_crl: try_into_boxed_slice(collateral.pck_crl, collateral.pck_crl_size)?, + tcb_info_issuer_chain: try_into_boxed_slice( + collateral.tcb_info_issuer_chain, + collateral.tcb_info_issuer_chain_size, + )?, + tcb_info: try_into_boxed_slice(collateral.tcb_info, collateral.tcb_info_size)?, + qe_identity_issuer_chain: try_into_boxed_slice( + collateral.qe_identity_issuer_chain, + collateral.qe_identity_issuer_chain_size, + )?, + qe_identity: try_into_boxed_slice(collateral.qe_identity, collateral.qe_identity_size)?, + }) + } + let mut buf = std::ptr::null_mut(); let mut buf_len = 0u32; @@ -439,15 +443,12 @@ pub fn tee_qv_get_collateral(quote: &[u8]) -> Result qvl_sys::tee_qv_get_collateral(quote.as_ptr(), quote.len() as u32, &mut buf, &mut buf_len) } { quote3_error_t::SGX_QL_SUCCESS => { - assert!(!buf.is_null()); - assert!(buf_len > 0); - assert_eq!( - (buf as usize) % mem::align_of::(), - 0 - ); - // SAFETY: buf is not null, buf_len is not zero, and buf is aligned. - let orig_collateral = &unsafe { *(buf as *const sgx_ql_qve_collateral_t) }; - Collateral::try_from(orig_collateral).map_err(|_| quote3_error_t::SGX_QL_ERROR_MAX) + let collateral = try_into_collateral(buf as _, buf_len); + + match unsafe { tee_qv_free_collateral(buf) } { + quote3_error_t::SGX_QL_SUCCESS => collateral, + error_code => Err(error_code), + } } error_code => Err(error_code), } diff --git a/crates/teepot/Cargo.toml b/crates/teepot/Cargo.toml index df200044..3b3b6b86 100644 --- a/crates/teepot/Cargo.toml +++ b/crates/teepot/Cargo.toml @@ -32,10 +32,12 @@ pkcs8.workspace = true rand.workspace = true rsa.workspace = true rustls.workspace = true +secp256k1 = { workspace = true, features = ["recovery"] } serde.workspace = true serde_json.workspace = true serde_with.workspace = true sha2.workspace = true +sha3.workspace = true signature.workspace = true tdx-attest-rs.workspace = true thiserror.workspace = true @@ -47,8 +49,8 @@ x509-cert.workspace = true zeroize.workspace = true [dev-dependencies] -anyhow.workspace = true base64.workspace = true testaso.workspace = true tokio.workspace = true tracing-test.workspace = true +zksync_basic_types.workspace = true diff --git a/crates/teepot/src/ethereum/mod.rs b/crates/teepot/src/ethereum/mod.rs new file mode 100644 index 00000000..a8ab8789 --- /dev/null +++ b/crates/teepot/src/ethereum/mod.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023-2024 Matter Labs + +//! Ethereum-specific helper functions for on-chain verification of Intel SGX attestation. + +use anyhow::Result; +use secp256k1::{ + ecdsa::{RecoverableSignature, RecoveryId}, + Message, PublicKey, SECP256K1, +}; +use sha3::{Digest, Keccak256}; + +/// Equivalent to the ecrecover precompile, ensuring that the signatures we produce off-chain +/// can be recovered on-chain. +pub fn recover_signer(sig: &[u8; 65], root_hash: &Message) -> Result<[u8; 20]> { + let sig = RecoverableSignature::from_compact( + &sig[0..64], + RecoveryId::from_i32(sig[64] as i32 - 27)?, + )?; + let public = SECP256K1.recover_ecdsa(root_hash, &sig)?; + Ok(public_key_to_ethereum_address(&public)) +} + +/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256. +pub fn public_key_to_ethereum_address(public: &PublicKey) -> [u8; 20] { + let public_key_bytes = public.serialize_uncompressed(); + + // Skip the first byte (0x04) which indicates uncompressed key + let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into(); + + // Take the last 20 bytes of the hash to get the Ethereum address + let mut address = [0u8; 20]; + address.copy_from_slice(&hash[12..]); + address +} + +#[cfg(test)] +mod tests { + use secp256k1::{Secp256k1, SecretKey}; + use zksync_basic_types::H256; + + use super::*; + + /// Signs the message in Ethereum-compatible format for on-chain verification. + fn sign_message(sec: &SecretKey, message: Message) -> Result<[u8; 65]> { + let s = SECP256K1.sign_ecdsa_recoverable(&message, sec); + let (rec_id, data) = s.serialize_compact(); + + let mut signature = [0u8; 65]; + signature[..64].copy_from_slice(&data); + // as defined in the Ethereum Yellow Paper (Appendix F) + // https://ethereum.github.io/yellowpaper/paper.pdf + signature[64] = 27 + rec_id.to_i32() as u8; + + Ok(signature) + } + + #[test] + fn recover() { + // Decode the sample secret key, generate the public key, and derive the Ethereum address + // from the public key + let secp = Secp256k1::new(); + let secret_key_bytes = + hex::decode("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3") + .unwrap(); + let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap(); + let address = public_key_to_ethereum_address(&public_key); + + assert_eq!(address, expected_address.as_slice()); + + // Generate a random root hash, create a message from the hash, and sign the message using + // the secret key + let root_hash = H256::random(); + let root_hash_bytes = root_hash.as_bytes(); + let msg_to_sign = Message::from_digest_slice(root_hash_bytes).unwrap(); + let signature = sign_message(&secret_key, msg_to_sign).unwrap(); + + // Recover the signer's Ethereum address from the signature and the message, and verify it + // matches the expected address + let proof_addr = recover_signer(&signature, &msg_to_sign).unwrap(); + + assert_eq!(proof_addr, expected_address.as_slice()); + } +} diff --git a/crates/teepot/src/lib.rs b/crates/teepot/src/lib.rs index 2d9f7cc9..5cdf25b8 100644 --- a/crates/teepot/src/lib.rs +++ b/crates/teepot/src/lib.rs @@ -7,6 +7,7 @@ #![deny(clippy::all)] pub mod client; +pub mod ethereum; pub mod json; pub mod log; pub mod quote; diff --git a/flake.nix b/flake.nix index dfff5396..0512524a 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,9 @@ }; outputs = inputs: - let src = ./.; in + let + src = ./.; + in inputs.snowfall-lib.mkFlake { inherit inputs; inherit src; diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 00000000..6ebd5432 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,3 @@ +{ ... }: { + nixosGenerate = import ./nixos-generate.nix; +} diff --git a/lib/nixos-generate.nix b/lib/nixos-generate.nix new file mode 100644 index 00000000..7b4806c5 --- /dev/null +++ b/lib/nixos-generate.nix @@ -0,0 +1,33 @@ +{ pkgs +, nixosSystem +, formatModule +, system +, specialArgs ? { } +, modules ? [ ] +}: +let + image = nixosSystem { + inherit pkgs specialArgs; + modules = + [ + formatModule + ( + { lib, ... }: { + options = { + fileExtension = lib.mkOption { + type = lib.types.str; + description = "Declare the path of the wanted file in the output directory"; + default = ""; + }; + formatAttr = lib.mkOption { + type = lib.types.str; + description = "Declare the default attribute to build"; + }; + }; + } + ) + ] + ++ modules; + }; +in +image.config.system.build.${image.config.formatAttr} diff --git a/packages/container-tee-key-preexec-dcap/default.nix b/packages/container-tee-key-preexec-dcap/default.nix new file mode 100644 index 00000000..b49ff69f --- /dev/null +++ b/packages/container-tee-key-preexec-dcap/default.nix @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2024 Matter Labs +{ teepot +, pkgs +, bash +, coreutils +, container-name ? "teepot-key-preexec-dcap" +, tag ? null +}: let + entrypoint = "${bash}/bin/bash"; +in +pkgs.lib.tee.sgxGramineContainer { + name = container-name; + inherit tag entrypoint; + + packages = [ teepot.teepot.tee_key_preexec coreutils bash ]; + + manifest = { + loader = { + argv = [ + entrypoint + "-c" + ("${teepot.teepot.tee_key_preexec}/bin/tee-key-preexec -- bash -c " + + "'echo \"SIGNING_KEY=$SIGNING_KEY\"; echo \"TEE_TYPE=$TEE_TYPE\";exec base64 \"$ATTESTATION_QUOTE_FILE_PATH\";'") + ]; + + log_level = "error"; + env = { + RUST_BACKTRACE = "1"; + RUST_LOG = "trace"; + }; + }; + sgx = { + edmm_enable = true; + max_threads = 2; + }; + }; +} diff --git a/packages/tdx_google/configuration.nix b/packages/tdx_google/configuration.nix new file mode 100644 index 00000000..c25618c4 --- /dev/null +++ b/packages/tdx_google/configuration.nix @@ -0,0 +1,180 @@ +{ lib +, modulesPath +, pkgs +, ... +}: { + imports = [ + "${toString modulesPath}/profiles/minimal.nix" + "${toString modulesPath}/profiles/qemu-guest.nix" + ]; + + /* + # SSH login for debugging + services.sshd.enable = true; + networking.firewall.allowedTCPPorts = [ 22 ]; + services.openssh.settings.PermitRootLogin = lib.mkOverride 999 "yes"; + users.users.root.openssh.authorizedKeys.keys = [ + "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIDsb/Tr69YN5MQLweWPuJaRGm+h2kOyxfD6sqKEDTIwoAAAABHNzaDo=" + "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBACLgT81iB1iWWVuXq6PdQ5GAAGhaZhSKnveQCvcNnAOZ5WKH80bZShKHyAYzrzbp8IGwLWJcZQ7TqRK+qZdfagAAAAEc3NoOg==" + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAYbUTKpy4QR3s944/hjJ1UK05asFEs/SmWeUbtS0cdA660sT4xHnRfals73FicOoz+uIucJCwn/SCM804j+wtM=" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMNsmP15vH8BVKo7bdvIiiEjiQboPGcRPqJK0+bH4jKD" + ]; + */ + + # the container might want to listen on ports + networking.firewall.enable = true; + networking.firewall.allowedTCPPortRanges = [{ from = 1024; to = 65535; }]; + networking.firewall.allowedUDPPortRanges = [{ from = 1024; to = 65535; }]; + + networking.useNetworkd = lib.mkDefault true; + + # don't fill up the logs + networking.firewall.logRefusedConnections = false; + + virtualisation.docker.enable = true; + + systemd.services.docker_start_container = { + description = "The main application container"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" "docker.service" ]; + requires = [ "network-online.target" "docker.service" ]; + serviceConfig = { + Type = "exec"; + User = "root"; + }; + path = [ pkgs.curl pkgs.docker pkgs.teepot.teepot.tdx_extend pkgs.coreutils ]; + script = '' + set -eu -o pipefail + : "''${CONTAINER_IMAGE:=$(curl --silent --fail "http://metadata.google.internal/computeMetadata/v1/instance/attributes/container_image" -H "Metadata-Flavor: Google")}" + : "''${CONTAINER_HUB:=$(curl --silent --fail "http://metadata.google.internal/computeMetadata/v1/instance/attributes/container_hub" -H "Metadata-Flavor: Google")}" + : "''${CONTAINER_USER:=$(curl --silent --fail "http://metadata.google.internal/computeMetadata/v1/instance/attributes/container_user" -H "Metadata-Flavor: Google")}" + : "''${CONTAINER_TOKEN:=$(curl --silent --fail "http://metadata.google.internal/computeMetadata/v1/instance/attributes/container_token" -H "Metadata-Flavor: Google")}" + + : "''${CONTAINER_IMAGE:?Error: Missing CONTAINER_IMAGE}" + : "''${CONTAINER_HUB:?Error: Missing CONTAINER_HUB}" + + if [[ $CONTAINER_USER ]] && [[ $CONTAINER_TOKEN ]]; then + docker login -u "$CONTAINER_USER" -p "$CONTAINER_TOKEN" "$CONTAINER_HUB" + fi + + docker pull "''${CONTAINER_HUB}/''${CONTAINER_IMAGE}" + DIGEST=$(docker inspect --format '{{.Id}}' "''${CONTAINER_HUB}/''${CONTAINER_IMAGE}") + DIGEST=''${DIGEST#sha256:} + echo "Measuring $DIGEST" >&2 + test -c /dev/tdx_guest && tdx-extend --digest "$DIGEST" --rtmr 3 + exec docker run --init --privileged "sha256:$DIGEST" + ''; + + postStop = lib.mkDefault '' + shutdown --reboot +5 + ''; + }; + + services.prometheus.exporters.node = { + enable = true; + port = 9100; + enabledCollectors = [ + "logind" + "systemd" + ]; + disabledCollectors = [ + "textfile" + ]; + #openFirewall = true; + #firewallFilter = "-i br0 -p tcp -m tcp --dport 9100"; + }; + + environment.systemPackages = with pkgs; [ + teepot.teepot + ]; + + # /var is on tmpfs anyway + services.journald.storage = "volatile"; + + # we can't rely on/trust the hypervisor + services.timesyncd.enable = false; + services.chrony = { + enable = true; + enableNTS = true; + servers = [ + "time.cloudflare.com" + "ntppool1.time.nl" + "ntppool2.time.nl" + ]; + }; + systemd.services."chronyd".after = [ "network-online.target" ]; + + boot.kernelPackages = lib.mkForce pkgs.linuxPackages_6_12; + boot.kernelPatches = [ + { + name = "tdx-rtmr"; + patch = pkgs.fetchurl { + url = "https://github.com/haraldh/linux/commit/12d08008a5c94175e7a7dfcee40dff33431d9033.patch"; + hash = "sha256-sVDhvC3qnXpL5FRxWiQotH7Nl/oqRBQGjJGyhsKeBTA="; + }; + } + ]; + + boot.kernelParams = [ + "console=ttyS0,115200n8" + "random.trust_cpu=on" + ]; + + boot.consoleLogLevel = 7; + + boot.initrd.includeDefaultModules = false; + boot.initrd.availableKernelModules = [ + "tdx_guest" + "nvme" + "sd_mod" + "dm_mod" + "ata_piix" + ]; + + boot.initrd.systemd.enable = lib.mkDefault true; + + services.logind.extraConfig = '' + NAutoVTs=0 + ReserveVT=0 + ''; + + services.dbus.implementation = "broker"; + + boot.initrd.systemd.tpm2.enable = lib.mkForce false; + systemd.tpm2.enable = lib.mkForce false; + + nix.enable = false; # it's a read-only nix store anyway + + security.pam.services.su.forwardXAuth = lib.mkForce false; + + users.mutableUsers = false; + users.allowNoPasswordLogin = true; + + system.stateVersion = lib.version; + system.switch.enable = lib.mkForce false; + + documentation.info.enable = lib.mkForce false; + documentation.nixos.enable = lib.mkForce false; + documentation.man.enable = lib.mkForce false; + documentation.enable = lib.mkForce false; + + services.udisks2.enable = false; # udisks has become too bloated to have in a headless system + + # Get rid of the perl ecosystem to minimize the TCB and disk size + + # Remove perl from activation + system.etc.overlay.enable = lib.mkDefault true; + services.userborn.enable = lib.mkDefault true; + + # Random perl remnants + system.disableInstallerTools = lib.mkForce true; + programs.less.lessopen = lib.mkDefault null; + programs.command-not-found.enable = lib.mkDefault false; + boot.enableContainers = lib.mkForce false; + boot.loader.grub.enable = lib.mkDefault false; + environment.defaultPackages = lib.mkDefault [ ]; + + # Check that the system does not contain a Nix store path that contains the + # string "perl". + system.forbiddenDependenciesRegexes = [ "perl" ]; +} diff --git a/packages/tdx_google/default.nix b/packages/tdx_google/default.nix new file mode 100644 index 00000000..5044fd77 --- /dev/null +++ b/packages/tdx_google/default.nix @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2024 Matter Labs +{ lib +, pkgs +, system +, ... +}: lib.teepot.nixosGenerate { + inherit (lib) nixosSystem; + inherit system pkgs; + modules = [ + ./configuration.nix + ./google.nix + ]; + formatModule = ./verity.nix; +} diff --git a/packages/tdx_google/google.nix b/packages/tdx_google/google.nix new file mode 100644 index 00000000..26847042 --- /dev/null +++ b/packages/tdx_google/google.nix @@ -0,0 +1,33 @@ +{ lib +, pkgs +, modulesPath +, ... +}: { + imports = [ + "${toString modulesPath}/profiles/headless.nix" + ]; + + system.image.id = "tdx_base"; + + boot.initrd.kernelModules = [ "virtio_scsi" ]; + boot.kernelModules = [ "virtio_pci" "virtio_net" ]; + + # Force getting the hostname from Google Compute. + networking.hostName = lib.mkForce ""; + + # Configure default metadata hostnames + networking.extraHosts = '' + 169.254.169.254 metadata.google.internal metadata + ''; + + networking.timeServers = [ "metadata.google.internal" ]; + + environment.etc."sysctl.d/60-gce-network-security.conf".source = "${pkgs.google-guest-configs}/etc/sysctl.d/60-gce-network-security.conf"; + + networking.usePredictableInterfaceNames = false; + + # GC has 1460 MTU + networking.interfaces.eth0.mtu = 1460; + + boot.extraModprobeConfig = lib.readFile "${pkgs.google-guest-configs}/etc/modprobe.d/gce-blacklist.conf"; +} diff --git a/packages/tdx_google/verity.nix b/packages/tdx_google/verity.nix new file mode 100644 index 00000000..316fbb72 --- /dev/null +++ b/packages/tdx_google/verity.nix @@ -0,0 +1,127 @@ +{ config +, pkgs +, lib +, modulesPath +, ... +}: +let + inherit (config.image.repart.verityStore) partitionIds; +in +{ + imports = [ + "${toString modulesPath}/image/repart.nix" + ]; + + fileSystems = { + "/" = { + fsType = "tmpfs"; + options = [ "mode=0755" "noexec" ]; + }; + + "/dev/shm" = { + fsType = "tmpfs"; + options = [ "defaults" "nosuid" "noexec" "nodev" "size=2G" ]; + }; + + "/run" = { + fsType = "tmpfs"; + options = [ "defaults" "mode=0755" "nosuid" "noexec" "nodev" "size=512M" ]; + }; + + "/usr" = { + device = "/dev/mapper/usr"; + # explicitly mount it read-only otherwise systemd-remount-fs will fail + options = [ "ro" ]; + fsType = config.image.repart.partitions.${partitionIds.store}.repartConfig.Format; + }; + + # bind-mount the store + "/nix/store" = { + device = "/usr/nix/store"; + options = [ "bind" ]; + }; + }; + + image.repart = { + verityStore = { + enable = true; + ukiPath = "/EFI/BOOT/BOOTx64.EFI"; + }; + + partitions = { + ${partitionIds.esp} = { + # the UKI is injected into this partition by the verityStore module + repartConfig = { + Type = "esp"; + Format = "vfat"; + SizeMinBytes = "64M"; + }; + }; + ${partitionIds.store-verity}.repartConfig = { + Minimize = "best"; + }; + ${partitionIds.store}.repartConfig = { + Minimize = "best"; + Format = "squashfs"; + }; + }; + }; + + boot = { + loader.grub.enable = false; + initrd.systemd.enable = true; + }; + + system.image = { + id = lib.mkDefault "nixos-appliance"; + version = "1"; + }; + + # don't create /usr/bin/env + # this would require some extra work on read-only /usr + # and it is not a strict necessity + system.activationScripts.usrbinenv = lib.mkForce ""; + + boot.kernelParams = [ + "systemd.verity_usr_options=panic-on-corruption" + "panic=30" + "boot.panic_on_fail" # reboot the machine upon fatal boot issues + "lockdown=1" + ]; + + system.build.vmdk_verity = + config.system.build.finalImage.overrideAttrs + ( + finalAttrs: previousAttrs: + let + kernel = config.boot.uki.settings.UKI.Linux; + ukifile = "${config.system.build.uki}/${config.system.boot.loader.ukiFile}"; + in + { + nativeBuildInputs = + previousAttrs.nativeBuildInputs + ++ [ + pkgs.qemu + pkgs.teepot.teepot.rtmr_calc + ]; + + postInstall = '' + qemu-img convert -f raw -O vmdk \ + $out/${config.image.repart.imageFileBasename}.raw \ + $out/${config.image.repart.imageFileBasename}.vmdk + qemu-img info \ + $out/${config.image.repart.imageFileBasename}.vmdk + echo "kernel: ${kernel}" + echo "uki: ${ukifile}" + rtmr-calc \ + --image $out/${config.image.repart.imageFileBasename}.raw \ + --bootefi "${ukifile}" \ + --kernel "${kernel}" | tee $out/${config.image.repart.imageFileBasename}_rtmr.json + rm -vf $out/${config.image.repart.imageFileBasename}.raw + ''; + } + ); + + formatAttr = lib.mkForce "vmdk_verity"; + fileExtension = lib.mkForce ".raw"; +} diff --git a/shells/teepot/default.nix b/shells/teepot/default.nix index effb4135..67bedf6a 100644 --- a/shells/teepot/default.nix +++ b/shells/teepot/default.nix @@ -1,16 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2024 Matter Labs { lib +, pkgs , mkShell , teepot -, dive -, taplo -, vault -, cargo-release , nixsgx , stdenv , teepotCrate -, pkg-config }: let toolchain_with_src = (teepotCrate.rustVersion.override { @@ -20,20 +16,26 @@ in mkShell { inputsFrom = [ teepot.teepot ]; - nativeBuildInputs = [ + nativeBuildInputs = with pkgs; [ toolchain_with_src pkg-config teepotCrate.rustPlatform.bindgenHook ]; - packages = [ + packages = with pkgs; [ dive taplo vault cargo-release + google-cloud-sdk-gce + azure-cli + kubectl + kubectx + k9s ]; TEE_LD_LIBRARY_PATH = lib.makeLibraryPath [ + pkgs.curl nixsgx.sgx-dcap nixsgx.sgx-dcap.quote_verify nixsgx.sgx-dcap.default_qpl diff --git a/systems/x86_64-linux/tdxtest/default.nix b/systems/x86_64-linux/tdxtest/default.nix new file mode 100644 index 00000000..d95dce5c --- /dev/null +++ b/systems/x86_64-linux/tdxtest/default.nix @@ -0,0 +1,172 @@ +{ config +, pkgs +, lib +, ... +}: { + imports = [ + ./../../../packages/tdx_google/configuration.nix + ]; + + systemd.services.docker_start_container = { + environment = { + CONTAINER_IMAGE = "amd64/hello-world@sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57"; + CONTAINER_HUB = "docker.io"; + CONTAINER_USER = ""; + CONTAINER_TOKEN = ""; + }; + + postStop = '' + : + ''; + }; + + console.enable = true; + + services.getty.autologinUser = lib.mkOverride 999 "root"; + + networking.firewall.allowedTCPPorts = [ 22 ]; + services.sshd.enable = true; + services.openssh.settings.PermitRootLogin = lib.mkOverride 999 "yes"; + users.users.root.openssh.authorizedKeys.keys = [ + "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIDsb/Tr69YN5MQLweWPuJaRGm+h2kOyxfD6sqKEDTIwoAAAABHNzaDo=" + "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBACLgT81iB1iWWVuXq6PdQ5GAAGhaZhSKnveQCvcNnAOZ5WKH80bZShKHyAYzrzbp8IGwLWJcZQ7TqRK+qZdfagAAAAEc3NoOg==" + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAYbUTKpy4QR3s944/hjJ1UK05asFEs/SmWeUbtS0cdA660sT4xHnRfals73FicOoz+uIucJCwn/SCM804j+wtM=" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMNsmP15vH8BVKo7bdvIiiEjiQboPGcRPqJK0+bH4jKD" + ]; + + + fileSystems = { + "/" = { + fsType = "ext4"; + device = "/dev/disk/by-id/test"; + options = [ "mode=0755" ]; + }; + }; + + boot = { + loader.grub.enable = false; + initrd.systemd.enable = true; + }; + + virtualisation.vmVariant = { + # following configuration is added only when building VM with build-vm + virtualisation = { + memorySize = 2048; # Use 2048MiB memory. + cores = 4; + }; + }; + + /* + services.loki = { + enable = true; + configuration = { + server.http_listen_port = 3030; + auth_enabled = false; + analytics.reporting_enabled = false; + + ingester = { + lifecycler = { + address = "127.0.0.1"; + ring = { + kvstore = { + store = "inmemory"; + }; + replication_factor = 1; + }; + }; + chunk_idle_period = "1h"; + max_chunk_age = "1h"; + chunk_target_size = 999999; + chunk_retain_period = "30s"; + }; + + schema_config = { + configs = [ + { + from = "2024-04-25"; + store = "tsdb"; + object_store = "filesystem"; + schema = "v13"; + index = { + prefix = "index_"; + period = "24h"; + }; + } + ]; + }; + + storage_config = { + tsdb_shipper = { + active_index_directory = "/var/lib/loki/tsdb-shipper-active"; + cache_location = "/var/lib/loki/tsdb-shipper-cache"; + cache_ttl = "24h"; + }; + + filesystem = { + directory = "/var/lib/loki/chunks"; + }; + }; + + limits_config = { + reject_old_samples = true; + reject_old_samples_max_age = "168h"; + volume_enabled = true; + }; + + + table_manager = { + retention_deletes_enabled = false; + retention_period = "0s"; + }; + + compactor = { + working_directory = "/var/lib/loki"; + compactor_ring = { + kvstore = { + store = "inmemory"; + }; + }; + }; + }; + }; + + services.promtail = { + enable = true; + configuration = { + server = { + http_listen_port = 3031; + grpc_listen_port = 0; + }; + clients = [ + { + url = "http://127.0.0.1:${toString config.services.loki.configuration.server.http_listen_port}/loki/api/v1/push"; + } + ]; + scrape_configs = [{ + job_name = "journal"; + journal = { + max_age = "12h"; + labels = { + job = "systemd-journal"; + }; + }; + relabel_configs = [ + { + source_labels = [ "__journal__systemd_unit" ]; + target_label = "systemd_unit"; + } + { + source_labels = [ "__journal__hostname" ]; + target_label = "nodename"; + } + { + source_labels = [ "__journal_container_id" ]; + target_label = "container_id"; + } + ]; + }]; + }; + # extraFlags + }; + */ +}