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

Implement AWS KMS cryptography (includes #21) #23

Merged
merged 4 commits into from Aug 8, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: [1.46.0, stable, nightly]
rust: [1.58.1, stable, nightly]
key_feature_set:
- key_openssl_pkey
steps:
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Expand Up @@ -8,6 +8,7 @@ keywords = ["COSE"]
categories = ["cryptography"]
repository = "https://github.com/awslabs/aws-nitro-enclaves-cose"
description = "This library aims to provide a safe Rust implementation of COSE, with COSE Sign1 currently implemented."
rust-version = "1.58"

[dependencies]
serde_cbor = { version="0.11", features = ["tags"] }
Expand All @@ -16,15 +17,20 @@ serde_bytes = { version = "0.11", features = ["std"] }
serde_with = { version = "1.5", default_features = false }
openssl = { version = "0.10", optional = true }
tss-esapi = { version = "6.1", optional = true }
aws-config = { version = "0.46", optional = true }
aws-sdk-kms = { version = "0.16", optional = true }
tokio = { version = "1.20", features = ["rt", "macros"], optional = true }

[dependencies.serde]
version = "1.0"
features = ["derive"]

[dev-dependencies]
hex = "0.4"
tokio = { version = "1.20", features = ["macros"] }

[features]
default = ["key_openssl_pkey"]
key_openssl_pkey = ["openssl"]
key_tpm = ["tss-esapi", "openssl"]
key_kms = ["aws-config", "aws-sdk-kms", "tokio", "key_openssl_pkey"]
3 changes: 2 additions & 1 deletion README.md
@@ -1,11 +1,12 @@
[![status]][actions] [![version]][crates.io] [![docs]][docs.rs]
[![status]][actions] [![version]][crates.io] [![docs]][docs.rs] ![msrv]

[status]: https://img.shields.io/github/workflow/status/awslabs/aws-nitro-enclaves-cose/CI/master
[actions]: https://github.com/awslabs/aws-nitro-enclaves-cose/actions?query=branch%3Amain
[version]: https://img.shields.io/crates/v/aws-nitro-enclaves-cose.svg
[crates.io]: https://crates.io/crates/aws-nitro-enclaves-cose
[docs]: https://img.shields.io/docsrs/aws-nitro-enclaves-cose
[docs.rs]: https://docs.rs/aws-nitro-enclaves-cose
[msrv]: https://img.shields.io/badge/MSRV-1.58.1-blue

## COSE for AWS Nitro Enclaves

Expand Down
232 changes: 232 additions & 0 deletions src/crypto/kms.rs
@@ -0,0 +1,232 @@
//! KMS implementation for cryptography

use openssl::{
bn::BigNum,
ecdsa::EcdsaSig,
pkey::{PKey, Public},
};

use aws_sdk_kms::{
error::{VerifyError, VerifyErrorKind},
model::{MessageType, SigningAlgorithmSpec},
types::Blob,
types::SdkError,
Client,
};

use crate::{
crypto::openssl_pkey::ec_curve_to_parameters,
crypto::{MessageDigest, SignatureAlgorithm, SigningPrivateKey, SigningPublicKey},
error::CoseError,
};

use tokio::runtime::Handle;

/// A reference to an AWS KMS key and client
pub struct KmsKey {
client: Client,
key_id: String,

sig_alg: SignatureAlgorithm,

public_key: Option<PKey<Public>>,
}

impl KmsKey {
/// Create a new KmsKey, using the specified client and key_id.
///
/// The sig_alg needs to be valid for the specified key.
/// This version will use the KMS Verify call to verify signatures.
///
/// AWS Permissions required on the specified key:
/// - Sign (for creating new signatures)
/// - Verify (for verifying existing signatures)
pub fn new(
client: Client,
key_id: String,
sig_alg: SignatureAlgorithm,
) -> Result<Self, CoseError> {
Ok(KmsKey {
client,
key_id,
sig_alg,
public_key: None,
})
}

/// Create a new KmsKey, using the specified client and key_id.
///
/// The sig_alg needs to be valid for the specified key.
/// This version will use local signature verification.
/// If no public key is passed in, the key will be retrieved with GetPublicKey.
///
/// AWS Permissions required on the specified key:
/// - Sign (for creating new signatures)
/// - GetPublicKey (to get the public key if it wasn't passed in)
#[cfg(feature = "key_openssl_pkey")]
pub fn new_with_public_key(
client: Client,
key_id: String,
public_key: Option<PKey<Public>>,
) -> Result<Self, CoseError> {
let handle = Handle::current();
let public_key = match public_key {
Some(key) => key,
None => {
// Retrieve public key from AWS
let request = client.get_public_key().key_id(key_id.clone()).send();

let public_key = handle
.block_on(request)
.map_err(CoseError::AwsGetPublicKeyError)?
.public_key
.ok_or_else(|| {
CoseError::UnsupportedError("No public key returned".to_string())
})?;

PKey::public_key_from_der(public_key.as_ref())
.map_err(|e| CoseError::SignatureError(Box::new(e)))?
}
};

let curve_name = public_key
.ec_key()
.map_err(|_| CoseError::UnsupportedError("Non-EC keys are not supported".to_string()))?
.group()
.curve_name()
.ok_or_else(|| {
CoseError::UnsupportedError("Anonymous EC keys are not supported".to_string())
})?;
let sig_alg = ec_curve_to_parameters(curve_name)?.0;

Ok(KmsKey {
client,
key_id,

sig_alg,
public_key: Some(public_key),
})
}

fn get_sig_alg_spec(&self) -> SigningAlgorithmSpec {
match self.sig_alg {
SignatureAlgorithm::ES256 => SigningAlgorithmSpec::EcdsaSha256,
SignatureAlgorithm::ES384 => SigningAlgorithmSpec::EcdsaSha384,
SignatureAlgorithm::ES512 => SigningAlgorithmSpec::EcdsaSha512,
}
}

#[cfg(feature = "key_openssl_pkey")]
fn verify_with_public_key(&self, data: &[u8], signature: &[u8]) -> Result<bool, CoseError> {
self.public_key.as_ref().unwrap().verify(data, signature)
}
}

impl SigningPublicKey for KmsKey {
fn get_parameters(&self) -> Result<(SignatureAlgorithm, MessageDigest), CoseError> {
Ok((self.sig_alg, self.sig_alg.suggested_message_digest()))
}

fn verify(&self, data: &[u8], signature: &[u8]) -> Result<bool, CoseError> {
if self.public_key.is_some() {
#[cfg(feature = "key_openssl_pkey")]
return self.verify_with_public_key(data, signature);

#[cfg(not(feature = "key_openssl_pkey"))]
panic!("Would have been impossible to get public_key set");
} else {
// Call KMS to verify

// Recover the R and S factors from the signature contained in the object
let (bytes_r, bytes_s) = signature.split_at(self.sig_alg.key_length());

let r =
BigNum::from_slice(&bytes_r).map_err(|e| CoseError::SignatureError(Box::new(e)))?;
let s =
BigNum::from_slice(&bytes_s).map_err(|e| CoseError::SignatureError(Box::new(e)))?;

let sig = EcdsaSig::from_private_components(r, s)
.map_err(|e| CoseError::SignatureError(Box::new(e)))?;
let sig = sig
.to_der()
.map_err(|e| CoseError::SignatureError(Box::new(e)))?;

let request = self
.client
.verify()
.key_id(self.key_id.clone())
.message(Blob::new(data.to_vec()))
.message_type(MessageType::Digest)
.signing_algorithm(self.get_sig_alg_spec())
.signature(Blob::new(sig))
.send();

let handle = Handle::current();
let reply = handle.block_on(request);

match reply {
Ok(v) => Ok(v.signature_valid),
Err(SdkError::ServiceError {
err:
VerifyError {
kind: VerifyErrorKind::KmsInvalidSignatureException(_),
..
},
..
}) => Ok(false),
Err(e) => Err(CoseError::AwsVerifyError(e)),
}
}
}
}

impl SigningPrivateKey for KmsKey {
fn sign(&self, data: &[u8]) -> Result<Vec<u8>, CoseError> {
let request = self
.client
.sign()
.key_id(self.key_id.clone())
.message(Blob::new(data.to_vec()))
.message_type(MessageType::Digest)
.signing_algorithm(self.get_sig_alg_spec())
.send();

let handle = Handle::current();
let signature = handle
.block_on(request)
.map_err(CoseError::AwsSignError)?
.signature
.ok_or_else(|| CoseError::UnsupportedError("No signature returned".to_string()))?;

let signature = EcdsaSig::from_der(signature.as_ref())
.map_err(|e| CoseError::SignatureError(Box::new(e)))?;

let key_length = self.sig_alg.key_length();

// The spec defines the signature as:
// Signature = I2OSP(R, n) | I2OSP(S, n), where n = ceiling(key_length / 8)
// The Signer interface doesn't provide this, so this will use EcdsaSig interface instead
// and concatenate R and S.
// See https://tools.ietf.org/html/rfc8017#section-4.1 for details.
let bytes_r = signature.r().to_vec();
let bytes_s = signature.s().to_vec();

// These should *never* exceed ceiling(key_length / 8)
assert!(bytes_r.len() <= key_length);
assert!(bytes_s.len() <= key_length);

let mut signature_bytes = vec![0u8; key_length * 2];

// This is big-endian encoding so padding might be added at the start if the factor is
// too short.
let offset_copy = key_length - bytes_r.len();
signature_bytes[offset_copy..offset_copy + bytes_r.len()].copy_from_slice(&bytes_r);

// This is big-endian encoding so padding might be added at the start if the factor is
// too short.
let offset_copy = key_length - bytes_s.len() + key_length;
signature_bytes[offset_copy..offset_copy + bytes_s.len()].copy_from_slice(&bytes_s);

Ok(signature_bytes)
}
}
2 changes: 2 additions & 0 deletions src/crypto/mod.rs
Expand Up @@ -14,6 +14,8 @@ mod openssl;
#[cfg(feature = "openssl")]
pub use self::openssl::Openssl;

#[cfg(feature = "key_kms")]
pub mod kms;
#[cfg(feature = "key_openssl_pkey")]
mod openssl_pkey;
#[cfg(feature = "key_tpm")]
Expand Down
15 changes: 15 additions & 0 deletions src/error.rs
Expand Up @@ -33,6 +33,15 @@ pub enum CoseError {
/// TPM error occured
#[cfg(feature = "key_tpm")]
TpmError(tss_esapi::Error),
/// AWS sign error occured
#[cfg(feature = "key_kms")]
AwsSignError(aws_sdk_kms::types::SdkError<aws_sdk_kms::error::SignError>),
/// AWS verify error occured
#[cfg(feature = "key_kms")]
AwsVerifyError(aws_sdk_kms::types::SdkError<aws_sdk_kms::error::VerifyError>),
/// AWS GetPublicKey error occured
#[cfg(all(feature = "key_kms", feature = "key_openssl_pkey"))]
AwsGetPublicKeyError(aws_sdk_kms::types::SdkError<aws_sdk_kms::error::GetPublicKeyError>),
}

impl fmt::Display for CoseError {
Expand All @@ -51,6 +60,12 @@ impl fmt::Display for CoseError {
CoseError::EncryptionError(e) => write!(f, "Encryption error: {}", e),
#[cfg(feature = "key_tpm")]
CoseError::TpmError(e) => write!(f, "TPM error: {}", e),
#[cfg(feature = "key_kms")]
CoseError::AwsSignError(e) => write!(f, "AWS sign error: {}", e),
#[cfg(feature = "key_kms")]
CoseError::AwsVerifyError(e) => write!(f, "AWS verify error: {}", e),
#[cfg(all(feature = "key_kms", feature = "key_openssl_pkey"))]
CoseError::AwsGetPublicKeyError(e) => write!(f, "AWS GetPublicKey error: {}", e),
}
}
}
Expand Down