Skip to content

Add ES256 and PS256 signing algorithm support#5

Open
zshenker wants to merge 2 commits into
mainfrom
add-es256-ps256-support
Open

Add ES256 and PS256 signing algorithm support#5
zshenker wants to merge 2 commits into
mainfrom
add-es256-ps256-support

Conversation

@zshenker

Copy link
Copy Markdown
Collaborator

Summary

The Common Access Token spec supports the ES256 and PS256 signing algorithms in addition to HS256. This PR adds both signing and verification for those two asymmetric algorithms. HS256 (HMAC-SHA256) behavior is unchanged.

Algorithm COSE id Structure sign() key verify() key
HmacSha256 5 COSE_Mac0 raw symmetric key raw symmetric key
Es256 -7 COSE_Sign1 PKCS#8 DER private SPKI DER public
Ps256 -37 COSE_Sign1 PKCS#8 DER private SPKI DER public

Changes

  • Algorithm enum (header.rs): adds Es256 / Ps256, updates identifier()/from_identifier(), adds an is_mac() helper to distinguish COSE_Mac0 from COSE_Sign1 algorithms.
  • Crypto primitives (utils.rs) via RustCrypto (p256, rsa, sha2): ES256 is ECDSA P-256 + SHA-256 (deterministic RFC 6979, 64-byte r || s signatures); PS256 is RSASSA-PSS + SHA-256/MGF1 using OsRng for the salt (WASM-friendly via getrandom).
  • Token wiring (token.rs): sign()/verify() dispatch per algorithm; asymmetric algorithms reuse the existing COSE_Sign1 input and emit CWT tag 61 + COSE_Sign1 tag 18.
  • Key format: existing &[u8] API preserved — PKCS#8 DER private keys for signing, SPKI DER public keys for verifying. New Error::InvalidKey (error.rs) for malformed keys.
  • Tests (tests.rs): 13 new tests — round-trip sign/verify, COSE_Sign1 tag bytes, wrong-key & cross-algorithm rejection, payload tampering, PSS salt randomization, invalid-key errors, and identifier round-trips.
  • Docs/example: new examples/asymmetric_signing.rs, README signing-algorithms section, updated crate docs.
  • Deps: bump thiserror to 2; bump rust-version to 1.72 (required by the RustCrypto crates). The crypto stack uses current stable RustCrypto versions (the newer 0.14/0.10 releases are pre-1.0 release candidates).

Verification

  • cargo test — 42 lib tests + 73 doctests pass
  • cargo clippy --all-targets — clean
  • cargo fmt --check — clean
  • cargo run --example asymmetric_signing — ES256 and PS256 both sign and verify

🤖 Generated with Claude Code

zshenker and others added 2 commits June 22, 2026 14:36
The Common Access Token spec supports the ES256 and PS256 signing
algorithms in addition to HS256. This adds both signing and verification
for those asymmetric algorithms.

- Algorithm enum gains Es256 (COSE -7) and Ps256 (COSE -37) plus an
  is_mac() helper to distinguish COSE_Mac0 from COSE_Sign1 algorithms
- Signing/verification primitives via RustCrypto (p256, rsa, sha2);
  ES256 uses deterministic RFC 6979, PS256 uses OsRng for the PSS salt
- token sign()/verify() dispatch per algorithm; asymmetric algorithms
  use COSE_Sign1 with CWT tag 61 + tag 18
- Keys reuse the existing &[u8] API: PKCS#8 DER private keys for signing,
  SPKI DER public keys for verifying; new Error::InvalidKey for malformed keys
- 13 new tests covering round-trip, wrong-key/cross-algorithm rejection,
  payload tampering, PSS salt randomization, and invalid-key handling
- New asymmetric_signing example, README signing-algorithms table, and
  updated crate docs
- Bump thiserror to 2 and rust-version to 1.72 (required by RustCrypto)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generates ES256 and PS256 tokens with a ~10-year expiration and prints
them in hex and base64url, alongside their public keys. Each token is
self-verified (signature and claims) before printing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the Common Access Token crate to support asymmetric signing and verification using ES256 (P-256 ECDSA) and PS256 (RSA-PSS), in addition to the existing HS256 (HMAC-SHA256) support. It updates the algorithm model, token encoding/verification dispatch, cryptographic primitives, and adds tests/examples/docs to cover the new functionality.

Changes:

  • Add Algorithm::{Es256, Ps256} (+ COSE identifiers) and use is_mac() to select COSE_Mac0 vs COSE_Sign1.
  • Implement ES256/PS256 signing + verification utilities using RustCrypto (p256, rsa, sha2, rand_core).
  • Add asymmetric round-trip tests and new examples/docs demonstrating key formats and COSE tags.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/utils.rs Adds ES256/PS256 compute + verify helpers with PKCS#8/SPKI DER key parsing.
src/token.rs Tags tokens as COSE_Mac0 (HS256) vs COSE_Sign1 (ES256/PS256) and dispatches sign/verify accordingly.
src/tests.rs Adds asymmetric algorithm tests (round-trips, tag bytes, wrong-key, tampering, invalid-key errors).
src/header.rs Extends Algorithm enum with identifiers and is_mac() helper.
src/constants.rs Adds COSE algorithm identifier constants for ES256/PS256.
src/error.rs Introduces Error::InvalidKey for malformed DER keys.
src/lib.rs Updates crate-level docs to mention ES256/PS256 support.
README.md Documents the new algorithms, key formats, and COSE_Sign1 usage.
examples/asymmetric_signing.rs Adds an example of signing/verifying with ES256/PS256.
examples/sample_es256_ps256_tokens.rs Adds an example to mint and print sample ES256/PS256 tokens.
Cargo.toml Adds crypto dependencies and bumps rust-version / thiserror.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tests.rs
Comment on lines +1554 to +1569
let wrong_public_key = ct_codecs::Base64::decode_to_vec(
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqM7q0vY3RfQ8vJpV4hQ4Z0H8K7m2xZ1k9d0c8N3vWf6m1pQ2rX5sT8uY9wA0bC1dE2fG3hI4jK5lM6nO7pPqQ==",
None,
);
// Even if decoding the bogus key fails or it's structurally invalid, the
// point is verification must not succeed against the real signature.
let token = build_signed_token(Algorithm::Es256, &private_key);
let token_bytes = token.to_bytes().expect("Failed to encode token");
let decoded = Token::from_bytes(&token_bytes).expect("Failed to decode token");

if let Ok(wrong_key) = wrong_public_key {
assert!(
decoded.verify(&wrong_key).is_err(),
"Verification should fail with a non-matching public key"
);
}
Comment thread src/tests.rs
Comment on lines +1600 to +1602
// Flip a byte near the end of the payload region (before the signature).
let idx = token_bytes.len() - 70;
token_bytes[idx] ^= 0xFF;
Comment on lines +30 to +34
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";
Comment on lines +15 to +19
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";
@jedisct1

Copy link
Copy Markdown
Collaborator

Thanks for putting this together. I reviewed the PR description, the changed files, the existing review comments, the token/header/utils implementation around the diff, and the current CI results. This is an automated review by Swival (https://swival.dev) using only open-source models, intended to help both the PR author and the maintainers.

My recommendation is not to merge this yet.

The main blocker is that the PR advertises an MSRV that the dependency graph no longer satisfies. Cargo.toml:14 sets rust-version = "1.72.0", while Cargo.toml:24-30 adds the RustCrypto stack through p256, rsa, sha2, and rand_core. With Rust/Cargo 1.72.0 on this branch, cargo check fails before compiling this crate because the resolver selects zeroize v1.9.0, pulled through the new crypto dependencies, and that crate's manifest uses the 2024 edition, which Cargo 1.72 cannot parse.

I also verified that the branch does build and test successfully on the latest local toolchain, and cargo fmt --check plus cargo clippy --all-targets -- -D warnings pass there. That does not cover the compatibility contract in Cargo.toml, though. Since this is a library crate and Cargo.lock is not committed, downstream users building with the declared Rust version can hit the same dependency resolution failure. That makes the MSRV bump inaccurate and creates a real compatibility regression for users who rely on the crate metadata.

Before this merges, the PR should either constrain the new dependency graph so it actually builds with Rust 1.72, or raise rust-version to the real minimum supported compiler and make that intentional in the PR. If the project wants to keep the lower MSRV, a direct dependency constraint or a different crypto dependency choice may be needed, because simply having CI pass on latest stable does not prove the published crate is usable with its declared minimum.

I would keep the PR open because the core ES256 and PS256 direction appears plausible and the new tests exercise the main signing and verification paths. The compatibility issue above needs to be fixed first, and the existing comments about the embedded demo private keys in the examples are also worth addressing before merge, even though I see those as secondary to the MSRV regression.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants