Add ES256 and PS256 signing algorithm support#5
Conversation
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>
There was a problem hiding this comment.
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 useis_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.
| 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" | ||
| ); | ||
| } |
| // Flip a byte near the end of the payload region (before the signature). | ||
| let idx = token_bytes.len() - 70; | ||
| token_bytes[idx] ^= 0xFF; |
| 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"; |
| 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"; |
|
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. I also verified that the branch does build and test successfully on the latest local toolchain, and Before this merges, the PR should either constrain the new dependency graph so it actually builds with Rust 1.72, or raise 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. |
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.
sign()keyverify()keyHmacSha256Es256Ps256Changes
Algorithmenum (header.rs): addsEs256/Ps256, updatesidentifier()/from_identifier(), adds anis_mac()helper to distinguish COSE_Mac0 from COSE_Sign1 algorithms.p256,rsa,sha2): ES256 is ECDSA P-256 + SHA-256 (deterministic RFC 6979, 64-byter || ssignatures); PS256 is RSASSA-PSS + SHA-256/MGF1 usingOsRngfor the salt (WASM-friendly viagetrandom).sign()/verify()dispatch per algorithm; asymmetric algorithms reuse the existing COSE_Sign1 input and emit CWT tag 61 + COSE_Sign1 tag 18.&[u8]API preserved — PKCS#8 DER private keys for signing, SPKI DER public keys for verifying. NewError::InvalidKey(error.rs) for malformed keys.thiserrorto2; bumprust-versionto1.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 passcargo clippy --all-targets— cleancargo fmt --check— cleancargo run --example asymmetric_signing— ES256 and PS256 both sign and verify🤖 Generated with Claude Code