Skip to content

Commit

Permalink
Implement AWS KMS cryptography
Browse files Browse the repository at this point in the history
This implements an abstracted signing crypto module that uses an Amazon
Web Services Key Management Service key to sign data.
It supports either KMS or local keys for verification.

Fixes: awslabs#5
Signed-off-by: Patrick Uiterwijk <patrick@puiterwijk.org>
  • Loading branch information
puiterwijk committed Jul 8, 2021
1 parent d7fd701 commit ff4e67c
Show file tree
Hide file tree
Showing 5 changed files with 448 additions and 15 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Expand Up @@ -16,6 +16,8 @@ serde_bytes = "0.11"
serde_with = "1.5"
openssl = "0.10"
tss-esapi = { version = "5.1", optional = true }
aws-sdk-kms = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main", package = "aws-sdk-kms", optional = true }
tokio = { version = "1.8", features = ["rt"], optional = true }

[dependencies.serde]
version = "1.0"
Expand All @@ -25,3 +27,7 @@ features = ["derive"]
default = ["key_openssl_pkey"]
key_openssl_pkey = []
key_tpm = ["tss-esapi"]
key_kms = ["aws-sdk-kms", "tokio"]

[dev-dependencies]
tokio = { version = "1.8", features = ["macros"] }
238 changes: 238 additions & 0 deletions src/crypto/kms.rs
@@ -0,0 +1,238 @@
//! KMS implementation for cryptography

use openssl::{
bn::BigNum,
ecdsa::EcdsaSig,
hash::MessageDigest,
pkey::{PKey, Public},
};
use tokio::runtime::Runtime;

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

use crate::{
crypto::{ec_curve_to_parameters, SigningPrivateKey, SigningPublicKey},
error::COSEError,
sign::SignatureAlgorithm,
};

/// 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>>,

runtime: Runtime,
}

impl KmsKey {
fn new_runtime() -> Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Error creating tokio runtime")
}

/// 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,

runtime: Self::new_runtime(),
})
}

/// 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 runtime = Self::new_runtime();

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 = runtime
.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(COSEError::SignatureError)?
}
};

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),

runtime,
})
}

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(COSEError::SignatureError)?;
let s = BigNum::from_slice(&bytes_s).map_err(COSEError::SignatureError)?;

let sig = EcdsaSig::from_private_components(r, s).map_err(COSEError::SignatureError)?;
let sig = sig.to_der().map_err(COSEError::SignatureError)?;

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 reply = self.runtime.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 signature = self
.runtime
.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(COSEError::SignatureError)?;

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)
}
}
32 changes: 19 additions & 13 deletions src/crypto/mod.rs
Expand Up @@ -4,6 +4,8 @@ use openssl::{hash::MessageDigest, nid::Nid};

use crate::{error::COSEError, sign::SignatureAlgorithm};

#[cfg(feature = "key_kms")]
pub mod kms;
#[cfg(feature = "key_openssl_pkey")]
mod openssl_pkey;
#[cfg(feature = "key_tpm")]
Expand All @@ -25,22 +27,26 @@ pub trait SigningPublicKey {
pub fn ec_curve_to_parameters(
curve_name: Nid,
) -> Result<(SignatureAlgorithm, MessageDigest, usize), COSEError> {
match curve_name {
let sig_alg = match curve_name {
// Recommended to use with SHA256
Nid::X9_62_PRIME256V1 => Ok((SignatureAlgorithm::ES256, MessageDigest::sha256(), 32)),
Nid::X9_62_PRIME256V1 => SignatureAlgorithm::ES256,
// Recommended to use with SHA384
Nid::SECP384R1 => Ok((SignatureAlgorithm::ES384, MessageDigest::sha384(), 48)),
Nid::SECP384R1 => SignatureAlgorithm::ES384,
// Recommended to use with SHA512
Nid::SECP521R1 => Ok((
SignatureAlgorithm::ES512,
MessageDigest::sha512(),
66, /* Not a typo */
)),
_ => Err(COSEError::UnsupportedError(format!(
"Curve name {:?} is not supported",
curve_name
))),
}
Nid::SECP521R1 => SignatureAlgorithm::ES512,
_ => {
return Err(COSEError::UnsupportedError(format!(
"Curve name {:?} is not supported",
curve_name
)))
}
};

Ok((
sig_alg,
sig_alg.suggested_message_digest(),
sig_alg.key_length(),
))
}

/// A private key that can produce new signatures
Expand Down
15 changes: 15 additions & 0 deletions src/error.rs
Expand Up @@ -29,6 +29,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::SdkError<aws_sdk_kms::error::SignError>),
/// AWS verify error occured
#[cfg(feature = "key_kms")]
AwsVerifyError(aws_sdk_kms::SdkError<aws_sdk_kms::error::VerifyError>),
/// AWS GetPublicKey error occured
#[cfg(all(feature = "key_kms", feature = "key_openssl_pkey"))]
AwsGetPublicKeyError(aws_sdk_kms::SdkError<aws_sdk_kms::error::GetPublicKeyError>),
}

impl fmt::Display for COSEError {
Expand All @@ -45,6 +54,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

0 comments on commit ff4e67c

Please sign in to comment.