Skip to content

Commit

Permalink
Abstract signing cryptography
Browse files Browse the repository at this point in the history
This allows implementing signing and verification with more than openssl
PKey(Ref), like a TPM or AWS KMS keys (awslabs#5).

It implements the abstracted methods for PKeyRef, and PKey calls out to
PKeyRef.

Signed-off-by: Patrick Uiterwijk <patrick@puiterwijk.org>
  • Loading branch information
puiterwijk committed Jul 6, 2021
1 parent 441e6e9 commit ec28015
Show file tree
Hide file tree
Showing 5 changed files with 740 additions and 634 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Expand Up @@ -19,3 +19,7 @@ openssl = "0.10"
[dependencies.serde]
version = "1.0"
features = ["derive"]

[features]
default = ["key_openssl_pkey"]
key_openssl_pkey = []
48 changes: 48 additions & 0 deletions src/crypto/mod.rs
@@ -0,0 +1,48 @@
//! (Signing) cryptography abstraction

use openssl::{hash::MessageDigest, nid::Nid};

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

#[cfg(feature = "key_openssl_pkey")]
pub mod openssl_pkey;

/// A public key that can verify an existing signature
pub trait SigningPublicKey {
/// This returns the signature algorithm and message digest to be used for this
/// public key.
fn get_parameters(&self) -> Result<(SignatureAlgorithm, MessageDigest), COSEError>;

/// Given a vector of data and a signature, returns a boolean whether the signature
/// was valid.
fn verify(&self, data: &[u8], signature: &[u8]) -> Result<bool, COSEError>;
}

/// Follows the recommandations put in place by the RFC and doesn't deal with potential
/// mismatches: https://tools.ietf.org/html/rfc8152#section-8.1.
pub fn ec_curve_to_parameters(
curve_name: Nid,
) -> Result<(SignatureAlgorithm, MessageDigest, usize), COSEError> {
match curve_name {
// Recommended to use with SHA256
Nid::X9_62_PRIME256V1 => Ok((SignatureAlgorithm::ES256, MessageDigest::sha256(), 32)),
// Recommended to use with SHA384
Nid::SECP384R1 => Ok((SignatureAlgorithm::ES384, MessageDigest::sha384(), 48)),
// 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
))),
}
}

/// A private key that can produce new signatures
pub trait SigningPrivateKey: SigningPublicKey {
/// Given a slice of data, returns a signature
fn sign(&self, data: &[u8]) -> Result<Vec<u8>, COSEError>;
}
121 changes: 121 additions & 0 deletions src/crypto/openssl_pkey.rs
@@ -0,0 +1,121 @@
//! OpenSSL PKey(Ref) implementation for cryptography

use openssl::{
bn::BigNum,
ecdsa::EcdsaSig,
hash::MessageDigest,
pkey::{HasPrivate, HasPublic, PKey, PKeyRef},
};

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

impl<T> SigningPublicKey for PKey<T>
where
T: HasPublic,
{
fn get_parameters(&self) -> Result<(SignatureAlgorithm, MessageDigest), COSEError> {
self.as_ref().get_parameters()
}

fn verify(&self, data: &[u8], signature: &[u8]) -> Result<bool, COSEError> {
self.as_ref().verify(data, signature)
}
}

impl<T> SigningPublicKey for PKeyRef<T>
where
T: HasPublic,
{
fn get_parameters(&self) -> Result<(SignatureAlgorithm, MessageDigest), COSEError> {
let curve_name = self
.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 curve_parameters = ec_curve_to_parameters(curve_name)?;

Ok((curve_parameters.0, curve_parameters.1))
}

fn verify(&self, data: &[u8], signature: &[u8]) -> Result<bool, COSEError> {
let key = self.ec_key().map_err(|_| {
COSEError::UnsupportedError("Non-EC keys are not yet supported".to_string())
})?;

let curve_name = key.group().curve_name().ok_or_else(|| {
COSEError::UnsupportedError("Anonymous EC keys are not supported".to_string())
})?;

let (_, _, key_length) = ec_curve_to_parameters(curve_name)?;

// Recover the R and S factors from the signature contained in the object
let (bytes_r, bytes_s) = signature.split_at(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)?;
sig.verify(data, &key).map_err(COSEError::SignatureError)
}
}

impl<T> SigningPrivateKey for PKey<T>
where
T: HasPrivate,
{
fn sign(&self, data: &[u8]) -> Result<Vec<u8>, COSEError> {
self.as_ref().sign(data)
}
}

impl<T> SigningPrivateKey for PKeyRef<T>
where
T: HasPrivate,
{
fn sign(&self, data: &[u8]) -> Result<Vec<u8>, COSEError> {
let key = self.ec_key().map_err(|_| {
COSEError::UnsupportedError("Non-EC keys are not yet supported".to_string())
})?;

let curve_name = key.group().curve_name().ok_or_else(|| {
COSEError::UnsupportedError("Anonymous EC keys are not supported".to_string())
})?;

let (_, _, key_length) = ec_curve_to_parameters(curve_name)?;

// 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 signature = EcdsaSig::sign(data, &key).map_err(COSEError::SignatureError)?;
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)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -9,6 +9,7 @@
//!
//! Currently only COSE Sign1 and COSE Encrypt0 are implemented.

pub mod crypto;
pub mod encrypt;
pub mod error;
pub mod header_map;
Expand Down

0 comments on commit ec28015

Please sign in to comment.