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

Add COSE structure extensions that allow use of predefined cryptographic backends + an OpenSSL based backend. #13

Merged
merged 32 commits into from
Aug 4, 2024
Merged
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d3ccc5c
feat: add signing functionality based on openssl impl (in theory)
pulsastrix Jul 15, 2023
5aecb43
fix: update dependencies, fix compilation issues
pulsastrix Jun 15, 2024
c3fd6ce
feat: expand on openssl sign impl, add signature verification
pulsastrix Jun 16, 2024
4cb4fe5
test(token): separate tests for verification against self/examples
pulsastrix Jun 17, 2024
a522f50
feat: add ECDSA with P-384 and P-521 curves, expand tests
pulsastrix Jun 20, 2024
be190b5
refactor: rewrite cipher backend interface
pulsastrix Jun 24, 2024
af651fd
test(cose): run COSE_Sign1 tests from cose-wg examples repo
pulsastrix Jun 28, 2024
55b2268
feat: allow providing the AAD per signature
pulsastrix Jun 29, 2024
f955ebf
fix: make all COSE sign tests pass
pulsastrix Jun 29, 2024
d9f460e
refactor: split up COSE stuff a bit more
pulsastrix Jun 29, 2024
e6d2e70
feat: add support for CoseEncrypt0 using backend
pulsastrix Jun 29, 2024
7d17dfc
test(cose): add tests for CoseEncrypt0
pulsastrix Jun 29, 2024
afddf27
fix: GitHub actions now pull submodules
pulsastrix Jun 29, 2024
e417306
feat: support for recursive CoseRecipient structures using backend
pulsastrix Jul 2, 2024
b5393c4
feat: add encryption of CoseRecipients
pulsastrix Jul 3, 2024
1d88bd0
refactor: separate tests for COSE structures
pulsastrix Jul 3, 2024
c3439c7
feat: cipher based enc-/decryption for CoseMac/CoseEncrypt
pulsastrix Jul 4, 2024
a036b4f
test: tests for CoseEncrypt enveloped and AES key wrap
pulsastrix Jul 5, 2024
bffa954
feat: support CoseMac0, tests for CoseMac and CoseMac0
pulsastrix Jul 6, 2024
85af40a
refactor: clean up COSE tests, apply lint fixes
pulsastrix Jul 9, 2024
d4326e4
refactor: fix non-documentation lints for cose module
pulsastrix Jul 9, 2024
3413cbe
tests: refactor and re-enable access token tests
pulsastrix Jul 14, 2024
ab47513
refactor: some more cleanup and lint fixes
pulsastrix Jul 14, 2024
f6824cc
refactor: apply review suggestions and some more lints
pulsastrix Jul 14, 2024
bb93edf
refactor: simplify, re-enable integration test, remaining #13 review …
pulsastrix Jul 15, 2024
3009a78
doc: large batch of documentation for most stuff, minor fixes
pulsastrix Jul 16, 2024
90f3e78
refactor: rewrite AAD and key providers, simplify API
pulsastrix Jul 19, 2024
50cf803
refactor: more descriptive error for nested recipient decrypters
pulsastrix Jul 23, 2024
d8cd1e2
refactor: code cleanup of COSE token cryptography
pulsastrix Jul 23, 2024
9b5f2be
docs: update documentation regarding COSE cryptography
pulsastrix Jul 24, 2024
696c62d
docs: fix and simplify documentation links
pulsastrix Jul 24, 2024
e3e65eb
chore: apply suggestions from code review (#13)
pulsastrix Jul 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add signing functionality based on openssl impl (in theory)
pulsastrix committed Jul 15, 2023
commit d3ccc5c057ca29e77375eaec3be6e806aebf5f70
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ strum = { version = "^0.24", default-features = false, features = ["derive"] }
strum_macros = { version = "^0.24", default-features = false }
enumflags2 = { version = "^0.7", default-features = false }
rand = { version = "^0.8", default-features = false }
openssl = { version = "^0.10" }

[dev-dependencies]
hex = { version = "^0.4" }
1 change: 1 addition & 0 deletions src/token/crypto_impl/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod openssl;
294 changes: 294 additions & 0 deletions src/token/crypto_impl/openssl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
use crate::error::CoseCipherError;
use crate::token::CoseCipher;
use crate::CoseSignCipher;
use alloc::collections::BTreeSet;
use ciborium::value::{Integer, Value};
use coset::iana::EnumI64;
use coset::{
iana, Algorithm, CoseKey, Header, KeyType, Label, ProtectedHeader, RegisteredLabel,
RegisteredLabelWithPrivate,
};
use openssl::bn::BigNum;
use openssl::ec::{EcGroup, EcGroupRef, EcKey, EcKeyRef};
use openssl::error::ErrorStack;
use openssl::hash::MessageDigest;
use openssl::nid::Nid;
use openssl::pkey::{PKey, Private, Public};
use openssl::sign::Signer;
use rand::{CryptoRng, RngCore};
use strum_macros::Display;

const P256CURVE_ID: Integer = Integer::from(iana::EllipticCurve::P_256.to_i64());
const P384CURVE_ID: Integer = Integer::from(iana::EllipticCurve::P_384.to_i64());
const P521CURVE_ID: Integer = Integer::from(iana::EllipticCurve::P_521.to_i64());

const P256GROUP: EcGroup = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();

#[derive(Debug, PartialEq, Eq, Clone, Display)]
#[non_exhaustive]
pub enum CoseOpensslCipherError {
UnsupportedKeyType,
UnsupportedCurve(Option<iana::EllipticCurve>),
UnsupportedAlgorithm(Option<iana::Algorithm>),
UnsupportedKeyOperation(iana::KeyOperation),
DuplicateHeaders(Vec<Label>),
InvalidKeyId(Vec<u8>),
MissingEc2KeyParam(iana::Ec2KeyParameter),
OpensslError(core::ffi::c_ulong),
Other(&'static str),
}

impl From<CoseOpensslCipherError> for CoseCipherError<CoseOpensslCipherError> {
fn from(value: CoseOpensslCipherError) -> Self {
CoseCipherError::Other(value)
}
}

impl From<ErrorStack> for CoseOpensslCipherError {
fn from(value: ErrorStack) -> Self {
CoseOpensslCipherError::OpensslError(value.errors().first().unwrap().code())
}
}

fn create_header_parameter_set(header_bucket: &Header) -> BTreeSet<Label> {
let mut header_bucket_fields = BTreeSet::new();

if header_bucket.alg.is_some() {
header_bucket_fields.insert(Label::Int(iana::HeaderParameter::Alg.to_i64()))
}
if header_bucket.content_type.is_some() {
header_bucket_fields.insert(Label::Int(iana::HeaderParameter::ContentType.to_i64()))
}
if !header_bucket.key_id.is_empty() {
header_bucket_fields.insert(Label::Int(iana::HeaderParameter::Kid.to_i64()))
}
if !header_bucket.crit.is_empty() {
header_bucket_fields.insert(Label::Int(iana::HeaderParameter::Crit.to_i64()))
}
if !header_bucket.counter_signatures.is_empty() {
header_bucket_fields.insert(Label::Int(iana::HeaderParameter::CounterSignature.to_i64()))
}
if !header_bucket.iv.is_empty() {
header_bucket_fields.insert(Label::Int(iana::HeaderParameter::Iv.to_i64()))
}
if !header_bucket.partial_iv.is_empty() {
header_bucket_fields.insert(Label::Int(iana::HeaderParameter::PartialIv.to_i64()))
}

header_bucket_fields.extend(header_bucket.rest.iter().map(|(k, _v)| k.clone()));

return header_bucket_fields;
}

fn find_param_by_label<'a>(label: &Label, param_vec: &'a Vec<(Label, Value)>) -> Option<&'a Value> {
param_vec
.binary_search_by(|(v, _)| v.cmp(label))
.map(|i| &param_vec.get(i).unwrap().1)
.ok()
}

impl CoseCipher for Signer<'_> {
type Error = CoseOpensslCipherError;

fn set_headers<RNG: RngCore + CryptoRng>(
key: &CoseKey,
unprotected_header: &mut Header,
protected_header: &mut Header,
rng: RNG,
) -> Result<(), CoseCipherError<Self::Error>> {
let duplicate_header_fields = create_header_parameter_set(unprotected_header)
.intersection(&create_header_parameter_set(protected_header))
.collect();
if duplicate_header_fields.is_empty() {
return Err(CoseCipherError::Other(
CoseOpensslCipherError::DuplicateHeaders(duplicate_header_fields),
));
}

match key.kty {
KeyType::Assigned(iana::KeyType::EC2) => {
// Key must be an ECDSA key conforming to RFC 9053, Section 2.1.0

// Key must support the signing operation.
if !key
.key_ops
.contains(&RegisteredLabel::Assigned(iana::KeyOperation::Sign))
{
return Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedKeyOperation(iana::KeyOperation::Sign),
));
}

// Get the value of the curve type parameter.
let curve_type = find_param_by_label(
&Label::Int(iana::Ec2KeyParameter::Crv.to_i64()),
&key.params,
);

// We do actually allow overriding the algorithm in the header, however, this must
// happen in the protected header.
if unprotected_header.alg.is_some() {
return Err(CoseCipherError::HeaderAlreadySet {
existing_header_name: String::from("alg"),
});
}

// Check if the chosen algorithm was overridden manually in the headers.
// TODO check if alg matches key type.
let mut algorithm = match &protected_header.alg {
Some(Algorithm::Assigned(
alg @ (iana::Algorithm::ES256
| iana::Algorithm::ES384
| iana::Algorithm::ES512),
)) => Some(*alg),
Some(Algorithm::Assigned(v)) => {
return Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedAlgorithm(Some(*v)),
))
}
Some(_) => {
return Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedAlgorithm(None),
))
}
None => None,
};

// If not, check if the chosen algorithm was overridden manually in the key.
// TODO check if alg matches key type.
if algorithm.is_none() {
algorithm = match &key.alg {
Some(Algorithm::Assigned(
alg @ (iana::Algorithm::ES256
| iana::Algorithm::ES384
| iana::Algorithm::ES512),
)) => Some(*alg),
Some(Algorithm::Assigned(v)) => {
return Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedAlgorithm(Some(*v)),
))
}
Some(_) => {
return Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedAlgorithm(None),
))
}
None => None,
}
}

// If not, set the default algorithm for a given curve.
if algorithm.is_none() {
algorithm = match curve_type {
Some(Value::Integer(P256CURVE_ID)) => Some(iana::Algorithm::ES256),
Some(Value::Integer(P384CURVE_ID)) => Some(iana::Algorithm::ES384),
Some(Value::Integer(P521CURVE_ID)) => Some(iana::Algorithm::ES512),
Some(Value::Integer(v)) => {
let curve_id = i64::try_from(i128::from(v.clone()))
.map(iana::EllipticCurve::from_i64)
.ok()
.flatten();
return Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedCurve(curve_id),
));
}
None => {
return Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedCurve(None),
))
}
}
}
debug_assert!(algorithm.is_some());
// At this point, due to the last match clause, we have either found an algorithm
// or returned with an error code, so it is reasonable to unwrap().
protected_header.alg = Some(Algorithm::Assigned(algorithm.unwrap()));

if !protected_header.is_empty() && protected_header.key_id != key.key_id {
return Err(CoseCipherError::Other(
CoseOpensslCipherError::InvalidKeyId(protected_header.key_id.clone()),
));
} else if !unprotected_header.is_empty() && unprotected_header.key_id != key.key_id
{
return Err(CoseCipherError::Other(
CoseOpensslCipherError::InvalidKeyId(unprotected_header.key_id.clone()),
));
} else {
unprotected_header.key_id = key.key_id.clone();
}

Ok(())
}
_ => Err(CoseCipherError::Other(
CoseOpensslCipherError::UnsupportedKeyType,
)),
}
}
}

impl CoseSignCipher for Signer<'_> {
fn sign(
key: &CoseKey,
target: &[u8],
unprotected_header: &Header,
protected_header: &Header,
) -> Result<Vec<u8>, CoseCipherError<CoseOpensslCipherError>> {
let mut signer = match &protected_header.alg {
Algorithm::Assigned(iana::Algorithm::ES256) => {
let x = find_param_by_label(
&Label::Int(iana::Ec2KeyParameter::X.to_i64()),
&key.params,
)
.map(Value::as_bytes)
.flatten()
.ok_or(CoseCipherError::Other(
CoseOpensslCipherError::MissingEc2KeyParam(iana::Ec2KeyParameter::X),
))?;
let y = find_param_by_label(
&Label::Int(iana::Ec2KeyParameter::Y.to_i64()),
&key.params,
)
.map(Value::as_bytes)
.flatten()
.ok_or(CoseCipherError::Other(
CoseOpensslCipherError::MissingEc2KeyParam(iana::Ec2KeyParameter::X),
))?;
let d = find_param_by_label(
&Label::Int(iana::Ec2KeyParameter::D.to_i64()),
&key.params,
)
.map(Value::as_bytes)
.flatten()
.ok_or(CoseCipherError::Other(
CoseOpensslCipherError::MissingEc2KeyParam(iana::Ec2KeyParameter::X),
))?;
let public_key = EcKey::<Public>::from_public_key_affine_coordinates(
&P256GROUP,
&BigNum::from_slice(x)?,
&BigNum::from_slice(y)?,
)?;
let private_key = EcKey::<Private>::from_private_components(
&P256GROUP,
&BigNum::from_slice(d)?,
public_key.public_key(),
)?;
Signer::new(MessageDigest::sha256(), &PKey::from_ec_key(private_key)?)?
}
_ => todo!(),
};

Ok(signer.sign_oneshot_to_vec(target)?)
}

fn verify(
key: &CoseKey,
signature: &[u8],
signed_data: &[u8],
unprotected_header: &Header,
protected_header: &ProtectedHeader,
unprotected_signature_header: Option<&Header>,
protected_signature_header: Option<&ProtectedHeader>,
) -> Result<(), CoseCipherError<Self::Error>> {
todo!()
}
}
7 changes: 6 additions & 1 deletion src/token/mod.rs
Original file line number Diff line number Diff line change
@@ -170,6 +170,8 @@ use rand::{CryptoRng, RngCore};
use crate::common::cbor_values::ByteString;
use crate::error::{AccessTokenError, CoseCipherError, MultipleCoseError};

pub mod crypto_impl;

#[cfg(test)]
mod tests;

@@ -289,7 +291,7 @@ pub trait CoseSignCipher: CoseCipher {
target: &[u8],
unprotected_header: &Header,
protected_header: &Header,
) -> Vec<u8>;
) -> Result<Vec<u8>, CoseCipherError<Self::Error>>;

/// Verifies the `signature` of the `signed_data` with the `key`.
///
@@ -822,6 +824,9 @@ where
let aad = external_aad.unwrap_or(&[0; 0]);
let kek_id = kek.key_id.as_slice();
// One of the recipient structures should contain CEK encrypted with our KEK.
// TODO: Recipient structures can be encrypted themselves, and have nested recipient structures
// inside of them. We should probably search those as well (while still ensuring that
// there is a maximum recursion depth to avoid DoS or stack overflow.
let recipients = encrypt
.recipients
.iter()