diff --git a/Cargo.lock b/Cargo.lock index 85888e81..884172c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1891,7 +1891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -3286,7 +3286,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/src/common/commands_parser.rs b/src/common/commands_parser.rs index e6d18134..be2def95 100644 --- a/src/common/commands_parser.rs +++ b/src/common/commands_parser.rs @@ -259,6 +259,32 @@ impl PcrArgs { } } +/// The arguments used by `sign-eif` command +#[derive(Debug, Clone)] +pub struct SignEifArgs { + /// Path to the EIF file needed for signing + pub eif_path: String, + /// The path to the signing certificate for signed enclaves. + pub signing_certificate: Option, + /// ARN of the KMS key or path to the local private key for signed enclaves. + pub private_key: Option, +} + +impl SignEifArgs { + /// Construct a new `SignEifArgs` instance from the given command-line arguments. + pub fn new_with(args: &ArgMatches) -> NitroCliResult { + let signing_certificate = parse_signing_certificate(args); + let private_key = parse_private_key(args); + + Ok(SignEifArgs { + eif_path: parse_eif_path(args) + .map_err(|e| e.add_subaction("Parse EIF path".to_string()))?, + signing_certificate, + private_key, + }) + } +} + /// Parse file path to hash from the command-line arguments. fn parse_file_path(args: &ArgMatches, val_name: &str) -> NitroCliResult { let path = args.get_one::(val_name).ok_or_else(|| { diff --git a/src/common/document_errors.rs b/src/common/document_errors.rs index 460e019f..07316063 100644 --- a/src/common/document_errors.rs +++ b/src/common/document_errors.rs @@ -68,6 +68,7 @@ lazy_static! { (NitroCliErrorEnum::HasherError, "E57"), (NitroCliErrorEnum::EnclaveNamingError, "E58"), (NitroCliErrorEnum::EIFSignatureCheckerError, "E59"), + (NitroCliErrorEnum::EIFSigningError, "E60"), ].iter().cloned().collect(); } @@ -333,6 +334,9 @@ pub fn get_detailed_info(error_code_str: String, additional_info: &[String]) -> "E59" => { ret.push_str("EIF signature checker error. Such error appears when validation of the signing certificate fails."); } + "E60" => { + ret.push_str("Signing error. Such error appears if incorrect key or certificate paths are provided, or when AWS credenrials need to be refreshed to use a KMS key."); + } _ => { ret.push_str(format!("No such error code {}", error_code_str).as_str()); } @@ -543,6 +547,9 @@ pub fn explain_error(error_code_str: String) { "E56" => { eprintln!("Logger error. Such error appears when attempting to initialize the underlying logging system fails."); } + "E60" => { + eprintln!("Signing error. Such error appears if incorrect key or certificate paths are provided, or when AWS credenrials need to be refreshed to use a KMS key."); + } _ => { eprintln!("No such error code {}", error_code_str); } diff --git a/src/common/mod.rs b/src/common/mod.rs index b329b95f..38674a97 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -179,6 +179,8 @@ pub enum NitroCliErrorEnum { EnclaveNamingError, /// Signature checker error EIFSignatureCheckerError, + /// Signing error + EIFSigningError, } impl Eq for NitroCliErrorEnum {} diff --git a/src/lib.rs b/src/lib.rs index 4a79364f..8aa86935 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,8 @@ pub mod utils; use aws_nitro_enclaves_image_format::defs::eif_hasher::EifHasher; use aws_nitro_enclaves_image_format::utils::eif_reader::EifReader; +use aws_nitro_enclaves_image_format::utils::eif_signer::EifSigner; +use aws_nitro_enclaves_image_format::utils::SignKeyData; use aws_nitro_enclaves_image_format::{generate_build_info, utils::get_pcrs}; use log::{debug, info}; use sha2::{Digest, Sha384}; @@ -25,9 +27,9 @@ use std::convert::TryFrom; use std::fs::{File, OpenOptions}; use std::io::{self, Read, Write}; use std::os::unix::net::UnixStream; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -use common::commands_parser::{BuildEnclavesArgs, EmptyArgs, RunEnclavesArgs}; +use common::commands_parser::{BuildEnclavesArgs, EmptyArgs, RunEnclavesArgs, SignEifArgs}; use common::json_output::{ EifDescribeInfo, EnclaveBuildInfo, EnclaveTerminateInfo, MetadataDescribeInfo, }; @@ -292,6 +294,62 @@ pub fn describe_eif(eif_path: String) -> NitroCliResult { Ok(info) } +/// Signs EIF with the given key and certificate. If EIF already has a signature, it will be replaced. +pub fn sign_eif(args: SignEifArgs) -> NitroCliResult<()> { + let sign_info = match (&args.private_key, &args.signing_certificate) { + (Some(key), Some(cert)) => SignKeyData::new(key, Path::new(&cert)).map_or_else( + |e| { + eprintln!("Could not read signing info: {:?}", e); + None + }, + Some, + ), + _ => None, + }; + + let signer = EifSigner::new(sign_info).ok_or_else(|| { + new_nitro_cli_failure!( + format!("Failed to create EifSigner"), + NitroCliErrorEnum::EIFSigningError + ) + })?; + + signer.sign_image(&args.eif_path).map_err(|e| { + new_nitro_cli_failure!( + format!("Failed to sign image: {}", e), + NitroCliErrorEnum::EIFSigningError + ) + })?; + + eprintln!("Enclave Image successfully signed."); + + let mut eif_reader = EifReader::from_eif(args.eif_path).map_err(|e| { + new_nitro_cli_failure!( + &format!("Failed to initialize EIF reader: {:?}", e), + NitroCliErrorEnum::EifParsingError + ) + })?; + eif_reader + .get_measurements() + .map_err(|e| { + new_nitro_cli_failure!( + &format!("Failed to get PCR values: {:?}", e), + NitroCliErrorEnum::EifParsingError + ) + }) + .and_then(|measurements| { + let info = EnclaveBuildInfo::new(measurements); + let printed_info = serde_json::to_string_pretty(&info).map_err(|err| { + new_nitro_cli_failure!( + &format!("Failed to display EnclaveBuild data: {:?}", err), + NitroCliErrorEnum::SerdeError + ) + })?; + println!("{}", printed_info); + Ok(()) + }) +} + /// Returns the value of the `NITRO_CLI_BLOBS` environment variable. /// /// This variable specifies where all the blobs necessary for building @@ -803,5 +861,26 @@ macro_rules! create_app { .required(true), ), ) + .subcommand( + Command::new("sign-eif") + .about("Sign EIF with the given key") + .arg( + Arg::new("eif-path") + .long("eif-path") + .help("Path pointing to a prebuilt Eif image") + ) + .arg( + Arg::new("signing-certificate") + .long("signing-certificate") + .help("Local path to developer's X509 signing certificate.") + .requires("private-key"), + ) + .arg( + Arg::new("private-key") + .long("private-key") + .help("KMS key ARN or local path to developer's Eliptic Curve private key.") + .requires("signing-certificate"), + ) + ) }; } diff --git a/src/main.rs b/src/main.rs index f4c8cfc8..eac080c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use std::os::unix::net::UnixStream; use nitro_cli::common::commands_parser::{ BuildEnclavesArgs, ConsoleArgs, DescribeEnclavesArgs, EmptyArgs, ExplainArgs, PcrArgs, - RunEnclavesArgs, TerminateEnclavesArgs, + RunEnclavesArgs, SignEifArgs, TerminateEnclavesArgs, }; use nitro_cli::common::document_errors::explain_error; use nitro_cli::common::json_output::{EnclaveDescribeInfo, EnclaveRunInfo, EnclaveTerminateInfo}; @@ -29,7 +29,7 @@ use nitro_cli::enclave_proc_comm::{ }; use nitro_cli::{ build_enclaves, console_enclaves, create_app, describe_eif, get_all_enclave_names, - get_file_pcr, new_enclave_name, new_nitro_cli_failure, terminate_all_enclaves, + get_file_pcr, new_enclave_name, new_nitro_cli_failure, sign_eif, terminate_all_enclaves, }; const RUN_ENCLAVE_STR: &str = "Run Enclave"; @@ -42,6 +42,7 @@ const ENCLAVE_CONSOLE_STR: &str = "Enclave Console"; const EXPLAIN_ERR_STR: &str = "Explain Error"; const NEW_NAME_STR: &str = "New Enclave Name"; const FILE_PCR_STR: &str = "File PCR"; +const SIGN_EIF_STR: &str = "Sign EIF"; /// *Nitro CLI* application entry point. fn main() { @@ -301,6 +302,20 @@ fn main() { .ok_or_exit_with_errno(None); explain_error(explain_args.error_code_str); } + Some(("sign-eif", args)) => { + let sign_args = SignEifArgs::new_with(args) + .map_err(|e| { + e.add_subaction("Failed to construct SignEIF arguments".to_string()) + .set_action(SIGN_EIF_STR.to_string()) + }) + .ok_or_exit_with_errno(None); + sign_eif(sign_args) + .map_err(|e| { + e.add_subaction("Failed to sign EIF".to_string()) + .set_action(SIGN_EIF_STR.to_string()) + }) + .ok_or_exit_with_errno(None); + } Some((&_, _)) | None => (), } } diff --git a/tests/test_nitro_cli_args.rs b/tests/test_nitro_cli_args.rs index 2e306138..c414ea2c 100644 --- a/tests/test_nitro_cli_args.rs +++ b/tests/test_nitro_cli_args.rs @@ -337,6 +337,53 @@ mod test_nitro_cli_args { assert!(app.try_get_matches_from(args).is_err()) } + #[test] + fn sign_enclave_correct_command() { + let app = create_app!(); + let args = vec![ + "nitro cli", + "sign-eif", + "--eif-path", + "image.eif", + "--signing-certificate", + "cert.pem", + "--private-key", + "key.pem", + ]; + + assert!(app.try_get_matches_from(args).is_ok()) + } + + #[test] + fn sign_enclave_missing_certificate() { + let app = create_app!(); + let args = vec![ + "nitro cli", + "sign-eif", + "--eif-path", + "image.eif", + "--private-key", + "key.pem", + ]; + + assert!(app.try_get_matches_from(args).is_err()) + } + + #[test] + fn sign_enclave_missing_key() { + let app = create_app!(); + let args = vec![ + "nitro cli", + "sign-eif", + "--eif-path", + "image.eif", + "--signing-certificate", + "cert.pem", + ]; + + assert!(app.try_get_matches_from(args).is_err()) + } + #[test] fn build_enclave_with_metadata_correct_command() { let app = create_app!(); diff --git a/tests/tests.rs b/tests/tests.rs index 380ea20d..55ac89df 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -6,7 +6,7 @@ #[cfg(test)] mod tests { use nitro_cli::common::commands_parser::{ - BuildEnclavesArgs, RunEnclavesArgs, TerminateEnclavesArgs, + BuildEnclavesArgs, RunEnclavesArgs, SignEifArgs, TerminateEnclavesArgs, }; use nitro_cli::common::json_output::EnclaveDescribeInfo; use nitro_cli::enclave_proc::commands::{describe_enclaves, run_enclaves, terminate_enclaves}; @@ -17,7 +17,7 @@ mod tests { use nitro_cli::utils::{Console, PcrType}; use nitro_cli::{ build_enclaves, build_from_docker, describe_eif, enclave_console, get_file_pcr, - new_enclave_name, + new_enclave_name, sign_eif, }; use nitro_cli::{CID_TO_CONSOLE_PORT_OFFSET, VMADDR_CID_HYPERVISOR}; use serde_json::json; @@ -365,6 +365,59 @@ mod tests { run_describe_terminate(args); } + #[test] + fn run_describe_terminate_separately_signed_enclave_image() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_str().unwrap(); + let eif_path = format!("{}/test.eif", dir_path); + let cert_path = format!("{}/cert.pem", dir_path); + let key_path = format!("{}/key.pem", dir_path); + generate_signing_cert_and_key(&cert_path, &key_path); + + setup_env(); + let build_args = BuildEnclavesArgs { + docker_uri: SAMPLE_DOCKER.to_string(), + docker_dir: None, + output: eif_path, + signing_certificate: None, + private_key: None, + img_name: None, + img_version: None, + metadata: None, + }; + + build_from_docker( + &build_args.docker_uri, + &build_args.docker_dir, + &build_args.output, + &build_args.signing_certificate, + &build_args.private_key, + &build_args.img_name, + &build_args.img_version, + &build_args.metadata, + ) + .expect("Docker build failed"); + + let sign_args = SignEifArgs { + eif_path: build_args.output.clone(), + signing_certificate: Some(cert_path), + private_key: Some(key_path), + }; + sign_eif(sign_args).expect("Sign EIF failed"); + + let args = RunEnclavesArgs { + enclave_cid: None, + eif_path: build_args.output, + cpu_ids: None, + cpu_count: Some(2), + memory_mib: 256, + debug_mode: true, + attach_console: false, + enclave_name: Some("testName".to_string()), + }; + run_describe_terminate(args); + } + #[test] fn run_describe_terminate_command_executer_docker_image() { let dir = tempdir().unwrap(); @@ -1029,6 +1082,121 @@ mod tests { assert!(eif_info.sign_check.unwrap()); } + #[test] + fn build_sign_decribe_simple_eif() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_str().unwrap(); + let eif_path = format!("{}/test.eif", dir_path); + let cert_path = format!("{}/cert.pem", dir_path); + let key_path = format!("{}/key.pem", dir_path); + generate_signing_cert_and_key(&cert_path, &key_path); + + setup_env(); + let args = BuildEnclavesArgs { + docker_uri: SAMPLE_DOCKER.to_string(), + docker_dir: None, + output: eif_path, + signing_certificate: None, + private_key: None, + img_name: None, + img_version: None, + metadata: None, + }; + + build_from_docker( + &args.docker_uri, + &args.docker_dir, + &args.output, + &args.signing_certificate, + &args.private_key, + &args.img_name, + &args.img_version, + &args.metadata, + ) + .expect("Docker build failed"); + + let eif_info = describe_eif(args.output.clone()).unwrap(); + + assert_eq!(eif_info.version, 4); + assert!(!eif_info.is_signed); + assert!(eif_info.cert_info.is_none()); + assert!(eif_info.crc_check); + assert!(eif_info.sign_check.is_none()); + + let sign_args = SignEifArgs { + eif_path: args.output.clone(), + signing_certificate: Some(cert_path), + private_key: Some(key_path), + }; + sign_eif(sign_args).expect("Sign EIF failed"); + let signed_eif_info = describe_eif(args.output).unwrap(); + + assert_eq!(signed_eif_info.version, 4); + assert!(signed_eif_info.is_signed); + assert!(signed_eif_info.cert_info.is_some()); + assert!(signed_eif_info.crc_check); + assert!(signed_eif_info.sign_check.unwrap()); + } + + #[test] + fn build_describe_signed_simple_eif_with_updated_signature() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_str().unwrap(); + let eif_path = format!("{}/test.eif", dir_path); + let cert_path = format!("{}/cert.pem", dir_path); + let key_path = format!("{}/key.pem", dir_path); + let cert_path2 = format!("{}/cert2.pem", dir_path); + let key_path2 = format!("{}/key2.pem", dir_path); + generate_signing_cert_and_key(&cert_path, &key_path); + generate_signing_cert_and_key(&cert_path2, &key_path2); + + setup_env(); + let args = BuildEnclavesArgs { + docker_uri: SAMPLE_DOCKER.to_string(), + docker_dir: None, + output: eif_path, + signing_certificate: Some(cert_path), + private_key: Some(key_path), + img_name: None, + img_version: None, + metadata: None, + }; + + build_from_docker( + &args.docker_uri, + &args.docker_dir, + &args.output, + &args.signing_certificate, + &args.private_key, + &args.img_name, + &args.img_version, + &args.metadata, + ) + .expect("Docker build failed"); + + let eif_info = describe_eif(args.output.clone()).unwrap(); + + assert_eq!(eif_info.version, 4); + assert!(eif_info.is_signed); + assert!(eif_info.cert_info.is_some()); + assert!(eif_info.crc_check); + assert!(eif_info.sign_check.unwrap()); + + let sign_args = SignEifArgs { + eif_path: args.output.clone(), + signing_certificate: Some(cert_path2), + private_key: Some(key_path2), + }; + sign_eif(sign_args).expect("Sign EIF failed"); + let signed_eif_info = describe_eif(args.output).unwrap(); + + assert_eq!(signed_eif_info.version, 4); + assert!(signed_eif_info.is_signed); + assert!(signed_eif_info.cert_info.is_some()); + assert!(signed_eif_info.crc_check); + assert!(signed_eif_info.sign_check.unwrap()); + } + #[test] fn get_certificate_pcr() { let dir = tempdir().unwrap(); @@ -1076,4 +1244,117 @@ mod tests { pcr.get(&"PCR8".to_string()).unwrap(), ); } + + #[test] + fn get_certificate_pcr_after_separate_signing() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_str().unwrap(); + let eif_path = format!("{}/test.eif", dir_path); + let cert_path = format!("{}/cert.pem", dir_path); + let key_path = format!("{}/key.pem", dir_path); + generate_signing_cert_and_key(&cert_path, &key_path); + + setup_env(); + let args = BuildEnclavesArgs { + docker_uri: SAMPLE_DOCKER.to_string(), + docker_dir: None, + output: eif_path, + signing_certificate: None, + private_key: None, + img_name: None, + img_version: None, + metadata: None, + }; + + build_from_docker( + &args.docker_uri, + &args.docker_dir, + &args.output, + &args.signing_certificate, + &args.private_key, + &args.img_name, + &args.img_version, + &args.metadata, + ) + .expect("Docker build failed"); + + let sign_args = SignEifArgs { + eif_path: args.output.clone(), + signing_certificate: Some(cert_path.clone()), + private_key: Some(key_path), + }; + sign_eif(sign_args).expect("Sign EIF failed"); + + // Describe EIF and get PCR8 + let eif_info = describe_eif(args.output).unwrap(); + // Hash signing certificate and verify that PCR8 is the same (identifying the certificate) + let pcr = get_file_pcr(cert_path, PcrType::SigningCertificate).unwrap(); + + assert_eq!( + eif_info + .build_info + .measurements + .get(&"PCR8".to_string()) + .unwrap(), + pcr.get(&"PCR8".to_string()).unwrap(), + ); + } + + #[test] + fn get_certificate_pcr_after_signature_update() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_str().unwrap(); + let eif_path = format!("{}/test.eif", dir_path); + let cert_path = format!("{}/cert.pem", dir_path); + let key_path = format!("{}/key.pem", dir_path); + let cert_path2 = format!("{}/cert2.pem", dir_path); + let key_path2 = format!("{}/key2.pem", dir_path); + generate_signing_cert_and_key(&cert_path, &key_path); + generate_signing_cert_and_key(&cert_path2, &key_path2); + + setup_env(); + let args = BuildEnclavesArgs { + docker_uri: SAMPLE_DOCKER.to_string(), + docker_dir: None, + output: eif_path, + signing_certificate: Some(cert_path), + private_key: Some(key_path), + img_name: None, + img_version: None, + metadata: None, + }; + + build_from_docker( + &args.docker_uri, + &args.docker_dir, + &args.output, + &args.signing_certificate, + &args.private_key, + &args.img_name, + &args.img_version, + &args.metadata, + ) + .expect("Docker build failed"); + + let sign_args = SignEifArgs { + eif_path: args.output.clone(), + signing_certificate: Some(cert_path2.clone()), + private_key: Some(key_path2), + }; + sign_eif(sign_args).expect("Sign EIF failed"); + + // Describe EIF and get PCR8 + let eif_info = describe_eif(args.output).unwrap(); + // Hash signing certificate and verify that PCR8 is the same (identifying the certificate) + let pcr = get_file_pcr(cert_path2, PcrType::SigningCertificate).unwrap(); + + assert_eq!( + eif_info + .build_info + .measurements + .get(&"PCR8".to_string()) + .unwrap(), + pcr.get(&"PCR8".to_string()).unwrap(), + ); + } }