src/utils/eif_signer.rs (384 lines of code) (raw):
use crate::defs::{EifHeader, EifSectionHeader, EifSectionType, PcrInfo, PcrSignature};
use crate::utils::eif_reader::EifReader;
use crate::utils::get_pcrs;
use aws_config::BehaviorVersion;
use aws_nitro_enclaves_cose::{
crypto::kms::KmsKey, crypto::Openssl, header_map::HeaderMap, CoseSign1,
};
use aws_sdk_kms::client::Client;
use aws_types::region::Region;
use openssl::pkey::PKey;
use regex::Regex;
use serde_cbor::to_vec;
use sha2::{Digest, Sha384};
use std::collections::BTreeMap;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::mem::size_of;
use std::sync::Arc;
use tokio::runtime::Runtime;
// Signing key for eif images
pub enum SignKey {
// Local private key
LocalPrivateKey(Vec<u8>),
// KMS signer implementation from Cose library
KmsKey(Arc<KmsKey>),
}
// Signing key details
#[derive(Clone, Debug)]
enum SignKeyInfo {
// Local private key file path
LocalPrivateKeyInfo { path: std::path::PathBuf },
// KMS key details
KmsKeyInfo { id: String, region: Option<String> },
}
impl SignKeyInfo {
pub fn new(key_location: &str) -> Result<Self, String> {
match parse_kms_arn(key_location) {
Some((region, key_id)) => Ok(SignKeyInfo::KmsKeyInfo {
id: key_id,
region: Some(region),
}),
None => Ok(SignKeyInfo::LocalPrivateKeyInfo {
path: key_location.into(),
}),
}
}
}
fn parse_kms_arn(s: &str) -> Option<(String, String)> {
// Matches KMS key ARNs in the format:
// arn:partition:kms:region:account-id:key[/|:]key-id where:
// - partition is: aws, aws-cn, or aws-us-gov
// - region is captured: letters, numbers, hyphens
// - account-id: exactly 12 digits
// - key-id is captured: letters, numbers, hyphens
let re = Regex::new(
r"^arn:(?:aws|aws-cn|aws-us-gov):kms:([a-z0-9-]+):\d{12}:key[:/]([a-zA-Z0-9-]+)$",
)
.expect("Regular expression for parsing ARNs must be valid");
re.captures(s).map(|caps| {
// Safe to use index access since we know the pattern has exactly 2 capture groups
(caps[1].to_string(), caps[2].to_string())
})
}
// Full signining key data
pub struct SignKeyData {
// x509 certificate
pub cert: Vec<u8>,
// Signing key itself
pub key: SignKey,
}
impl SignKeyData {
pub fn new(key_location: &str, certificate: &std::path::Path) -> Result<Self, String> {
let key_info = SignKeyInfo::new(key_location)?;
let mut cert_file = File::open(certificate)
.map_err(|err| format!("Could not open the certificate file: {:?}", err))?;
let mut cert = Vec::new();
cert_file
.read_to_end(&mut cert)
.map_err(|err| format!("Could not read the certificate file: {:?}", err))?;
let key = match &key_info {
SignKeyInfo::LocalPrivateKeyInfo { path } => {
let mut key_file = File::open(path)
.map_err(|err| format!("Could not open the key file: {:?}", err))?;
let mut key_data = Vec::new();
key_file
.read_to_end(&mut key_data)
.map_err(|err| format!("Could not read the key file: {:?}", err))?;
SignKey::LocalPrivateKey(key_data)
}
SignKeyInfo::KmsKeyInfo { id, region } => {
// Method `KmsKey::new_with_public_key` must be called from a thread being run
// by Tokio runtime, or from a thread with an active `EnterGuard`.
let act = async {
let mut config_loader = aws_config::defaults(BehaviorVersion::latest());
if let Some(region_id) = region {
config_loader = config_loader.region(Region::new(region_id.clone()));
}
let sdk_config = config_loader.load().await;
if sdk_config.region().is_none() {
return Err("AWS region for KMS is not specified".to_string());
}
let id_copy = id.clone();
tokio::task::spawn_blocking(move || {
let client = Client::new(&sdk_config);
KmsKey::new_with_public_key(client, id_copy, None)
.map_err(|e| e.to_string())
})
.await
.unwrap()
};
let runtime = Runtime::new().map_err(|e| e.to_string())?;
let key = runtime.block_on(act)?;
SignKey::KmsKey(Arc::new(key))
}
};
Ok(SignKeyData { cert, key })
}
}
pub struct EifSigner {
key_data: SignKeyData,
}
impl EifSigner {
pub fn new(sign_key: Option<SignKeyData>) -> Option<Self> {
sign_key.map(|key| EifSigner { key_data: key })
}
pub fn sign(&self, payload: &[u8]) -> Result<PcrSignature, String> {
let cose_sign = match &self.key_data.key {
SignKey::LocalPrivateKey(key) => {
let pkey = PKey::private_key_from_pem(key).map_err(|e| {
format!("Failed to deserialize PEM-formatted private key: {}", e)
})?;
CoseSign1::new::<Openssl>(payload, &HeaderMap::new(), &pkey)
.map_err(|e| format!("Failed to create CoseSign1 with local key: {}", e))?
}
SignKey::KmsKey(key) => {
let arc_key = key.clone();
let runtime =
Runtime::new().map_err(|e| format!("Failed to create Tokio runtime: {}", e))?;
runtime.block_on(async move {
let payload_copy = Vec::from(payload);
tokio::task::spawn_blocking(move || {
CoseSign1::new::<Openssl>(&payload_copy, &HeaderMap::new(), &*arc_key)
.map_err(|e| format!("Failed to create CoseSign1 with KMS key: {}", e))
})
.await
.map_err(|e| format!("Task join error: {}", e))?
})?
}
};
let signature = cose_sign
.as_bytes(false)
.map_err(|e| format!("Failed to get signature bytes: {}", e))?;
Ok(PcrSignature {
signing_certificate: self.key_data.cert.clone(),
signature,
})
}
/// Generate the signature of a certain PCR.
fn generate_pcr_signature(
&self,
register_index: i32,
register_value: Vec<u8>,
) -> Result<PcrSignature, String> {
let pcr_info = PcrInfo::new(register_index, register_value);
let payload = to_vec(&pcr_info).expect("Could not serialize PCR info");
self.sign(payload.as_slice())
}
/// Generate the signature of the EIF.
/// eif_signature = [pcr0_signature]
pub fn generate_eif_signature(
&self,
measurements: &BTreeMap<String, String>,
) -> Result<Vec<u8>, String> {
let pcr0_index = 0;
let pcr0_value = hex::decode(
measurements
.get("PCR0")
.ok_or_else(|| "PCR0 measurement not found".to_string())?,
)
.map_err(|e| format!("Failed to decode PCR0 hex value: {}", e))?;
let pcr0_signature = self.generate_pcr_signature(pcr0_index, pcr0_value)?;
let eif_signature = vec![pcr0_signature];
to_vec(&eif_signature).map_err(|e| format!("Failed to serialize signature: {}", e))
}
pub fn get_cert_der(&self) -> Result<Vec<u8>, String> {
let cert = openssl::x509::X509::from_pem(&self.key_data.cert)
.map_err(|e| format!("Failed to parse PEM certificate: {}", e))?;
cert.to_der()
.map_err(|e| format!("Failed to convert certificate to DER format: {}", e))
}
/// Writes the provided pcr signature to an existing EIF
pub fn write_signature(
&self,
eif_path: &str,
serialized_signature: Vec<u8>,
is_signed: bool,
) -> Result<(), String> {
let mut eif_file = OpenOptions::new()
.read(true)
.write(true)
.open(eif_path)
.map_err(|e| format!("Failed to open file: {:?}", e))?;
let new_signature_size = serialized_signature.len() as u64;
let mut header = self.read_and_parse_header(&mut eif_file)?;
let signature_section = EifSectionHeader {
section_type: EifSectionType::EifSectionSignature,
flags: 0,
section_size: new_signature_size,
};
// Determine where to write the signature
let (signature_offset, section_id, old_signature_size) = if is_signed {
self.find_existing_signature(&mut eif_file, &header)?
} else {
let file_len = eif_file
.metadata()
.map_err(|e| format!("Failed to get file metadata: {:?}", e))?
.len();
(file_len, header.num_sections as usize, 0)
};
let section_header_size = EifSectionHeader::size() as u64;
let old_section_end = signature_offset + section_header_size + old_signature_size;
let new_section_end = signature_offset + section_header_size + new_signature_size;
let mut remaining_data = Vec::new();
if is_signed {
// Read all data after the old signature section
eif_file
.seek(SeekFrom::Start(old_section_end))
.and_then(|_| eif_file.read_to_end(&mut remaining_data))
.map_err(|e| format!("Failed to read remaining data: {:?}", e))?;
// Calculate the shift amount (positive if expanding, negative if shrinking)
let shift_amount = (new_section_end as i64) - (old_section_end as i64);
// Update offsets in the header for all sections after the signature
for i in (section_id + 1)..header.num_sections as usize {
header.section_offsets[i] =
(header.section_offsets[i] as i64 + shift_amount) as u64;
}
} else {
// For new signatures, just append to the end
header.section_offsets[section_id] = signature_offset;
header.section_sizes[section_id] = new_signature_size;
header.num_sections += 1;
}
// Write updated header
eif_file
.seek(SeekFrom::Start(0))
.and_then(|_| eif_file.write_all(&header.to_be_bytes()))
.map_err(|e| format!("Failed to write header: {:?}", e))?;
// Write signature section
eif_file
.seek(SeekFrom::Start(signature_offset))
.and_then(|_| eif_file.write_all(&signature_section.to_be_bytes()))
.and_then(|_| eif_file.write_all(&serialized_signature))
.map_err(|e| format!("Failed to write signature: {:?}", e))?;
// Write the remaining data at the new position
eif_file
.write_all(&remaining_data)
.map_err(|e| format!("Failed to write remaining data: {:?}", e))?;
// Set the new file length
let new_file_size = new_section_end + remaining_data.len() as u64;
eif_file.set_len(new_file_size).map_err(|e| {
format!(
"Failed to set new file length after writing signature: {:?}",
e
)
})
}
fn read_and_parse_header(&self, file: &mut File) -> Result<EifHeader, String> {
let mut header_buf = vec![0u8; EifHeader::size()];
file.read_exact(&mut header_buf)
.map_err(|e| format!("Error while reading EIF header: {:?}", e))?;
EifHeader::from_be_bytes(&header_buf).map_err(|e| format!("Error parsing header: {:?}", e))
}
fn find_existing_signature(
&self,
eif_file: &mut File,
header: &EifHeader,
) -> Result<(u64, usize, u64), String> {
for i in 0..header.num_sections as usize {
let offset = header.section_offsets[i];
let mut section_header_buf = vec![0u8; EifSectionHeader::size()];
eif_file
.seek(SeekFrom::Start(offset))
.map_err(|e| format!("Failed to seek: {:?}", e))?;
eif_file
.read_exact(&mut section_header_buf)
.map_err(|e| format!("Failed to read section header: {:?}", e))?;
let section_header = EifSectionHeader::from_be_bytes(§ion_header_buf)
.map_err(|e| format!("Failed to parse section header: {:?}", e))?;
if section_header.section_type == EifSectionType::EifSectionSignature {
return Ok((offset, i, section_header.section_size));
}
}
Err("Signature section not found".to_string())
}
/// Generates the signature based on the selected method and writes it to the EIF
pub fn sign_image(&self, eif_path: &str) -> Result<(), String> {
// Read PCRs and check if EIF already has a signature
let mut eif_reader = EifReader::from_eif(eif_path.into())?;
let has_signature = eif_reader.signature_section.is_some();
let measurements = get_pcrs(
&mut eif_reader.image_hasher,
&mut eif_reader.bootstrap_hasher,
&mut eif_reader.app_hasher,
&mut eif_reader.cert_hasher,
Sha384::new(),
has_signature,
)?;
let signature = self.generate_eif_signature(&measurements)?;
self.write_signature(eif_path, signature, has_signature)
.map_err(|e| format!("Failed to write signature to EIF: {}", e))?;
// Update CRC of the EIF
self.update_crc(eif_path)
}
pub fn update_crc(&self, eif_path: &str) -> Result<(), String> {
// Create a new instance of Reader to calculate the actual CRC
let eif_reader = EifReader::from_eif(eif_path.into())?;
let mut eif_file = OpenOptions::new()
.read(true)
.write(true)
.open(eif_path)
.map_err(|err| format!("Could not open the EIF: {:?}", err))?;
let len_without_crc = EifHeader::size() - size_of::<u32>();
eif_file
.seek(SeekFrom::Start(len_without_crc as u64))
.map_err(|err| format!("Could not seek in the EIF: {:?}", err))?;
eif_file
.write_all(&eif_reader.eif_crc.to_be_bytes())
.map_err(|err| format!("Failed to write checksum: {:?}", err))
}
}
#[cfg(test)]
mod arn_tests {
use super::parse_kms_arn;
#[test]
fn test_valid_kms_arns() {
// Test cases with expected captures: (arn, region, key_id)
let test_cases = vec![
(
"arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
"us-east-1",
"1234abcd-12ab-34cd-56ef-1234567890ab",
),
(
"arn:aws:kms:us-east-1:123456789012:key:1234abcd-12ab-34cd-56ef-1234567890ab",
"us-east-1",
"1234abcd-12ab-34cd-56ef-1234567890ab",
),
(
"arn:aws-cn:kms:cn-north-1:123456789012:key/abcd1234",
"cn-north-1",
"abcd1234",
),
(
"arn:aws-us-gov:kms:us-gov-west-1:123456789012:key:5678efgh",
"us-gov-west-1",
"5678efgh",
),
];
for (arn, expected_region, expected_key_id) in test_cases {
let (captured_region, captured_key_id) =
parse_kms_arn(&arn).expect("Should match valid ARN");
assert_eq!(captured_region, expected_region);
assert_eq!(captured_key_id, expected_key_id);
}
}
#[test]
fn test_invalid_kms_arns() {
let invalid_arns = vec![
// Invalid partition
"arn:invalid:kms:us-east-1:123456789012:key/abcd1234",
// Missing region
"arn:aws:kms::123456789012:key/abcd1234",
// Invalid account ID (too short)
"arn:aws:kms:us-east-1:12345678901:key/abcd1234",
// Invalid account ID (too long)
"arn:aws:kms:us-east-1:1234567890123:key/abcd1234",
// Invalid account ID (non-numeric)
"arn:aws:kms:us-east-1:12345678901a:key/abcd1234",
// Wrong service
"arn:aws:s3:us-east-1:123456789012:key/abcd1234",
// Invalid resource type
"arn:aws:kms:us-east-1:123456789012:alias/abcd1234",
// Invalid key ID format
"arn:aws:kms:us-east-1:123456789012:key/abc@1234",
// Missing key ID
"arn:aws:kms:us-east-1:123456789012:key/",
"arn:aws:kms:us-east-1:123456789012:key:",
// Invalid separator
"arn:aws:kms:us-east-1:123456789012:key-abcd1234",
// Empty string
"",
];
for arn in invalid_arns {
assert!(
parse_kms_arn(arn).is_none(),
"ARN should not match: {}",
arn
);
}
}
#[test]
fn test_region_formats() {
let valid_regions = vec![
"us-east-1",
"us-west-2",
"eu-central-1",
"ap-southeast-2",
"cn-north-1",
"us-gov-west-1",
];
for region in valid_regions {
let arn = format!("arn:aws:kms:{}:123456789012:key/abcd1234", region);
let (captured_region, _) = parse_kms_arn(&arn).expect("Should match valid region");
assert_eq!(captured_region, region);
}
}
#[test]
fn test_key_id_formats() {
let valid_key_ids = vec![
"1234abcd-12ab-34cd-56ef-1234567890ab", // UUID format
"abcd1234", // Short format
"12345678-1234-1234-1234-123456789012", // Another UUID format
"a1b2c3d4-e5f6", // Partial UUID format
];
for key_id in valid_key_ids {
let arn = format!("arn:aws:kms:us-east-1:123456789012:key/{}", key_id);
let (_, captured_id) = parse_kms_arn(&arn).expect("Should match valid key ID");
assert_eq!(captured_id, key_id);
}
}
}