Skip to content

Commit

Permalink
feat: allow specifying JWT header when signing
Browse files Browse the repository at this point in the history
- Add alternative versions of signing methods that take a jwt::Header
  instead of an algorithm.
- Add `new_jwt_header` helper function that creates a new jwt::Header
  from an Algorithm.

Signed-off-by: Sergei Trofimov <[email protected]>
  • Loading branch information
setrofim committed Nov 6, 2024
1 parent 14889cb commit 8dcd09e
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 27 deletions.
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ registred profile and add the associated extensions.
# JWT/CWT common claims

The only common JWT/CWT claim specified by EAR spec is "iat" (issued at). Other claims (e.g.
"iss" or "exp") are not not expected to be present inside a valid EAR. It is, however, possible
"iss" or "exp") are not expected to be present inside a valid EAR. It is, however, possible
to define them for a particular profile and include them as extensions via mechanisms described
above.

Expand Down Expand Up @@ -238,6 +238,48 @@ let exp2 = match ear2.extensions.get_by_name("exp").unwrap() {
assert!(SystemTime::now().duration_since(UNIX_EPOCH).unwrap() < exp2);
```

# JWT 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 reated from an alorithm using
`new_jwt_header`.

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

const SIGNING_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPp4XZRnRHSMhGg0t
6yjQCRV35J4TUY4idLgiCu6EyLqhRANCAAQbx8C533c2AKDwL/RtjVipVnnM2WRv
5w2wZNCJrubSK0StYKJ71CikDgkhw8M90ojfRIowqpl0uLA3kW3PEZy9
-----END PRIVATE KEY-----
";

fn main() {
let token = Ear{
profile: "test".to_string(),
iat: 1,
vid: VerifierID {
build: "vsts 0.0.1".to_string(),
developer: "https://veraison-project.org".to_string(),
},
raw_evidence: None,
nonce: None,
submods: BTreeMap::from([("test".to_string(), Appraisal::new())]),
extensions: Extensions::new(),
};

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

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


# Limitations

- Signing supports PEM and DER keys; verification currently only supports JWK
Expand Down
94 changes: 68 additions & 26 deletions src/ear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,42 +199,66 @@ impl Ear {
/// Encode the EAR as a JWT token, signing it with the specified PEM-encoded key
#[allow(clippy::type_complexity)]
pub fn sign_jwt_pem(&self, alg: Algorithm, key: &[u8]) -> Result<String, Error> {
let (jwt_alg, keyfunc): (
jwt::Algorithm,
fn(&[u8]) -> Result<jwt::EncodingKey, jwt::errors::Error>,
) = match alg {
Algorithm::ES256 => (jwt::Algorithm::ES256, jwt::EncodingKey::from_ec_pem),
Algorithm::ES384 => (jwt::Algorithm::ES384, jwt::EncodingKey::from_ec_pem),
Algorithm::EdDSA => (jwt::Algorithm::EdDSA, jwt::EncodingKey::from_ed_pem),
Algorithm::PS256 => (jwt::Algorithm::PS256, jwt::EncodingKey::from_rsa_pem),
Algorithm::PS384 => (jwt::Algorithm::PS384, jwt::EncodingKey::from_rsa_pem),
Algorithm::PS512 => (jwt::Algorithm::PS512, jwt::EncodingKey::from_rsa_pem),
_ => return Err(Error::SignError(format!("algorithm {alg:?} not supported"))),
let header = &jwt::Header::new(alg_to_jwt_alg(&alg)?);
self.sign_jwt_pem_with_header(header, key)
}

/// Encode the EAR as a JWT token, signing it with the specified PEM-encoded key, and including
/// the provided headers.
pub fn sign_jwt_pem_with_header(
&self,
header: &jwt::Header,
key: &[u8],
) -> Result<String, Error> {
let keyfunc: fn(&[u8]) -> Result<jwt::EncodingKey, jwt::errors::Error> = match header.alg {
jwt::Algorithm::ES256 => jwt::EncodingKey::from_ec_pem,
jwt::Algorithm::ES384 => jwt::EncodingKey::from_ec_pem,
jwt::Algorithm::EdDSA => jwt::EncodingKey::from_ed_pem,
jwt::Algorithm::PS256 => jwt::EncodingKey::from_rsa_pem,
jwt::Algorithm::PS384 => jwt::EncodingKey::from_rsa_pem,
jwt::Algorithm::PS512 => jwt::EncodingKey::from_rsa_pem,
_ => {
return Err(Error::SignError(format!(
"algorithm {0:?} not supported",
header.alg
)))
}
};

let ek = keyfunc(key).map_err(|e| Error::KeyError(e.to_string()))?;

self.sign_jwk(jwt_alg, &ek)
jwt::encode(header, self, &ek).map_err(|e| Error::SignError(e.to_string()))
}

/// Encode the EAR as a JWT token, signing it with the specified DER-encoded key
pub fn sign_jwk_der(&self, alg: Algorithm, key: &[u8]) -> Result<String, Error> {
let (jwt_alg, ek) = match alg {
Algorithm::ES256 => (jwt::Algorithm::ES256, jwt::EncodingKey::from_ec_der(key)),
Algorithm::ES384 => (jwt::Algorithm::ES384, jwt::EncodingKey::from_ec_der(key)),
Algorithm::EdDSA => (jwt::Algorithm::EdDSA, jwt::EncodingKey::from_ed_der(key)),
Algorithm::PS256 => (jwt::Algorithm::PS256, jwt::EncodingKey::from_rsa_der(key)),
Algorithm::PS384 => (jwt::Algorithm::PS384, jwt::EncodingKey::from_rsa_der(key)),
Algorithm::PS512 => (jwt::Algorithm::PS512, jwt::EncodingKey::from_rsa_der(key)),
_ => return Err(Error::SignError(format!("algorithm {alg:?} not supported"))),
};

self.sign_jwk(jwt_alg, &ek)
let header = &jwt::Header::new(alg_to_jwt_alg(&alg)?);
self.sign_jwk_der_with_header(header, key)
}

fn sign_jwk(&self, alg: jwt::Algorithm, key: &jwt::EncodingKey) -> Result<String, Error> {
let header = jwt::Header::new(alg);
jwt::encode(&header, self, key).map_err(|e| Error::SignError(e.to_string()))
/// Encode the EAR as a JWT token, signing it with the specified DER-encoded key,
/// including the specified header(s).
pub fn sign_jwk_der_with_header(
&self,
header: &jwt::Header,
key: &[u8],
) -> Result<String, Error> {
let ek = match header.alg {
jwt::Algorithm::ES256 => jwt::EncodingKey::from_ec_der(key),
jwt::Algorithm::ES384 => jwt::EncodingKey::from_ec_der(key),
jwt::Algorithm::EdDSA => jwt::EncodingKey::from_ed_der(key),
jwt::Algorithm::PS256 => jwt::EncodingKey::from_rsa_der(key),
jwt::Algorithm::PS384 => jwt::EncodingKey::from_rsa_der(key),
jwt::Algorithm::PS512 => jwt::EncodingKey::from_rsa_der(key),
_ => {
return Err(Error::SignError(format!(
"algorithm {:?} not supported",
header.alg
)))
}
};

jwt::encode(header, self, &ek).map_err(|e| Error::SignError(e.to_string()))
}

/// Encode the EAR as a COSE token, signing it with the specified PEM-encoded key
Expand Down Expand Up @@ -514,6 +538,24 @@ impl<'de> Visitor<'de> for EarVisitor {
}
}

#[inline]
pub fn new_jwt_header(alg: &Algorithm) -> Result<jwt::Header, Error> {
Ok(jwt::Header::new(alg_to_jwt_alg(alg)?))
}

#[inline]
fn alg_to_jwt_alg(alg: &Algorithm) -> Result<jwt::Algorithm, Error> {
match alg {
Algorithm::ES256 => Ok(jwt::Algorithm::ES256),
Algorithm::ES384 => Ok(jwt::Algorithm::ES384),
Algorithm::EdDSA => Ok(jwt::Algorithm::EdDSA),
Algorithm::PS256 => Ok(jwt::Algorithm::PS256),
Algorithm::PS384 => Ok(jwt::Algorithm::PS384),
Algorithm::PS512 => Ok(jwt::Algorithm::PS512),
_ => Err(Error::SignError(format!("algorithm {alg:?} not supported"))),
}
}

#[inline]
fn alg_to_cose(alg: &Algorithm) -> Result<i32, Error> {
match alg {
Expand Down
43 changes: 43 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,48 @@
//! assert!(SystemTime::now().duration_since(UNIX_EPOCH).unwrap() < exp2);
//! ```
//!
//! # JWT 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 reated from an alorithm using
//! `new_jwt_header`.
//!
//! ```
//! use std::collections::BTreeMap;
//! use ear::{Ear, VerifierID, Algorithm, Appraisal, Extensions, new_jwt_header};
//!
//! const SIGNING_KEY: &str = "-----BEGIN PRIVATE KEY-----
//! MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPp4XZRnRHSMhGg0t
//! 6yjQCRV35J4TUY4idLgiCu6EyLqhRANCAAQbx8C533c2AKDwL/RtjVipVnnM2WRv
//! 5w2wZNCJrubSK0StYKJ71CikDgkhw8M90ojfRIowqpl0uLA3kW3PEZy9
//! -----END PRIVATE KEY-----
//! ";
//!
//! fn main() {
//! let token = Ear{
//! profile: "test".to_string(),
//! iat: 1,
//! vid: VerifierID {
//! build: "vsts 0.0.1".to_string(),
//! developer: "https://veraison-project.org".to_string(),
//! },
//! raw_evidence: None,
//! nonce: None,
//! submods: BTreeMap::from([("test".to_string(), Appraisal::new())]),
//! extensions: Extensions::new(),
//! };
//!
//! let mut header = new_jwt_header(&Algorithm::ES256).unwrap();
//! // set additional header(s)
//! header.kid = Some("key-ident".to_string());
//!
//! let signed = token.sign_jwt_pem_with_header(&header, SIGNING_KEY.as_bytes()).unwrap();
//! }
//! ```
//!
//!
//! # Limitations
//!
//! - Signing supports PEM and DER keys; verification currently only supports JWK
Expand All @@ -261,6 +303,7 @@ mod trust;
pub use self::algorithm::Algorithm;
pub use self::appraisal::Appraisal;
pub use self::base64::Bytes;
pub use self::ear::new_jwt_header;
pub use self::ear::Ear;
pub use self::error::Error;
pub use self::extension::get_profile;
Expand Down

0 comments on commit 8dcd09e

Please sign in to comment.