tvc 0.7.0

CLI for Turnkey Verifiable Cloud
Documentation
//! Shared provisioning bundle and attestation verification helpers.

use anyhow::{anyhow, bail, Context};
use aws_nitro_enclaves_nsm_api::api::AttestationDoc;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use qos_core::protocol::services::boot::ManifestEnvelope;
use qos_core::protocol::QosHash;
use qos_p256::P256Public;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ProvisionBundle {
    attestation_document_cose_sign1_base64: String,
    manifest_envelope: ManifestEnvelope,
    fetched_at_unix_ms: u64,
    deployment_id: String,
    ephemeral_public_key_hex: String,
}

impl ProvisionBundle {
    pub(crate) fn new(
        deployment_id: String,
        attestation_document: &[u8],
        manifest_envelope: ManifestEnvelope,
        fetched_at_unix_ms: u64,
        ephemeral_public_key: &[u8],
    ) -> Self {
        Self {
            attestation_document_cose_sign1_base64: BASE64_STANDARD.encode(attestation_document),
            manifest_envelope,
            fetched_at_unix_ms,
            deployment_id,
            ephemeral_public_key_hex: hex::encode(ephemeral_public_key),
        }
    }

    pub(crate) fn manifest_envelope(&self) -> &ManifestEnvelope {
        &self.manifest_envelope
    }

    pub(crate) fn ephemeral_public_key(
        &self,
        dangerous_skip_verification: bool,
    ) -> anyhow::Result<P256Public> {
        self.ephemeral_public_key_with_validation_time(dangerous_skip_verification, None)
    }

    fn ephemeral_public_key_with_validation_time(
        &self,
        dangerous_skip_verification: bool,
        validation_time_override: Option<u64>,
    ) -> anyhow::Result<P256Public> {
        let bundled_public_key = decode_ephemeral_public_key_hex(&self.ephemeral_public_key_hex)?;

        if dangerous_skip_verification {
            return Ok(bundled_public_key);
        }

        let attestation_document = BASE64_STANDARD
            .decode(&self.attestation_document_cose_sign1_base64)
            .context("failed to decode attestation document in provision bundle")?;

        let attestation_doc = verify_provisioning_details(
            &attestation_document,
            &self.manifest_envelope,
            validation_time_override,
        )?;

        let attested_public_key = extract_ephemeral_public_key_bytes(
            attestation_doc
                .public_key
                .as_ref()
                .map(|public_key| public_key.as_ref()),
        )?;

        if attested_public_key != bundled_public_key.to_bytes() {
            bail!("provision bundle ephemeral public key does not match attestation document");
        }

        Ok(bundled_public_key)
    }
}

pub(crate) fn verify_provisioning_details(
    cose_sign1_der: &[u8],
    manifest_envelope: &ManifestEnvelope,
    validation_time_override: Option<u64>,
) -> anyhow::Result<AttestationDoc> {
    manifest_envelope
        .check_approvals()
        .context("failed to verify manifest approvals")?;

    let attestation_doc = qos_nsm::nitro::attestation_doc_from_der(
        cose_sign1_der,
        &qos_nsm::nitro::cert_from_pem(qos_nsm::nitro::AWS_ROOT_CERT_PEM)
            .context("failed to parse AWS Nitro root certificate")?,
        validation_time_secs(validation_time_override)?,
    )
    .context("failed to parse and verify attestation document")?;

    qos_nsm::nitro::verify_attestation_doc_against_user_input(
        &attestation_doc,
        &manifest_envelope.manifest.qos_hash(),
        &manifest_envelope.manifest.enclave.pcr0,
        &manifest_envelope.manifest.enclave.pcr1,
        &manifest_envelope.manifest.enclave.pcr2,
        &manifest_envelope.manifest.enclave.pcr3,
    )
    .context("attestation document did not match manifest expectations")?;

    Ok(attestation_doc)
}

pub(crate) fn extract_ephemeral_public_key_bytes(
    public_key: Option<&[u8]>,
) -> anyhow::Result<Vec<u8>> {
    let public_key =
        public_key.ok_or_else(|| anyhow!("attestation document missing ephemeral public key"))?;

    P256Public::from_bytes(public_key)
        .map(|ephemeral_key| ephemeral_key.to_bytes())
        .map_err(|err| anyhow!("invalid ephemeral public key: {err:?}"))
}

fn decode_ephemeral_public_key_hex(ephemeral_public_key_hex: &str) -> anyhow::Result<P256Public> {
    let public_key_bytes = hex::decode(ephemeral_public_key_hex.trim())
        .context("failed to decode ephemeral public key from provision bundle")?;

    P256Public::from_bytes(&public_key_bytes)
        .map_err(|err| anyhow!("invalid ephemeral public key in provision bundle: {err:?}"))
}

fn validation_time_secs(validation_time_override: Option<u64>) -> anyhow::Result<u64> {
    match validation_time_override {
        Some(time) => Ok(time),
        None => Ok(SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .context("system time before unix epoch")?
            .as_secs()),
    }
}

#[cfg(test)]
mod tests {
    use super::{extract_ephemeral_public_key_bytes, verify_provisioning_details, ProvisionBundle};
    use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
    use qos_core::protocol::services::boot::ManifestEnvelope;
    use qos_p256::P256Pair;
    use serde::Deserialize;
    use serde_json::json;

    #[derive(Debug, Deserialize)]
    struct ValidProvisioningDetailsFixture {
        validation_time_secs: u64,
        attestation_document_cose_sign1_base64: String,
        manifest_envelope: ManifestEnvelope,
    }

    fn valid_provisioning_details_fixture() -> ValidProvisioningDetailsFixture {
        serde_json::from_str(include_str!("../fixtures/valid_provisioning_details.json")).unwrap()
    }

    fn sample_manifest_envelope() -> ManifestEnvelope {
        serde_json::from_value(json!({
            "manifest": {
                "namespace": {
                    "name": "test-namespace",
                    "nonce": 7,
                    "quorumKey": "0102"
                },
                "pivot": {
                    "hash": "0000000000000000000000000000000000000000000000000000000000000000",
                    "restart": "Never",
                    "bridgeConfig": [],
                    "debugMode": false,
                    "args": ["--serve"]
                },
                "manifestSet": {
                    "threshold": 1,
                    "members": [{
                        "alias": "member-1",
                        "pubKey": "aabbcc"
                    }]
                },
                "shareSet": {
                    "threshold": 0,
                    "members": []
                },
                "enclave": {
                    "pcr0": "00",
                    "pcr1": "11",
                    "pcr2": "22",
                    "pcr3": "33",
                    "awsRootCertificate": "44",
                    "qosCommit": "test-commit"
                },
                "patchSet": {
                    "threshold": 0,
                    "members": []
                }
            },
            "manifestSetApprovals": [{
                "signature": "beef",
                "member": {
                    "alias": "member-1",
                    "pubKey": "aabbcc"
                }
            }],
            "shareSetApprovals": []
        }))
        .unwrap()
    }

    #[test]
    fn extract_ephemeral_public_key_bytes_requires_key() {
        let err = extract_ephemeral_public_key_bytes(None).unwrap_err();

        assert!(err
            .to_string()
            .contains("attestation document missing ephemeral public key"));
    }

    #[test]
    fn extract_ephemeral_public_key_bytes_rejects_malformed_key() {
        let err = extract_ephemeral_public_key_bytes(Some(&[1, 2, 3])).unwrap_err();

        assert!(err.to_string().contains("invalid ephemeral public key"));
    }

    #[test]
    fn provision_bundle_serializes_expected_fields() {
        let manifest_envelope = sample_manifest_envelope();
        let bundle = ProvisionBundle::new(
            "deploy-123".to_string(),
            &[1, 2, 3, 4],
            manifest_envelope.clone(),
            1_712_345_678_901,
            &[0x04, 0xab, 0xcd],
        );

        let value = serde_json::to_value(&bundle).unwrap();

        assert_eq!(
            value["attestationDocumentCoseSign1Base64"],
            json!(BASE64_STANDARD.encode([1, 2, 3, 4])),
        );
        assert_eq!(value["fetchedAtUnixMs"], json!(1_712_345_678_901_u64));
        assert_eq!(value["deploymentId"], json!("deploy-123"));
        assert_eq!(value["ephemeralPublicKeyHex"], json!("04abcd"));
        assert_eq!(
            value["manifestEnvelope"],
            serde_json::to_value(&manifest_envelope).unwrap()
        );
    }

    #[test]
    fn verify_provisioning_details_accepts_real_fixture() {
        let fixture = valid_provisioning_details_fixture();
        let attestation_document = BASE64_STANDARD
            .decode(&fixture.attestation_document_cose_sign1_base64)
            .unwrap();

        verify_provisioning_details(
            &attestation_document,
            &fixture.manifest_envelope,
            Some(fixture.validation_time_secs),
        )
        .unwrap();
    }

    #[test]
    fn verify_provisioning_details_rejects_real_fixture_with_missing_manifest_approval() {
        let fixture = valid_provisioning_details_fixture();
        let attestation_document = BASE64_STANDARD
            .decode(&fixture.attestation_document_cose_sign1_base64)
            .unwrap();
        let mut manifest_envelope = fixture.manifest_envelope;
        manifest_envelope.manifest_set_approvals.clear();

        assert!(verify_provisioning_details(
            &attestation_document,
            &manifest_envelope,
            Some(fixture.validation_time_secs),
        )
        .is_err());
    }

    #[test]
    fn safe_bundle_ephemeral_key_extraction_rejects_mismatched_bundle_key() {
        let fixture = valid_provisioning_details_fixture();
        let attestation_document = BASE64_STANDARD
            .decode(&fixture.attestation_document_cose_sign1_base64)
            .unwrap();
        let attestation_doc =
            qos_nsm::nitro::unsafe_attestation_doc_from_der(&attestation_document).unwrap();
        let valid_ephemeral_key = extract_ephemeral_public_key_bytes(
            attestation_doc
                .public_key
                .as_ref()
                .map(|public_key| public_key.as_ref()),
        )
        .unwrap();
        let mut bundle = ProvisionBundle::new(
            "deploy-123".to_string(),
            &attestation_document,
            fixture.manifest_envelope,
            1_712_345_678_901,
            &valid_ephemeral_key,
        );
        bundle.ephemeral_public_key_hex =
            hex::encode(P256Pair::generate().unwrap().public_key().to_bytes());

        let err = match bundle
            .ephemeral_public_key_with_validation_time(false, Some(fixture.validation_time_secs))
        {
            Ok(_) => panic!("mismatched bundle key should be rejected"),
            Err(err) => err,
        };

        assert!(err
            .to_string()
            .contains("does not match attestation document"));
    }

    #[test]
    fn skip_bundle_ephemeral_key_extraction_accepts_valid_bundle_key_without_attestation() {
        let expected_public_key = P256Pair::generate().unwrap().public_key();
        let bundle = ProvisionBundle {
            attestation_document_cose_sign1_base64: "not base64".to_string(),
            manifest_envelope: sample_manifest_envelope(),
            fetched_at_unix_ms: 1_712_345_678_901,
            deployment_id: "deploy-123".to_string(),
            ephemeral_public_key_hex: hex::encode(expected_public_key.to_bytes()),
        };

        let public_key = bundle.ephemeral_public_key(true).unwrap();

        assert_eq!(public_key.to_bytes(), expected_public_key.to_bytes());
    }
}