Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sign EIFs separately #652

Merged
merged 2 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions src/common/commands_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// ARN of the KMS key or path to the local private key for signed enclaves.
pub private_key: Option<String>,
}

impl SignEifArgs {
/// Construct a new `SignEifArgs` instance from the given command-line arguments.
pub fn new_with(args: &ArgMatches) -> NitroCliResult<Self> {
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<String> {
let path = args.get_one::<String>(val_name).ok_or_else(|| {
Expand Down
7 changes: 7 additions & 0 deletions src/common/document_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ lazy_static! {
(NitroCliErrorEnum::HasherError, "E57"),
(NitroCliErrorEnum::EnclaveNamingError, "E58"),
(NitroCliErrorEnum::EIFSignatureCheckerError, "E59"),
(NitroCliErrorEnum::EIFSigningError, "E60"),
].iter().cloned().collect();
}

Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ pub enum NitroCliErrorEnum {
EnclaveNamingError,
/// Signature checker error
EIFSignatureCheckerError,
/// Signing error
EIFSigningError,
}

impl Eq for NitroCliErrorEnum {}
Expand Down
83 changes: 81 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
};
Expand Down Expand Up @@ -292,6 +294,62 @@ pub fn describe_eif(eif_path: String) -> NitroCliResult<EifDescribeInfo> {
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
Expand Down Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to retain the local signing option, this should be --signing-certificate-path for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How I see it, the parameter names shall be the same as the ones used when signing an image during build. Updating syntax for build-enclave is a breaking change unless we keep both the old parameter name and the new one, plus add a deprecation warning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote for consistency with what we already have

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we keep both the old parameter name and the new one, plus add a deprecation warning

Yes, this is what I meant. To clarify, --signing-certificate or --private-key should remain in place until the next major release of nitro-cli.

.long("signing-certificate")
.help("Local path to developer's X509 signing certificate.")
.requires("private-key"),
)
.arg(
Arg::new("private-key")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have concerns about including this feature in the new command. This approach to handling private keys doesn't follow security best practices. While I understand the desire to maintain consistency with the build-eif command, I recommend removing this feature from the sign-eif command entirely.

If there are compelling reasons to keep it, we should:

  • Add detailed explanation in the help text explaining the security implications
  • Implement warning messages to alert users that this is not the recommended approach for private key handling

CC: @eugkoira

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My vision is, we might have customers who build EIFs in one (insecure) environment, and then sign those EIFs separately in a secure environment with a local private key, similar to #639.

Copy link
Contributor

@eugkoira eugkoira Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get it, is the proposal to not have the private-key signing at all?
Why is it a bad practice?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We all know what could happen if a private key is leaked. :) Given the security risks associated with private key exposure, the users should be encouraged to use KMS as the recommended best practice. (Or using PKCS#11 in the future. This was also mentioned by @dwmw2 here.)

I believe there are two options:

  • Omit local private-key signing for sign-eif command and mark the argument in build-eif as deprecated
  • Keep private-key as an option for sign-eif but add warnings about security implications.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customers can still have a secure isolated environment for signing with a private key like in #639. Why should we restrict their possibilities?

We can specify in our documentation the recommended way but I don't understand the urge to remove it completely.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Customers can literally use Nitro Enclaves to receive the signing key from KMS and pass it as parameter here, right? I don't believe the tool should per se assume an insecure environment. To clarify environmental requirements and establish best practices is the task of our documentation.

Copy link

@dwmw2 dwmw2 Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing requires a private key. That's kind of fundamental. The question is where that private key is found.

The real problem here seems to be that we have two completely separate command line arguments to provide the private key. There's the --private-key argument, which makes sense. A user will reasonably expect to be able to pass that a filename, an RFC7512 PKCS#11 URI, and maybe other kinds of things like a TPM URI referencing keys in non-volatile storage (although that's kind of dead now, and mostly a user would expect to provide a PEM file with a TPM-wrapped key and have that Just Work too).

And then we have the --kms-key-id argument. Which probably should have just accepted an ARN for a KMS key, e.g. --private-key arn:aws:kms:us-west-2:111122223333:key/0987dcba-09fe-87dc-65ba-ab0987654321, from the start? If we're talking about cleaning up the user interface, I'd much prefer us to deprecate --kms-key-id and use only --private-key to tell the tooling which private key to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that having a single parameter which dispatches internally basing on the provided value is the best option in terms of UX, and good thing that --kms-key-id has only been introduced to mainline, there is no actual release which fixed that in stone.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good thing that --kms-key-id has only been introduced to mainline, there is no actual release which fixed that in stone.

Excellent. So let's fix that to use --private-key arn:aws:kms:… before ever releasing it.

.long("private-key")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to retain the local signing option, this should be --private-key-path for clarity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be inconsistent with build-eif experience and can bring more confusion then benefits

.help("KMS key ARN or local path to developer's Eliptic Curve private key.")
.requires("signing-certificate"),
)
)
};
}
19 changes: 17 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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";
Expand All @@ -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() {
Expand Down Expand Up @@ -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 => (),
}
}
47 changes: 47 additions & 0 deletions tests/test_nitro_cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();
Expand Down
Loading
Loading