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 e3bb1ea
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 94 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
109 changes: 15 additions & 94 deletions src/sign.rs
@@ -1,17 +1,13 @@
//! COSE Signing

use openssl::bn::BigNum;
use openssl::ecdsa::EcdsaSig;
use openssl::hash::{hash, MessageDigest};
use openssl::nid::Nid;
use openssl::pkey::PKeyRef;
use openssl::pkey::{Private, Public};
use openssl::hash::hash;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use serde_cbor::Error as CborError;
use serde_cbor::Value as CborValue;
use serde_repr::{Deserialize_repr, Serialize_repr};

use crate::crypto::{SigningPrivateKey, SigningPublicKey};
use crate::error::COSEError;
use crate::header_map::{map_to_empty_or_serialized, HeaderMap};

Expand Down Expand Up @@ -209,45 +205,15 @@ pub struct COSESign1(
);

impl COSESign1 {
/// 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.
fn 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
))),
}
}

/// Creates a COSESign1 structure from the given payload and some unprotected data in the form
/// of a HeaderMap. Signs the content with the given key using the recommedations from the spec
/// and sets the protected part of the document to reflect the algorithm used.
pub fn new(
payload: &[u8],
unprotected: &HeaderMap,
key: &PKeyRef<Private>,
key: &dyn SigningPrivateKey,
) -> Result<Self, COSEError> {
let ec_key = key.ec_key().map_err(|_| COSEError::UnimplementedError)?;

let curve_name = ec_key
.group()
.curve_name()
.ok_or(COSEError::UnimplementedError)?;

let (sig_alg, _, _) = COSESign1::curve_to_parameters(curve_name)?;
let (sig_alg, _) = key.get_parameters()?;

let mut protected = HeaderMap::new();
protected.insert(1.into(), (sig_alg as i8).into());
Expand All @@ -262,16 +228,9 @@ impl COSESign1 {
payload: &[u8],
protected: &HeaderMap,
unprotected: &HeaderMap,
key: &PKeyRef<Private>,
key: &dyn SigningPrivateKey,
) -> Result<Self, COSEError> {
let key = key.ec_key().map_err(|_| COSEError::UnimplementedError)?;

let curve_name = key
.group()
.curve_name()
.ok_or(COSEError::UnimplementedError)?;

let (_, digest, key_length) = COSESign1::curve_to_parameters(curve_name)?;
let (_, digest) = key.get_parameters()?;

// Create the SigStruct to sign
let protected_bytes =
Expand All @@ -288,37 +247,13 @@ impl COSESign1 {
)
.map_err(COSEError::SignatureError)?;

// 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(struct_digest.as_ref(), &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);
let signature = key.sign(struct_digest.as_ref())?;

Ok(COSESign1(
ByteBuf::from(protected_bytes),
unprotected.clone(),
ByteBuf::from(payload.to_vec()),
ByteBuf::from(signature_bytes),
ByteBuf::from(signature),
))
}

Expand Down Expand Up @@ -368,17 +303,11 @@ impl COSESign1 {

/// This checks the signature included in the structure against the given public key and
/// returns true if the signature matches the given key.
pub fn verify_signature(&self, key: &PKeyRef<Public>) -> Result<bool, COSEError> {
let key = key.ec_key().map_err(|_| COSEError::UnimplementedError)?;
// Don't support anonymous curves
let curve_name = key.group().curve_name().ok_or_else(|| {
COSEError::UnsupportedError("Anonymous curves are not supported".to_string())
})?;

pub fn verify_signature(&self, key: &dyn SigningPublicKey) -> Result<bool, COSEError> {
// In theory, the digest itself does not have to match the curve, however,
// this is the recommendation and the spec does not even provide a way to specify
// another digest type, so, signatures will fail if this is done differently
let (signature_alg, digest, key_length) = COSESign1::curve_to_parameters(curve_name)?;
let (signature_alg, digest) = key.get_parameters()?;

// The spec reads as follows:
// alg: This parameter is used to indicate the algorithm used for the
Expand Down Expand Up @@ -431,23 +360,15 @@ impl COSESign1 {
)
.map_err(COSEError::SignatureError)?;

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

/// This gets the `payload` and `protected` data of the document.
/// If `key` is provided, it only gets the data if the signature is correctly verified,
/// otherwise returns `Err(COSEError::UnverifiedSignature)`.
pub fn get_protected_and_payload(
&self,
key: Option<&PKeyRef<Public>>,
key: Option<&dyn SigningPublicKey>,
) -> Result<(HeaderMap, Vec<u8>), COSEError> {
if key.is_some() && !self.verify_signature(key.unwrap())? {
return Err(COSEError::UnverifiedSignature);
Expand All @@ -460,8 +381,8 @@ impl COSESign1 {
/// This gets the `payload` of the document. If `key` is provided, it only gets the payload
/// if the signature is correctly verified, otherwise returns
/// `Err(COSEError::UnverifiedSignature)`.
pub fn get_payload(&self, key: Option<&PKeyRef<Public>>) -> Result<Vec<u8>, COSEError> {
if key.is_some() && !self.verify_signature(&key.unwrap())? {
pub fn get_payload(&self, key: Option<&dyn SigningPublicKey>) -> Result<Vec<u8>, COSEError> {
if key.is_some() && !self.verify_signature(key.unwrap())? {
return Err(COSEError::UnverifiedSignature);
}
Ok(self.2.to_vec())
Expand All @@ -476,7 +397,7 @@ impl COSESign1 {
#[cfg(test)]
mod tests {
use super::*;
use openssl::pkey::PKey;
use openssl::pkey::{PKey, Private, Public};

// Public domain work: Pride and Prejudice by Jane Austen, taken from https://www.gutenberg.org/files/1342/1342.txt
const TEXT: &[u8] = b"It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.";
Expand Down

0 comments on commit e3bb1ea

Please sign in to comment.