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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ jobs:
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

msrv:
# Verify the crate actually builds with the MSRV declared in Cargo.toml.
# The dependency graph (RustCrypto + minicbor-derive) requires a recent
# compiler, so this guards against the declared rust-version going stale.
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Install MSRV toolchain
uses: dtolnay/rust-toolchain@1.88.0
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - Unreleased

### Added

- **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
verification support, producing/consuming `COSE_Sign1` structures.
- `Algorithm::Es256` and `Algorithm::Ps256` variants, plus their COSE algorithm
identifiers (`-7` and `-37`).
- `Error::InvalidKey` for malformed DER key material.
- `examples/asymmetric_signing.rs` and `examples/sample_es256_ps256_tokens.rs`
demonstrating the new asymmetric algorithms and key formats.

### Changed

- **BREAKING:** `Algorithm` and `Error` are now `#[non_exhaustive]`. Downstream
`match` expressions over either enum must include a wildcard arm. This is a
one-time break that allows future variants (additional algorithms or error
cases) to be added without further breakage.
- **BREAKING:** Minimum supported Rust version (MSRV) raised to `1.88.0`,
required by the asymmetric crypto dependencies.
- Tokens are tagged according to their algorithm: HMAC (MAC) algorithms use
`COSE_Mac0` (tag 17) while asymmetric signature algorithms use `COSE_Sign1`
(tag 18). `Token::from_bytes` accepts both tagged and untagged input.

## [0.2.7] - 2025-11-05

### Added

- **CATTPRINT** (TLS Fingerprint) claim support, including verification of
fingerprint type and value.
- `FingerprintType` enum representing the supported TLS fingerprint types.

### Changed

- TLS fingerprint values are matched case-insensitively (compared in lowercase).
- Updated dependencies.

### Fixed

- Corrected error messages and comments around fingerprint matching.
- Handle signed integer CBOR types when decoding claims.

## [0.2.6] - 2025-10-30

### Added

- **CATU** URI component matching for `FILENAME`, `STEM`, and `PARENT_PATH`.
- Additional tests and documentation for CATU URI components.
- GitHub CI workflow.

### Changed

- Updated dependencies.

[0.3.0]: https://github.com/fastly/rust-cat/compare/0.2.7...HEAD
[0.2.7]: https://github.com/fastly/rust-cat/compare/0.2.6...0.2.7
[0.2.6]: https://github.com/fastly/rust-cat/releases/tag/0.2.6
13 changes: 10 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "common-access-token"
version = "0.2.7"
version = "0.3.0"
edition = "2021"
description = "Implementation of the Common Access Token (CAT) specification"
authors = ["Frank Denis"]
Expand All @@ -11,13 +11,20 @@ repository = "https://github.com/fastly/rust-cat"
readme = "README.md"
keywords = ["token", "authentication", "authorization", "cbor", "cose"]
categories = ["authentication", "cryptography", "web-programming"]
rust-version = "1.60.0"
rust-version = "1.88.0"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the PR description was stale. The code is correct at 1.88.0; I've updated the description to match and explain why (uncommitted Cargo.lock → downstream re-resolves to minicbor-derive 0.19.4 which needs let-chains/1.88, and zeroize 1.9 needs edition 2024/1.85). The pinned msrv CI job already builds+tests on 1.88.0, so all three (Cargo.toml, CI, description) are now consistent.


[dependencies]
minicbor = { version = "2.1.3", features = ["derive", "std"] }
hmac-sha256 = "1.1.12"
hmac-sha512 = "1.1.7"
thiserror = "1.0.65"
thiserror = "2"
ct-codecs = "1.1.6"
url = "2.5.2"
regex = "1.9.6"
# ES256 (ECDSA P-256 + SHA-256, COSE alg -7)
p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] }
# PS256 (RSASSA-PSS + SHA-256, COSE alg -37)
rsa = { version = "0.9", features = ["sha2"] }
sha2 = "0.10"
# RNG for PSS salt generation (WASM-friendly via getrandom)
rand_core = { version = "0.6", features = ["getrandom"] }
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This library uses the [minicbor](https://crates.io/crates/minicbor) crate for CB
- Custom Claims: Support for application-specific claims with string, binary, integer, and nested map values
- CAT-specific Claims: Support for CAT-specific claims like CATU (URI restrictions), CATR (token renewal), CATM (HTTP methods), and more
- Key Identifiers: Support for both binary and string key identifiers (kid)
- HMAC-SHA256 Authentication: Secure token signing and verification
- Multiple Signing Algorithms: HMAC-SHA256 (HS256) MACs, plus ECDSA P-256 (ES256) and RSASSA-PSS (PS256) asymmetric signatures
- Comprehensive Verification: Validate signatures, expiration times, and other claims
- CAT-specific Validation: Validate URI restrictions, HTTP method constraints, and replay protection

Expand All @@ -31,7 +31,7 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
common-access-token = "0.2"
common-access-token = "0.3"
```

## Usage
Expand Down Expand Up @@ -157,6 +157,40 @@ COSE_Mac0 = [
]
```

Asymmetric signature algorithms (ES256, PS256) instead use the COSE_Sign1 structure with CWT (tag 61) and COSE_Sign1 (tag 18) CBOR tags. The array layout is identical; only the final element is a signature rather than a MAC.

### 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:

| 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 |

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.

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

// `private_key` is PKCS#8 DER; `public_key` is SPKI DER.
let token = TokenBuilder::new()
.algorithm(Algorithm::Es256)
.protected_key_id(KeyId::string("my-ec-key"))
.registered_claims(
RegisteredClaims::new()
.with_issuer("example-issuer")
.with_expiration(current_timestamp() + 3600),
)
.sign(private_key)
.expect("Failed to sign token");

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");
```

### Nested Map Claims

You can include complex structured data in tokens using nested maps:
Expand Down
102 changes: 102 additions & 0 deletions examples/asymmetric_signing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Example: signing and verifying tokens with the asymmetric ES256 and PS256
//! algorithms.
//!
//! Unlike HMAC (HS256), these algorithms use a key pair: the token is signed
//! with a private key and verified with the corresponding public key, using the
//! COSE_Sign1 structure.
//!
//! Keys are interpreted as DER:
//! - signing key: PKCS#8 DER-encoded private key
//! - verifying key: SPKI DER-encoded public key
//!
//! The keys below were generated once with OpenSSL and embedded as base64 so the
//! example is self-contained:
//!
//! ES256 (P-256):
//! openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out es.pem
//! openssl pkcs8 -topk8 -nocrypt -in es.pem -outform DER # private (PKCS#8)
//! openssl pkey -in es.pem -pubout -outform DER # public (SPKI)
//!
//! PS256 (RSA-2048):
//! openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform DER \
//! | openssl pkcs8 -topk8 -nocrypt -inform DER -outform DER # private (PKCS#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,
};
use ct_codecs::{Base64, Decoder};

// ⚠️ DEMO KEYS — DO NOT USE IN PRODUCTION.
// These private keys are committed to a public repository and are therefore
// publicly known. They exist only so this example is self-contained. Generate
// your own key pair (see the OpenSSL commands above) for any real use.
const ES256_PRIVATE_KEY_B64: &str = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg7BOlgwBOMKscTUCaG3RmlSCgUznDdxMn+9Pvoqp4pUOhRANCAARWMcvR3DnF1U15IvgcOyAxr3pJPfOHcF7ESuY+H+ya3LCH03PC1d99/XgN1ldF+wmMxVhY0w9iop10N6tNZDTg";
const ES256_PUBLIC_KEY_B64: &str = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVjHL0dw5xdVNeSL4HDsgMa96ST3zh3BexErmPh/smtywh9NzwtXfff14DdZXRfsJjMVYWNMPYqKddDerTWQ04A==";

const PS256_PRIVATE_KEY_B64: &str = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCHjA5iauwvo2sRB529iV1c+p+WuGFzk5EUGFFLYoIHxAwo/rSmZ2/D00epwb4WzOxA4c8+1QA+0rZIN35Fti9Wiunt0b1DgC0tuSglNzpEE5gjhTDcAWZOBPOCMt9pKEuuQC4eqBRxPoG5Y14dVi46/aQOQSqU5I0T3cbeLliTzjXkrvdqySFXMGpM9/I469SZRxZbDgB8wUcB2nTIuwOokjN/Vp+BpMM5QmR66J6aFNi8LqCmQv3grUI1kM1fqrC3az/YcyXcDvjinagyXsGYgW2ZpXIf2760UXv/bASAOO01sgI8zxbIDdG6Vd+7iPhr4b/v6QIj6rpuURFfns2LAgMBAAECggEAH9CdXbdYCZRzYHNnsGGqEtVWmQNdCEo2Lr/IcQfFmnoHGqYyE67Kmm2gb/VkHyjpOQ9nXAmVvakqlMfFsSoicU84uhPVNx9CO22uwRF18R2iQ5ATGEiR0TUzTLeRHbcSEGvLB3IPHkd8Hl327K7aOglntNrR2lHM1UFkWKkLLGHObPoLBSTQLjX5JkvtpUuBgnPVlfBUc5al9+CH+m/SiC4BvVWo4hiHEKCQgMIQ/Dh8UtS9Vk91FIizqKpqBXE6+PNmAnn9ZwRjZoRNBSLn0paAyiEXXdr5rV8zeYU0ktY40J9qWEFOJmTYII4pUK1U8tukrQ0w4LUm17f8zMkufQKBgQC7MGcSrbFWVjlEwA760sG6NKOZb5sL+2etIVAJyfSoGrwr8H4aQA1WFP+pmmlCWsLZj8qfTYSyocwfT/p9aY9Na7ftyks+q1QSsDF+D7frgxmITJeCSwiPa7jnOTrmReqAEOyPn8IlytHIhJbaPxzDxPf572QIAIBgsWhdygn21QKBgQC5X9agS0u2Joypz36ZIilbgbtgmSvFAE/22U0il+3GgXQbjmxPCip1UZm1cBgmLhq12bxU1xYxJpGVPWhEsmkIrOkEfNf/RYlSvVLzbuZLQxeB1g5FDrFb1EbaegrFznv/rFonyXMeRyJ7PHtDttfN5jxNTxTiV3BQ4uobgsai3wKBgFEiW6q26mSXnt7zuApzi1CgPEDnJPb+kyNxivWTOZ4baHBLHv1VwfILy/zBVtpR6J7QOmzt9pROmOEBk3sEY/6Ur/Y7dn3FWP14rRsMyRUlj82KFSl+SEmR0WU3YxYoO8oii8Z84nPrAx68iX4zWM5p82m7n0nwnbRLcQcl6Ue5AoGACjVN42viEnjS/DLx/MrVzjU5tVsZ/vJCdQyIY+RL8seENlREgKHFrso8lbJDki6tx9/isCVcEn7WO4qzKD1O7WxgNKAPYP5aTpUgcUllIzXhoIPCK2lguPbapANefoAdcfnyyQgd78fpDTJKc3MpNSx9m6BEPSalh77HN5afC68CgYBSHR2vz1GuUzHSgU+3xKqGSc+jlroetJ1dC5913Z+9eawW7QrRfmSod+JfEiJSw8eS+5/rGYjKihMtNPyqzadRvZtp0QGZrrm1k1/vqqeeH5Uq6AgH/2Djql4tUvC3gmgpHjY7RyPDv6v+u+L9C6MP0Nu5vVfQwpAmX9bsjn/Tjw==";
const PS256_PUBLIC_KEY_B64: &str = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh4wOYmrsL6NrEQedvYldXPqflrhhc5ORFBhRS2KCB8QMKP60pmdvw9NHqcG+FszsQOHPPtUAPtK2SDd+RbYvVorp7dG9Q4AtLbkoJTc6RBOYI4Uw3AFmTgTzgjLfaShLrkAuHqgUcT6BuWNeHVYuOv2kDkEqlOSNE93G3i5Yk8415K73askhVzBqTPfyOOvUmUcWWw4AfMFHAdp0yLsDqJIzf1afgaTDOUJkeuiemhTYvC6gpkL94K1CNZDNX6qwt2s/2HMl3A744p2oMl7BmIFtmaVyH9u+tFF7/2wEgDjtNbICPM8WyA3RulXfu4j4a+G/7+kCI+q6blERX57NiwIDAQAB";

fn main() {
demo(
"ES256 (ECDSA P-256 + SHA-256)",
Algorithm::Es256,
ES256_PRIVATE_KEY_B64,
ES256_PUBLIC_KEY_B64,
"ec-key-1",
);

demo(
"PS256 (RSASSA-PSS + SHA-256)",
Algorithm::Ps256,
PS256_PRIVATE_KEY_B64,
PS256_PUBLIC_KEY_B64,
"rsa-key-1",
);
}

fn demo(name: &str, alg: Algorithm, private_key_b64: &str, public_key_b64: &str, kid: &str) {
let private_key = Base64::decode_to_vec(private_key_b64, None).expect("valid private key");
let public_key = Base64::decode_to_vec(public_key_b64, None).expect("valid public key");

let now = current_timestamp();
let token = TokenBuilder::new()
.algorithm(alg)
.protected_key_id(KeyId::string(kid))
.registered_claims(
RegisteredClaims::new()
.with_issuer("example-issuer")
.with_subject("example-subject")
.with_audience("example-audience")
.with_expiration(now + 3600)
.with_not_before(now)
.with_issued_at(now),
)
.custom_string(100, "custom-string-value")
.sign(&private_key)
.expect("Failed to sign token");

let token_bytes = token.to_bytes().expect("Failed to encode token");
println!(
"{name}: signed token is {} bytes ({}-byte signature)",
token_bytes.len(),
token.signature.len()
);

// The verifier only needs the public key.
let decoded = Token::from_bytes(&token_bytes).expect("Failed to decode token");
decoded
.verify(&public_key)
.expect("Failed to verify signature");

let options = VerificationOptions::new()
.verify_exp(true)
.verify_nbf(true)
.expected_issuer("example-issuer")
.expected_audience("example-audience");
decoded
.verify_claims(&options)
.expect("Failed to verify claims");

println!("{name}: signature and claims verified successfully\n");
}
108 changes: 108 additions & 0 deletions examples/sample_es256_ps256_tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Mint sample ES256 and PS256 tokens with a ~10-year expiration and print
//! them in hex and base64url.
//! Run with: `cargo run --example sample_es256_ps256_tokens`.
//!
//! The key pairs are the same ones used by the test suite (PKCS#8 DER private
//! keys, SPKI DER public keys, base64-encoded here so the example is
//! self-contained). The public keys are printed so the tokens can be verified
//! elsewhere.

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

// ⚠️ DEMO KEYS — DO NOT USE IN PRODUCTION.
// These private keys are committed to a public repository (they are the same
// keys used by the test suite) and are therefore publicly known. They exist
// only so this example is self-contained. Generate your own key pair for any
// real use.
const ES256_PRIVATE_KEY_B64: &str = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg7BOlgwBOMKscTUCaG3RmlSCgUznDdxMn+9Pvoqp4pUOhRANCAARWMcvR3DnF1U15IvgcOyAxr3pJPfOHcF7ESuY+H+ya3LCH03PC1d99/XgN1ldF+wmMxVhY0w9iop10N6tNZDTg";
const ES256_PUBLIC_KEY_B64: &str = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVjHL0dw5xdVNeSL4HDsgMa96ST3zh3BexErmPh/smtywh9NzwtXfff14DdZXRfsJjMVYWNMPYqKddDerTWQ04A==";

const PS256_PRIVATE_KEY_B64: &str = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCHjA5iauwvo2sRB529iV1c+p+WuGFzk5EUGFFLYoIHxAwo/rSmZ2/D00epwb4WzOxA4c8+1QA+0rZIN35Fti9Wiunt0b1DgC0tuSglNzpEE5gjhTDcAWZOBPOCMt9pKEuuQC4eqBRxPoG5Y14dVi46/aQOQSqU5I0T3cbeLliTzjXkrvdqySFXMGpM9/I469SZRxZbDgB8wUcB2nTIuwOokjN/Vp+BpMM5QmR66J6aFNi8LqCmQv3grUI1kM1fqrC3az/YcyXcDvjinagyXsGYgW2ZpXIf2760UXv/bASAOO01sgI8zxbIDdG6Vd+7iPhr4b/v6QIj6rpuURFfns2LAgMBAAECggEAH9CdXbdYCZRzYHNnsGGqEtVWmQNdCEo2Lr/IcQfFmnoHGqYyE67Kmm2gb/VkHyjpOQ9nXAmVvakqlMfFsSoicU84uhPVNx9CO22uwRF18R2iQ5ATGEiR0TUzTLeRHbcSEGvLB3IPHkd8Hl327K7aOglntNrR2lHM1UFkWKkLLGHObPoLBSTQLjX5JkvtpUuBgnPVlfBUc5al9+CH+m/SiC4BvVWo4hiHEKCQgMIQ/Dh8UtS9Vk91FIizqKpqBXE6+PNmAnn9ZwRjZoRNBSLn0paAyiEXXdr5rV8zeYU0ktY40J9qWEFOJmTYII4pUK1U8tukrQ0w4LUm17f8zMkufQKBgQC7MGcSrbFWVjlEwA760sG6NKOZb5sL+2etIVAJyfSoGrwr8H4aQA1WFP+pmmlCWsLZj8qfTYSyocwfT/p9aY9Na7ftyks+q1QSsDF+D7frgxmITJeCSwiPa7jnOTrmReqAEOyPn8IlytHIhJbaPxzDxPf572QIAIBgsWhdygn21QKBgQC5X9agS0u2Joypz36ZIilbgbtgmSvFAE/22U0il+3GgXQbjmxPCip1UZm1cBgmLhq12bxU1xYxJpGVPWhEsmkIrOkEfNf/RYlSvVLzbuZLQxeB1g5FDrFb1EbaegrFznv/rFonyXMeRyJ7PHtDttfN5jxNTxTiV3BQ4uobgsai3wKBgFEiW6q26mSXnt7zuApzi1CgPEDnJPb+kyNxivWTOZ4baHBLHv1VwfILy/zBVtpR6J7QOmzt9pROmOEBk3sEY/6Ur/Y7dn3FWP14rRsMyRUlj82KFSl+SEmR0WU3YxYoO8oii8Z84nPrAx68iX4zWM5p82m7n0nwnbRLcQcl6Ue5AoGACjVN42viEnjS/DLx/MrVzjU5tVsZ/vJCdQyIY+RL8seENlREgKHFrso8lbJDki6tx9/isCVcEn7WO4qzKD1O7WxgNKAPYP5aTpUgcUllIzXhoIPCK2lguPbapANefoAdcfnyyQgd78fpDTJKc3MpNSx9m6BEPSalh77HN5afC68CgYBSHR2vz1GuUzHSgU+3xKqGSc+jlroetJ1dC5913Z+9eawW7QrRfmSod+JfEiJSw8eS+5/rGYjKihMtNPyqzadRvZtp0QGZrrm1k1/vqqeeH5Uq6AgH/2Djql4tUvC3gmgpHjY7RyPDv6v+u+L9C6MP0Nu5vVfQwpAmX9bsjn/Tjw==";
const PS256_PUBLIC_KEY_B64: &str = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh4wOYmrsL6NrEQedvYldXPqflrhhc5ORFBhRS2KCB8QMKP60pmdvw9NHqcG+FszsQOHPPtUAPtK2SDd+RbYvVorp7dG9Q4AtLbkoJTc6RBOYI4Uw3AFmTgTzgjLfaShLrkAuHqgUcT6BuWNeHVYuOv2kDkEqlOSNE93G3i5Yk8415K73askhVzBqTPfyOOvUmUcWWw4AfMFHAdp0yLsDqJIzf1afgaTDOUJkeuiemhTYvC6gpkL94K1CNZDNX6qwt2s/2HMl3A744p2oMl7BmIFtmaVyH9u+tFF7/2wEgDjtNbICPM8WyA3RulXfu4j4a+G/7+kCI+q6blERX57NiwIDAQAB";

// ~10 years in seconds (10 * 365.25 days).
const TEN_YEARS_SECS: u64 = 315_576_000;

fn main() {
let now = current_timestamp();
let exp = now + TEN_YEARS_SECS;
println!("issued_at (iat): {now}");
println!("expiration (exp): {exp} (~10 years from now)\n");

mint(
"ES256 (ECDSA P-256 + SHA-256)",
Algorithm::Es256,
ES256_PRIVATE_KEY_B64,
ES256_PUBLIC_KEY_B64,
"es256-sample-key",
now,
exp,
);

mint(
"PS256 (RSASSA-PSS + SHA-256)",
Algorithm::Ps256,
PS256_PRIVATE_KEY_B64,
PS256_PUBLIC_KEY_B64,
"ps256-sample-key",
now,
exp,
);
}

#[allow(clippy::too_many_arguments)]
fn mint(
name: &str,
alg: Algorithm,
private_key_b64: &str,
public_key_b64: &str,
kid: &str,
iat: u64,
exp: u64,
) {
let private_key = Base64::decode_to_vec(private_key_b64, None).expect("valid private key");
let public_key = Base64::decode_to_vec(public_key_b64, None).expect("valid public key");

let token = TokenBuilder::new()
.algorithm(alg)
.protected_key_id(KeyId::string(kid))
.registered_claims(
RegisteredClaims::new()
.with_issuer("example-issuer")
.with_subject("example-subject")
.with_audience("example-audience")
.with_issued_at(iat)
.with_not_before(iat)
.with_expiration(exp),
)
.sign(&private_key)
.expect("Failed to sign token");

let token_bytes = token.to_bytes().expect("Failed to encode token");
let hex = Hex::encode_to_string(&token_bytes).expect("hex encode");
let b64url = Base64UrlSafeNoPadding::encode_to_string(&token_bytes).expect("b64url encode");

// 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");
decoded
.verify_claims(
&VerificationOptions::new()
.verify_exp(true)
.verify_nbf(true)
.expected_issuer("example-issuer")
.expected_audience("example-audience"),
)
.expect("verify claims");

println!("=== {name} ===");
println!("kid: {kid}");
println!("public key (SPKI DER, base64): {public_key_b64}");
println!("token length: {} bytes", token_bytes.len());
println!("token (hex): {hex}");
println!("token (b64url): {b64url}");
println!();
}
4 changes: 4 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,8 @@ pub mod cose_labels {
pub mod cose_algs {
/// HMAC with SHA-256 (COSE algorithm identifier: 5)
pub const HMAC_SHA_256: i32 = 5;
/// ECDSA using P-256 curve and SHA-256 (COSE algorithm identifier: -7)
pub const ES256: i32 = -7;
/// RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (COSE algorithm identifier: -37)
pub const PS256: i32 = -37;
}
Loading
Loading