diff --git a/.github/workflows/build-solution.yml b/.github/workflows/build-solution.yml index 820ef7d2..1a67285a 100644 --- a/.github/workflows/build-solution.yml +++ b/.github/workflows/build-solution.yml @@ -49,7 +49,7 @@ jobs: - name: Setup cmake uses: jwlawson/actions-setup-cmake@v2 with: - cmake-version: "3.28" + cmake-version: "3.31" - name: Set LIBCLANG_PATH correctly run: | diff --git a/vsock-proxy/Cargo.lock b/vsock-proxy/Cargo.lock index 768afdfb..5e321fce 100644 --- a/vsock-proxy/Cargo.lock +++ b/vsock-proxy/Cargo.lock @@ -253,6 +253,20 @@ dependencies = [ "serde_cbor", ] +[[package]] +name = "aws-nitro-enclaves-nsm-api" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986ca4c3a91a67cadc5ae92b3dfe9de373c2b432bb2288eed90203343e9f47c4" +dependencies = [ + "libc", + "log 0.4.33", + "nix 0.31.3", + "serde", + "serde_bytes", + "serde_cbor", +] + [[package]] name = "b64-ct" version = "0.1.3" @@ -817,7 +831,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f80401fb67d9befdc2e98a2659d4d3e38e40618d325b80869df7bb2e6264a1" dependencies = [ - "aws-nitro-enclaves-nsm-api", + "aws-nitro-enclaves-nsm-api 0.2.1", "b64-ct", "em-client", "em-node-agent-client", @@ -890,17 +904,19 @@ dependencies = [ "api-model", "async-process", "async-trait", + "aws-nitro-enclaves-nsm-api 0.5.2", "base64 0.13.0", "chrono", "clap", "client", "const_format", - "em-app", "em-client", "em-node-agent-client", "enclaveos-encrypted-fs", "env_logger 0.7.1", + "ftx-cert-build", "futures 0.3.16", + "hex", "hmac", "hyper", "hyper-rustls", @@ -910,7 +926,6 @@ dependencies = [ "log 0.4.33", "mbedtls", "nix 0.15.0", - "openssl", "parent_lib", "pkix", "rand 0.8.5", @@ -919,6 +934,7 @@ dependencies = [ "salmiac-build-support", "sdkms", "serde", + "serde_bytes", "serde_cbor", "serde_json 1.0.2", "sha2", @@ -932,6 +948,7 @@ dependencies = [ "tun", "url 2.3.1", "uuid 0.7.4", + "vme-pkix", "walkdir", ] @@ -1556,7 +1573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] diff --git a/vsock-proxy/Cargo.toml b/vsock-proxy/Cargo.toml index f5b51513..0f071d74 100644 --- a/vsock-proxy/Cargo.toml +++ b/vsock-proxy/Cargo.toml @@ -7,7 +7,14 @@ members = [ ] [workspace.dependencies] -em-app = { version = "0.5.3" } +attestation-client = { git = "https://github.com/fortanix/attestation.git", package = "client" } +aws-nitro-enclaves-nsm-api = "0.5" em-client = { version = "4.0.2" } em-node-agent-client = { version = "1.0.0" } -attestation-client = { git = "https://github.com/fortanix/attestation.git", package = "client" } +ftx-cert-build = { git = "https://github.com/fortanix/ftx-cert-build.git", features = ["mbedtls_adapter"] } +hex = "0.4.3" +serde = "1.0.127" +serde_cbor = "0.11.2" +serde_bytes = "0.11" +serde_json = { git = "https://github.com/fortanix/serde-json.git", branch = "base64_bytes" } +vme-pkix = "0.1.0" diff --git a/vsock-proxy/enclave/Cargo.toml b/vsock-proxy/enclave/Cargo.toml index 11aa0056..23be9ebd 100644 --- a/vsock-proxy/enclave/Cargo.toml +++ b/vsock-proxy/enclave/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/fortanix/salmiac" [dependencies] attestation-client = { workspace = true } +aws-nitro-enclaves-nsm-api = { workspace = true } api-model = { path = "../../api-model", features = ["serde"] } async-process = "1.2.0" async-trait = "0.1.51" @@ -16,12 +17,13 @@ base64 = "0.13.0" chrono = "0.4" clap = "2.33" const_format = "0.2.32" -em-app = { workspace = true } em-client = { workspace = true } em-node-agent-client = { workspace = true } enclaveos-encrypted-fs = { path = "../../tools/enclaveos-encrypted-fs" } env_logger = "0.7" +ftx-cert-build = { workspace = true } futures = "0.3" +hex = { workspace = true } hmac = "0.12.1" hyper = "0.10" hyper-rustls = "0.6.2" @@ -30,15 +32,15 @@ ipnetwork = { version = "0.18.0", features = ["serde"] } log = "0.4" mbedtls = { version = "0.13.5", features = ["chrono"] } nix = "0.15.0" -openssl = "0.10" pkix = "0.2.6" rand = "0.8.5" rustls = "0.13" rtnetlink = "0.8.0" sdkms = { version = "0.3", default-features = false, features = ["hyper-native-tls"] } -serde = { version = "1.0.127", features = ["derive"] } -serde_cbor = "0.11.2" -serde_json = { git = "https://github.com/fortanix/serde-json.git", branch = "base64_bytes" } +serde = { workspace = true, features = ["derive"] } +serde_cbor = { workspace = true } +serde_bytes = { workspace = true } +serde_json = { workspace = true } sha2 = "0.10.9" shared = { path = "../shared" } sysinfo = "0.29.8" @@ -47,6 +49,7 @@ tokio-vsock = "0.3.1" tun = { version = "0.5.3", features = ["async"] } url = "2.3.1" uuid = "0.7.4" +vme-pkix = { workspace = true } [build-dependencies] mbedtls = { version = "0.13.5", features = ["rdrand", "std", "ssl"], default-features = false } diff --git a/vsock-proxy/enclave/src/certificate.rs b/vsock-proxy/enclave/src/certificate.rs index 99bc0463..6e3465f9 100644 --- a/vsock-proxy/enclave/src/certificate.rs +++ b/vsock-proxy/enclave/src/certificate.rs @@ -187,6 +187,7 @@ pub(crate) trait CSRApi { } pub(crate) struct EmAppCSRApi {} + impl CSRApi for EmAppCSRApi { fn get_remote_attestation_csr( &self, @@ -203,7 +204,7 @@ impl CSRApi for EmAppCSRApi { if common_name.is_empty() { subject = pkix::types::Name::from(vec![]); } else { - subject = em_app::common_name_to_subject(common_name); + subject = crate::utils::nitro::common_name_to_subject(common_name); } let alt_names = cert_config @@ -213,8 +214,7 @@ impl CSRApi for EmAppCSRApi { .map(|s| Cow::Owned(s)) .collect(); - em_app::get_remote_attestation_csr_subject( - "localhost", //this param is not used for now + crate::utils::nitro::get_remote_attestation_csr_subject( &subject, key, Some(alt_names), diff --git a/vsock-proxy/enclave/src/main.rs b/vsock-proxy/enclave/src/main.rs index a6140c81..d98ee30c 100644 --- a/vsock-proxy/enclave/src/main.rs +++ b/vsock-proxy/enclave/src/main.rs @@ -9,7 +9,6 @@ mod certificate; mod enclave; mod file_system; mod platform; -#[cfg(platform = "nitro")] mod utils; use std::path::Path; diff --git a/vsock-proxy/enclave/src/utils/mod.rs b/vsock-proxy/enclave/src/utils/mod.rs index f9faf2ff..9dab45ba 100644 --- a/vsock-proxy/enclave/src/utils/mod.rs +++ b/vsock-proxy/enclave/src/utils/mod.rs @@ -1 +1,3 @@ +#[cfg(platform = "nitro")] pub mod net; +pub(crate) mod nitro; diff --git a/vsock-proxy/enclave/src/utils/nitro.rs b/vsock-proxy/enclave/src/utils/nitro.rs new file mode 100644 index 00000000..242d2ba4 --- /dev/null +++ b/vsock-proxy/enclave/src/utils/nitro.rs @@ -0,0 +1,189 @@ +/* Copyright (c) Fortanix, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::borrow::Cow; + +use aws_nitro_enclaves_nsm_api::{ + api::{Request, Response}, + driver, +}; +use ftx_cert_build::{Csr, NameBuilder}; +use mbedtls::{ + pk::{Pk, Type as PkType}, + rng::{Random, Rdrand}, +}; +use pkix::{ + pem::{der_to_pem, PEM_CERTIFICATE_REQUEST}, + types::{Attribute, EcdsaX962, Name, RsaPkcs15, Sha256}, + ToDer, +}; +use sha2::{Digest, Sha256 as Sha256Digest}; + +pub(crate) fn common_name_to_subject(common_name: &str) -> Name { + NameBuilder::new().add_common_name(common_name).build_name() +} + +fn get_user_data(pub_key: &[u8], config_id: Option<&str>) -> Result<[u8; 64], String> { + let mut data = [0; 64]; + data[..32].copy_from_slice(&Sha256Digest::digest(pub_key)); + if let Some(id) = config_id { + let id = hex::decode(id).map_err(|err| err.to_string())?; + if id.len() != 32 { + return Err("config ID must be 32 bytes".into()); + } + let mut payload = [0; 65]; + payload[0] = 1; + payload[1..33].copy_from_slice(&data[..32]); + payload[33..].copy_from_slice(&id[..32]); + + // The payload is formed as follows in case of workflow report. + // First 32 bytes is a Sha256 of (Version + public key sha256 + config-id) + // Second 32 bytes part is the actual config-id. + data[..32].copy_from_slice(&Sha256Digest::digest(&payload)); + data[32..].copy_from_slice(&id[..32]); + } + // if non-workflow report then first 32 bytes is the hash of the public key, second 32 bytes are all 0. + + Ok(data) +} + +pub(crate) fn get_remote_attestation_csr_subject( + subject: &Name, + signer: &mut Pk, + alt_names: Option>>, + config_id: Option<&str>, +) -> Result { + let pub_key = signer.write_public_der_vec().map_err(|e| e.to_string())?; + let user_data = get_user_data(&pub_key, config_id)?; + let csr_pem = get_attestation_parameters_subject(signer, subject, &user_data, alt_names)?; + Ok(csr_pem) +} + +pub(crate) fn get_attestation_parameters_subject( + signer: &mut Pk, + subject: &Name, + user_data: &[u8; 64], + alt_names: Option>>, +) -> Result { + let document = get_attestation(user_data)?; + let csr_pem = get_csr(subject, signer, document, alt_names)?; + Ok(csr_pem) +} + +/// Request a Nitro attestation document bound to the supplied user data. +pub(crate) fn get_attestation(user_data: &[u8; 64]) -> Result, String> { + let mut nonce = [0; 16]; + Rdrand + .random(&mut nonce) + .map_err(|e| format!("Failed generating nonce: {}", e))?; + let nsm_fd = driver::nsm_init(); + let response = driver::nsm_process_request( + nsm_fd, + Request::Attestation { + public_key: None, + user_data: Some(serde_bytes::ByteBuf::from(user_data.to_vec())), + nonce: Some(serde_bytes::ByteBuf::from(nonce.to_vec())), + }, + ); + driver::nsm_exit(nsm_fd); + let document = match response { + Response::Attestation { document } => document, + Response::Error(e) => { + return Err(format!( + "Failed getting attestation from nsm_driver: {:?}", + e + )) + } + other => return Err(format!("Unknown response from nsm_driver: {:?}", other)), + }; + Ok(document) +} + +fn get_csr( + subject: &Name, + signer: &mut Pk, + document: Vec, + alt_names: Option>>, +) -> Result { + let attestation = document.to_der(); + let mut builder = Csr::mbedtls_crypto_builder() + .with_subject(subject.clone().into()) + .with_attributes(vec![Attribute { + oid: vme_pkix::oid::ATTESTATION_NITRO.clone(), + value: vec![attestation.into()], + }]) + .map_err(|e| e.to_string())?; + if let Some(names) = alt_names { + builder = builder.with_san_extension_from_strs(&names); + } + // Unlike em-app's Nitro CSR builder, which omitted basicConstraints, + // ftx-cert-build always requests this critical extension (CA=false by default). + let csr = match signer.pk_type() { + PkType::Rsa => builder + .with_self_signing_key(signer, RsaPkcs15(Sha256)) + .and_then(|builder| builder.build_csr()), + PkType::Eckey | PkType::Ecdsa => builder + .with_self_signing_key(signer, EcdsaX962(Sha256)) + .and_then(|builder| builder.build_csr()), + other => return Err(format!("Invalid key type: {:?}", other)), + } + .map_err(|e| format!("Failed building CSR: {}", e))?; + Ok(der_to_pem(&*csr, PEM_CERTIFICATE_REQUEST)) +} + +#[cfg(test)] +mod tests { + use super::*; + use pkix::yasna; + + #[test] + fn report_data_binds_key_and_configuration() { + let key = b"public key"; + let plain = get_user_data(key, None).unwrap(); + assert_eq!(&plain[..32], Sha256Digest::digest(key).as_slice()); + assert_eq!(&plain[32..], &[0; 32]); + + let id = "ab".repeat(32); + let workflow = get_user_data(key, Some(&id)).unwrap(); + let mut payload = vec![1]; + payload.extend_from_slice(&plain[..32]); + payload.extend_from_slice(&[0xab; 32]); + assert_eq!(&workflow[..32], Sha256Digest::digest(&payload).as_slice()); + assert_eq!(&workflow[32..], &[0xab; 32]); + assert_eq!(workflow, get_user_data(key, Some(&id)).unwrap()); + for invalid in ["", "ab", &"gg".repeat(32), &"é".repeat(32)] { + assert!(get_user_data(key, Some(invalid)).is_err()); + } + } + + #[test] + fn csr_embeds_nitro_attestation_document() { + use pkix::{pem::pem_to_der, pkcs10::DerCertificationRequest, FromDer}; + + let mut key = Pk::generate_rsa(&mut Rdrand, 2048, 65537).unwrap(); + let document = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let pem = get_csr( + &common_name_to_subject("example.com"), + &mut key, + document.clone(), + None, + ) + .unwrap(); + let der = pem_to_der(&pem, Some(PEM_CERTIFICATE_REQUEST)).unwrap(); + let csr = DerCertificationRequest::from_der(&der).unwrap(); + let attributes = csr + .reqinfo + .attributes + .iter() + .filter(|attribute| attribute.oid == *vme_pkix::oid::ATTESTATION_NITRO) + .collect::>(); + assert_eq!(attributes.len(), 1); + assert_eq!(attributes[0].value.len(), 1); + let decoded = + yasna::parse_der(attributes[0].value[0].as_ref(), |r| r.read_bytes()).unwrap(); + assert_eq!(decoded, document); + } +} diff --git a/vsock-proxy/shared/Cargo.toml b/vsock-proxy/shared/Cargo.toml index f2acd219..023175b5 100644 --- a/vsock-proxy/shared/Cargo.toml +++ b/vsock-proxy/shared/Cargo.toml @@ -16,8 +16,8 @@ futures = "0.3" ipnetwork = { version = "0.18.0", features = ["serde"] } log = "0.4" rtnetlink = "0.8.0" -serde = { version = "1.0.127", features = ["derive"] } -serde_cbor = "0.11.2" +serde = { workspace = true, features = ["derive"] } +serde_cbor = { workspace = true } threadpool = "1.7.1" tokio = { version = "1.0.1", features = ["macros", "rt", "rt-multi-thread", "io-util", "net"] } tokio-vsock = "0.3.1"