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

feat: allow specifying COSE header when signing #30

Merged
merged 1 commit into from
Nov 6, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,17 +238,21 @@ let exp2 = match ear2.extensions.get_by_name("exp").unwrap() {
assert!(SystemTime::now().duration_since(UNIX_EPOCH).unwrap() < exp2);
```

# JWT headers
# JWT/CWT headers

When signing with `sign_jwt_pem`/`sign_jwk_der`, only the `alg` header is set in the resulting
JWT based on the the specified algorithm. If other headers need to be specified, then
`sign_jwt_pem_with_header` and `sign_jwk_der_with_header` can be used instead; these take a
`jwt::Header` instead of an algorithm. A new header can be created from an algorithm using
`new_jwt_header`.

The same goes when signing as COSE_Sign1Message. Only `alg` header is set by default, however,
`_with_header` signing methods can be used to specify a custom `cose::headers::CoseHeader`,
which can be reating from an algorithm using `new_cwt_header`.

```rust
use std::collections::BTreeMap;
use ear::{Ear, VerifierID, Algorithm, Appraisal, Extensions, new_jwt_header};
use ear::{Ear, VerifierID, Algorithm, Appraisal, Extensions, new_jwt_header, new_cose_header};

const SIGNING_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPp4XZRnRHSMhGg0t
Expand All @@ -271,11 +275,20 @@ fn main() {
extensions: Extensions::new(),
};

let mut header = new_jwt_header(&Algorithm::ES256).unwrap();
// JWT

let mut jwt_header = new_jwt_header(&Algorithm::ES256).unwrap();
// set additional header(s)
jwt_header.kid = Some("key-ident".to_string());

let signed_jwt = token.sign_jwt_pem_with_header(&jwt_header, SIGNING_KEY.as_bytes()).unwrap();
// CWT

let mut cwt_header = new_cose_header(&Algorithm::ES256).unwrap();
// set additional header(s)
header.kid = Some("key-ident".to_string());
cwt_header.kid("key-ident".as_bytes().to_vec(), true, false);

let signed = token.sign_jwt_pem_with_header(&header, SIGNING_KEY.as_bytes()).unwrap();
let signed_cwt = token.sign_cose_pem_with_header(cwt_header, SIGNING_KEY.as_bytes()).unwrap();
}
```

Expand Down
65 changes: 53 additions & 12 deletions src/ear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,28 +263,52 @@ impl Ear {

/// Encode the EAR as a COSE token, signing it with the specified PEM-encoded key
pub fn sign_cose_pem(&self, alg: Algorithm, key: &[u8]) -> Result<Vec<u8>, Error> {
self.sign_cose_bytes(alg, key, KeyFormat::PEM)
let header = new_cose_header(&alg)?;
self.sign_cose_bytes_with_header(header, key, KeyFormat::PEM)
}

/// Encode the EAR as a COSE token, signing it with the specified DER-encoded key
pub fn sign_cose_der(&self, alg: Algorithm, key: &[u8]) -> Result<Vec<u8>, Error> {
self.sign_cose_bytes(alg, key, KeyFormat::DER)
let header = new_cose_header(&alg)?;
self.sign_cose_bytes_with_header(header, key, KeyFormat::DER)
}

fn sign_cose_bytes(
/// Encode the EAR as a COSE token with the specified header, signing it with the specified
/// PEM-encoded key
pub fn sign_cose_pem_with_header(
&self,
alg: Algorithm,
header: cose::headers::CoseHeader,
key: &[u8],
) -> Result<Vec<u8>, Error> {
self.sign_cose_bytes_with_header(header, key, KeyFormat::PEM)
}

/// Encode the EAR as a COSE token with the specified header, signing it with the specified
/// DER-encoded key
pub fn sign_cose_der_with_header(
&self,
header: cose::headers::CoseHeader,
key: &[u8],
) -> Result<Vec<u8>, Error> {
self.sign_cose_bytes_with_header(header, key, KeyFormat::DER)
}

fn sign_cose_bytes_with_header(
&self,
header: cose::headers::CoseHeader,
key: &[u8],
key_fmt: KeyFormat,
) -> Result<Vec<u8>, Error> {
let cose_alg = alg_to_cose(&alg)?;
let cose_alg = header
.alg
.ok_or(Error::SignError("alg header must be set".to_string()))?;

let mut cose_key = cose::keys::CoseKey::new();
cose_key.alg(cose_alg);
cose_key.key_ops(vec![cose::keys::KEY_OPS_SIGN]);

match alg {
Algorithm::ES256 | Algorithm::ES384 | Algorithm::PS512 => {
match cose_alg {
cose::algs::ES256 | cose::algs::ES384 | cose::algs::PS512 => {
let ec_key = match key_fmt {
KeyFormat::PEM => ec::EcKey::private_key_from_pem(key),
KeyFormat::DER => ec::EcKey::private_key_from_der(key),
Expand Down Expand Up @@ -320,7 +344,7 @@ impl Ear {
cose_key.y(y_ref.to_vec());
cose_key.d(ec_key.private_key().to_vec());
}
Algorithm::EdDSA => {
cose::algs::EDDSA => {
cose_key.kty(cose::keys::OKP);
cose_key.crv(cose::keys::ED25519);

Expand All @@ -337,20 +361,28 @@ impl Ear {
cose_key.d(raw[..32].to_vec());
cose_key.x(raw[32..].to_vec());
}
_ => return Err(Error::SignError(format!("algorithm {alg:?} not supported"))),
_ => {
return Err(Error::SignError(format!(
"algorithm {cose_alg:?} not supported"
)))
}
};

self.sign_cose(cose_alg, &cose_key)
self.sign_cose_with_header(header, &cose_key)
}

fn sign_cose(&self, alg: i32, key: &cose::keys::CoseKey) -> Result<Vec<u8>, Error> {
fn sign_cose_with_header(
&self,
header: cose::headers::CoseHeader,
key: &cose::keys::CoseKey,
) -> Result<Vec<u8>, Error> {
let mut payload: Vec<u8> = Vec::new();
ciborium::ser::into_writer(self, &mut payload)
.map_err(|e| Error::SignError(e.to_string()))?;

let mut sign1 = CoseMessage::new_sign();
sign1.payload(payload);
sign1.header.alg(alg, true, false);
sign1.add_header(header);

if let Some(a) = key.alg {
if a != sign1.header.alg.unwrap() {
Expand Down Expand Up @@ -543,6 +575,15 @@ pub fn new_jwt_header(alg: &Algorithm) -> Result<jwt::Header, Error> {
Ok(jwt::Header::new(alg_to_jwt_alg(alg)?))
}

#[inline]
pub fn new_cose_header(alg: &Algorithm) -> Result<cose::headers::CoseHeader, Error> {
let cose_alg = alg_to_cose(alg)?;
let mut header = cose::headers::CoseHeader::new();
header.alg(cose_alg, true, false);

Ok(header)
}

#[inline]
fn alg_to_jwt_alg(alg: &Algorithm) -> Result<jwt::Algorithm, Error> {
match alg {
Expand Down
24 changes: 19 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,17 +238,21 @@
//! assert!(SystemTime::now().duration_since(UNIX_EPOCH).unwrap() < exp2);
//! ```
//!
//! # JWT headers
//! # JWT/CWT headers
//!
//! When signing with `sign_jwt_pem`/`sign_jwk_der`, only the `alg` header is set in the resulting
//! JWT based on the the specified algorithm. If other headers need to be specified, then
//! `sign_jwt_pem_with_header` and `sign_jwk_der_with_header` can be used instead; these take a
//! `jwt::Header` instead of an algorithm. A new header can be created from an algorithm using
//! `new_jwt_header`.
//!
//! The same goes when signing as COSE_Sign1Message. Only `alg` header is set by default, however,
//! `_with_header` signing methods can be used to specify a custom `cose::headers::CoseHeader`,
//! which can be reating from an algorithm using `new_cwt_header`.
//!
//! ```
//! use std::collections::BTreeMap;
//! use ear::{Ear, VerifierID, Algorithm, Appraisal, Extensions, new_jwt_header};
//! use ear::{Ear, VerifierID, Algorithm, Appraisal, Extensions, new_jwt_header, new_cose_header};
//!
//! const SIGNING_KEY: &str = "-----BEGIN PRIVATE KEY-----
//! MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPp4XZRnRHSMhGg0t
Expand All @@ -271,11 +275,20 @@
//! extensions: Extensions::new(),
//! };
//!
//! let mut header = new_jwt_header(&Algorithm::ES256).unwrap();
//! // JWT
//!
//! let mut jwt_header = new_jwt_header(&Algorithm::ES256).unwrap();
//! // set additional header(s)
//! jwt_header.kid = Some("key-ident".to_string());
//!
//! let signed_jwt = token.sign_jwt_pem_with_header(&jwt_header, SIGNING_KEY.as_bytes()).unwrap();
//! // CWT
//!
//! let mut cwt_header = new_cose_header(&Algorithm::ES256).unwrap();
//! // set additional header(s)
//! header.kid = Some("key-ident".to_string());
//! cwt_header.kid("key-ident".as_bytes().to_vec(), true, false);
//!
//! let signed = token.sign_jwt_pem_with_header(&header, SIGNING_KEY.as_bytes()).unwrap();
//! let signed_cwt = token.sign_cose_pem_with_header(cwt_header, SIGNING_KEY.as_bytes()).unwrap();
//! }
//! ```
//!
Expand Down Expand Up @@ -303,6 +316,7 @@ mod trust;
pub use self::algorithm::Algorithm;
pub use self::appraisal::Appraisal;
pub use self::base64::Bytes;
pub use self::ear::new_cose_header;
pub use self::ear::new_jwt_header;
pub use self::ear::Ear;
pub use self::error::Error;
Expand Down