Skip to content
Open
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `VerifyingKey`, a typed verification key that carries its algorithm
(`HmacSha256`, `Es256`, `Ps256`), and `Token::verify_with_key`, which rejects
a token whose header `alg` doesn't match the key's algorithm before running
any cryptography. `VerifyingKey`'s `Debug` output redacts the key bytes.
- **ES256** (ECDSA using the P-256 curve and SHA-256) signing and verification
support, producing/consuming `COSE_Sign1` structures.
- **PS256** (RSASSA-PSS using SHA-256 and MGF1 with SHA-256) signing and
Expand Down Expand Up @@ -37,6 +41,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- **Algorithm confusion:** `Token::verify(&[u8])` picked the algorithm from the
token's own `alg` header. With ES256/PS256 support, an attacker could forge a
token declaring HMAC and MAC it with the verifier's public key bytes, and
`verify` accepted it. Use `verify_with_key` with a typed `VerifyingKey`.
`verify` is **deprecated**, only accepts HMAC-SHA256, and returns
`Error::InvalidAlgorithm` for ES256/PS256; it is still unsafe to call with a
public key.
- ES256 signatures are now produced in canonical "low-S" form, and `verify`
rejects high-S (non-canonical) ECDSA signatures. ECDSA signatures are
malleable — for a valid `(r, s)`, the pair `(r, n - s)` verifies over the same
Expand Down
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@ let token_bytes = token.to_bytes().expect("Failed to encode token");
### Verifying a token

```rust
use common_access_token::{Token, VerificationOptions};
use common_access_token::{Token, VerificationOptions, VerifyingKey};

// Decode the token
let token = Token::from_bytes(&token_bytes).expect("Failed to decode token");

// Verify the signature
token.verify(key).expect("Failed to verify signature");
token
.verify_with_key(VerifyingKey::HmacSha256(key))
.expect("Failed to verify signature");

// Verify the claims
let options = VerificationOptions::new()
Expand Down Expand Up @@ -161,18 +163,20 @@ Asymmetric signature algorithms (ES256, PS256) instead use the COSE_Sign1 struct

### Signing Algorithms

The library supports three algorithms, selected via `Algorithm` on the builder. The bytes passed to `sign()` and `verify()` are interpreted according to the algorithm:
The library supports three algorithms, selected via `Algorithm` on the builder. The `sign()` key bytes and the [`VerifyingKey`] passed to `verify_with_key()` are interpreted according to the algorithm:

| Algorithm | COSE id | Structure | `sign()` key | `verify()` key |
| ------------------- | ------- | ---------- | -------------------------------- | ------------------------------- |
| `Algorithm::HmacSha256` | 5 | COSE_Mac0 | raw symmetric key bytes | raw symmetric key bytes |
| `Algorithm::Es256` | -7 | COSE_Sign1 | PKCS#8 DER P-256 private key | SPKI DER P-256 public key |
| `Algorithm::Ps256` | -37 | COSE_Sign1 | PKCS#8 DER RSA private key | SPKI DER RSA public key |
| Algorithm | COSE id | Structure | `sign()` key | `verify_with_key()` key |
| ------------------- | ------- | ---------- | -------------------------------- | ------------------------------------ |
| `Algorithm::HmacSha256` | 5 | COSE_Mac0 | raw symmetric key bytes | `VerifyingKey::HmacSha256` (raw bytes) |
| `Algorithm::Es256` | -7 | COSE_Sign1 | PKCS#8 DER P-256 private key | `VerifyingKey::Es256` (SPKI DER public key) |
| `Algorithm::Ps256` | -37 | COSE_Sign1 | PKCS#8 DER RSA private key | `VerifyingKey::Ps256` (SPKI DER public key) |

ES256 is ECDSA over the NIST P-256 curve with SHA-256; signatures are the fixed 64-byte COSE `r || s` form. PS256 is RSASSA-PSS with SHA-256 and MGF1-SHA-256. PSS uses a random salt, so each signature over the same input differs while all remain valid.

> **Verify with a typed `VerifyingKey`.** `verify_with_key()` binds the algorithm to the key you provide and rejects any token whose header `alg` does not match, which prevents algorithm-confusion forgery (an attacker crafting an `HmacSha256` token whose MAC is computed over your public verification key). The older `verify(&[u8])` is deprecated and only accepts HMAC-SHA256.

```rust
use common_access_token::{Algorithm, KeyId, RegisteredClaims, TokenBuilder, current_timestamp};
use common_access_token::{Algorithm, KeyId, RegisteredClaims, TokenBuilder, VerifyingKey, current_timestamp};

// `private_key` is PKCS#8 DER; `public_key` is SPKI DER.
let token = TokenBuilder::new()
Expand All @@ -188,7 +192,9 @@ let token = TokenBuilder::new()

let token_bytes = token.to_bytes().expect("Failed to encode token");
let decoded = common_access_token::Token::from_bytes(&token_bytes).expect("decode");
decoded.verify(public_key).expect("Failed to verify ES256 signature");
decoded
.verify_with_key(VerifyingKey::Es256(public_key))
.expect("Failed to verify ES256 signature");
```

### Nested Map Claims
Expand Down Expand Up @@ -256,7 +262,9 @@ This library creates HMAC tokens using the COSE_Mac0 structure with proper CBOR

```rust
// This will work with both COSE_Sign1 and COSE_Mac0 tokens
token.verify(key).expect("Failed to verify signature");
token
.verify_with_key(VerifyingKey::HmacSha256(key))
.expect("Failed to verify signature");
```

## CAT-specific Claims
Expand Down
22 changes: 18 additions & 4 deletions benches/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use std::fs;
use std::path::Path;

use common_access_token::{
current_timestamp, Algorithm, KeyId, RegisteredClaims, Token, TokenBuilder, VerificationOptions,
current_timestamp, Algorithm, KeyId, RegisteredClaims, Token, TokenBuilder,
VerificationOptions, VerifyingKey,
};
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use ct_codecs::{Base64, Decoder};
Expand All @@ -43,10 +44,22 @@ struct AlgCase {
algorithm: Algorithm,
/// Key passed to `sign` (HMAC secret, or PKCS#8 DER private key).
signing_key: Vec<u8>,
/// Key passed to `verify` (same HMAC secret, or SPKI DER public key).
/// Key passed to `verify_with_key` (same HMAC secret, or SPKI DER public key).
verifying_key: Vec<u8>,
}

impl AlgCase {
/// The verifying key typed to this case's algorithm.
fn typed_verifying_key(&self) -> VerifyingKey<'_> {
match self.algorithm {
Algorithm::HmacSha256 => VerifyingKey::HmacSha256(&self.verifying_key),
Algorithm::Es256 => VerifyingKey::Es256(&self.verifying_key),
Algorithm::Ps256 => VerifyingKey::Ps256(&self.verifying_key),
other => unreachable!("no benchmark case for {other:?}"),
}
}
}

/// Build the set of algorithms to benchmark.
fn alg_cases() -> Vec<AlgCase> {
let es_priv = Base64::decode_to_vec(ES256_PRIVATE_KEY_B64, None).expect("valid ES256 private");
Expand Down Expand Up @@ -121,10 +134,11 @@ fn bench_verify(c: &mut Criterion) {
let decoded = Token::from_bytes(&bytes).expect("decode");

// Sanity check before timing so a broken setup fails loudly.
decoded.verify(&case.verifying_key).expect("verify");
let key = case.typed_verifying_key();
decoded.verify_with_key(key).expect("verify");

group.bench_function(case.id, |b| {
b.iter(|| decoded.verify(&case.verifying_key).expect("verify"));
b.iter(|| decoded.verify_with_key(key).expect("verify"));
});
}
group.finish();
Expand Down
14 changes: 11 additions & 3 deletions examples/asymmetric_signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
//! openssl pkey -inform DER -in priv.der -pubout -outform DER # public (SPKI)

use common_access_token::{
current_timestamp, Algorithm, KeyId, RegisteredClaims, Token, TokenBuilder, VerificationOptions,
current_timestamp, Algorithm, KeyId, RegisteredClaims, Token, TokenBuilder,
VerificationOptions, VerifyingKey,
};
use ct_codecs::{Base64, Decoder};

Expand Down Expand Up @@ -89,10 +90,17 @@ fn demo(name: &str, alg: Algorithm, private_key_b64: &str, public_key_b64: &str,
token.signature.len()
);

// The verifier only needs the public key.
// The verifier only needs the public key. The key is wrapped in a typed
// `VerifyingKey` that names its algorithm, so the token's self-declared
// `alg` header cannot steer verification to a different primitive.
let decoded = Token::from_bytes(&token_bytes).expect("Failed to decode token");
let verifying_key = match alg {
Algorithm::Es256 => VerifyingKey::Es256(&public_key),
Algorithm::Ps256 => VerifyingKey::Ps256(&public_key),
other => panic!("unsupported algorithm for this example: {other:?}"),
};
decoded
.verify(&public_key)
.verify_with_key(verifying_key)
.expect("Failed to verify signature");

let options = VerificationOptions::new()
Expand Down
6 changes: 3 additions & 3 deletions examples/basic_usage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use common_access_token::{
current_timestamp, Algorithm, CborValue, KeyId, RegisteredClaims, TokenBuilder,
VerificationOptions,
VerificationOptions, VerifyingKey,
};
use std::collections::BTreeMap;

Expand Down Expand Up @@ -132,7 +132,7 @@ fn verify_token(token_bytes: &[u8], key: &[u8], expected_token_type: &str) {
};

// Verify the signature
if let Err(err) = token.verify(key) {
if let Err(err) = token.verify_with_key(VerifyingKey::HmacSha256(key)) {
println!(
"Failed to verify {} token signature: {}",
expected_token_type, err
Expand Down Expand Up @@ -195,7 +195,7 @@ fn verify_nested_map_token(token_bytes: &[u8], key: &[u8]) {
};

// Verify the signature
if let Err(err) = token.verify(key) {
if let Err(err) = token.verify_with_key(VerifyingKey::HmacSha256(key)) {
println!("Failed to verify nested map token signature: {}", err);
return;
}
Expand Down
4 changes: 2 additions & 2 deletions examples/cat_specific_claims.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use common_access_token::{
cat_keys, catm, catr, catreplay, catu, current_timestamp, uri_components, Algorithm, KeyId,
RegisteredClaims, TokenBuilder, VerificationOptions,
RegisteredClaims, TokenBuilder, VerificationOptions, VerifyingKey,
};
use std::collections::BTreeMap;

Expand All @@ -21,7 +21,7 @@ fn main() {

// Verify the signature
decoded_token
.verify(key)
.verify_with_key(VerifyingKey::HmacSha256(key))
.expect("Failed to verify signature");

// Verify the claims
Expand Down
4 changes: 2 additions & 2 deletions examples/cat_validation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use common_access_token::{
cat_keys, catm, catr, catreplay, catu, current_timestamp, uri_components, Algorithm, KeyId,
RegisteredClaims, TokenBuilder, VerificationOptions,
RegisteredClaims, TokenBuilder, VerificationOptions, VerifyingKey,
};
use std::collections::BTreeMap;

Expand All @@ -25,7 +25,7 @@ fn main() {

// Verify signature
decoded_token
.verify(key)
.verify_with_key(VerifyingKey::HmacSha256(key))
.expect("Failed to verify signature");

// Demonstrate different CAT-specific claim validations
Expand Down
12 changes: 10 additions & 2 deletions examples/sample_es256_ps256_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
//! elsewhere.

use common_access_token::{
current_timestamp, Algorithm, KeyId, RegisteredClaims, Token, TokenBuilder, VerificationOptions,
current_timestamp, Algorithm, KeyId, RegisteredClaims, Token, TokenBuilder,
VerificationOptions, VerifyingKey,
};
use ct_codecs::{Base64, Base64UrlSafeNoPadding, Decoder, Encoder, Hex};

Expand Down Expand Up @@ -93,7 +94,14 @@ fn mint(

// Sanity check: the token verifies against its public key.
let decoded = Token::from_bytes(&token_bytes).expect("decode");
decoded.verify(&public_key).expect("verify signature");
let verifying_key = match alg {
Algorithm::Es256 => VerifyingKey::Es256(&public_key),
Algorithm::Ps256 => VerifyingKey::Ps256(&public_key),
other => panic!("unsupported algorithm for this example: {other:?}"),
};
decoded
.verify_with_key(verifying_key)
.expect("verify signature");
decoded
.verify_claims(
&VerificationOptions::new()
Expand Down
69 changes: 69 additions & 0 deletions src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use crate::constants::cose_algs;
use std::collections::BTreeMap;
use std::fmt;

/// Supported algorithms for token signing and verification.
///
Expand Down Expand Up @@ -141,6 +142,74 @@ impl Algorithm {
}
}

/// A typed verification key, used by [`crate::token::Token::verify_with_key`].
///
/// Each variant carries the key material *and* the algorithm it is valid for.
/// Because the key's type is the algorithm, the token's self-declared `alg`
/// header cannot steer verification to a different cryptographic primitive:
/// `verify_with_key` rejects any token whose header algorithm does not match
/// the key's algorithm before running any cryptography.
///
/// This prevents algorithm-confusion forgery. For example, an attacker who
/// crafts a token with `alg = HmacSha256` and a MAC computed over the (public)
/// ES256 verification key cannot have it accepted by a verifier that supplies a
/// [`VerifyingKey::Es256`] key, because the algorithms do not match.
///
/// # Example
///
/// ```
/// use common_access_token::VerifyingKey;
///
/// let secret = b"my-secret-key";
/// let key = VerifyingKey::HmacSha256(secret);
/// ```
///
/// The [`Debug`] implementation never prints key material, so a
/// `VerifyingKey` holding an HMAC secret is safe to log.
#[derive(Clone, Copy)]
#[non_exhaustive]
pub enum VerifyingKey<'a> {
/// HMAC-SHA256 shared secret, as raw key bytes (COSE_Mac0, alg 5).
HmacSha256(&'a [u8]),
/// ES256 public key, as SPKI DER (COSE_Sign1, alg -7).
Es256(&'a [u8]),
/// PS256 public key, as SPKI DER (COSE_Sign1, alg -37).
Ps256(&'a [u8]),
}

impl VerifyingKey<'_> {
/// The algorithm this key is valid for.
pub fn algorithm(&self) -> Algorithm {
match self {
VerifyingKey::HmacSha256(_) => Algorithm::HmacSha256,
VerifyingKey::Es256(_) => Algorithm::Es256,
VerifyingKey::Ps256(_) => Algorithm::Ps256,
}
}

/// The raw key material.
pub fn key_bytes(&self) -> &[u8] {
match self {
VerifyingKey::HmacSha256(k) | VerifyingKey::Es256(k) | VerifyingKey::Ps256(k) => k,
}
}
}

// Manual impl so the HMAC shared secret (and, for consistency, public key
// bytes) never ends up in logs, panics, or other diagnostics.
impl fmt::Debug for VerifyingKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
VerifyingKey::HmacSha256(_) => "HmacSha256",
VerifyingKey::Es256(_) => "Es256",
VerifyingKey::Ps256(_) => "Ps256",
};
f.debug_tuple(name)
.field(&format_args!("<redacted>"))
.finish()
}
}

/// Key identifier that can be either a binary or string value.
///
/// The key identifier (kid) is used to indicate which key was used to secure the token.
Expand Down
10 changes: 5 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
//! ## Basic Example
//!
//! ```rust
//! use common_access_token::{Algorithm, KeyId, RegisteredClaims, TokenBuilder, VerificationOptions};
//! use common_access_token::{Algorithm, KeyId, RegisteredClaims, TokenBuilder, VerificationOptions, VerifyingKey};
//! use common_access_token::current_timestamp;
//!
//! // Create a key for signing and verification
Expand Down Expand Up @@ -52,7 +52,7 @@
//! .expect("Failed to decode token");
//!
//! // Verify the signature
//! decoded_token.verify(key).expect("Failed to verify signature");
//! decoded_token.verify_with_key(VerifyingKey::HmacSha256(key)).expect("Failed to verify signature");
//!
//! // Verify the claims
//! let options = VerificationOptions::new()
Expand All @@ -66,7 +66,7 @@
//!
//! ```rust
//! use common_access_token::{
//! Algorithm, KeyId, RegisteredClaims, TokenBuilder, VerificationOptions,
//! Algorithm, KeyId, RegisteredClaims, TokenBuilder, VerificationOptions, VerifyingKey,
//! cat_keys, catm, catr, catreplay, catu, uri_components, current_timestamp,
//! cattprint, FingerprintType
//! };
Expand Down Expand Up @@ -113,7 +113,7 @@
//! .expect("Failed to decode token");
//!
//! // Verify signature
//! decoded_token.verify(key).expect("Failed to verify signature");
//! decoded_token.verify_with_key(VerifyingKey::HmacSha256(key)).expect("Failed to verify signature");
//!
//! // Verify standard claims and CAT-specific claims
//! let options = VerificationOptions::new()
Expand Down Expand Up @@ -148,7 +148,7 @@ pub use constants::{
replay_values, tprint_params, uri_components, FingerprintType,
};
pub use error::Error;
pub use header::{Algorithm, AlgorithmClass, CborValue, Header, HeaderMap, KeyId};
pub use header::{Algorithm, AlgorithmClass, CborValue, Header, HeaderMap, KeyId, VerifyingKey};
pub use token::{Token, TokenBuilder, VerificationOptions};
pub use utils::current_timestamp;

Expand Down
Loading
Loading