diff --git a/CHANGELOG.md b/CHANGELOG.md index c137f7f..af09021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased — Rust cryptography + +- Upgrade to p256 0.14 and require crypto-bigint >= 0.7.6 to address + RUSTSEC-2026-0387. Key generation uses the fallible OS-randomness API. +- Preserve PEM/JWK encodings, key IDs, credential DER signatures, and persisted + TOFU pins; add fixed vectors produced by the unmodified 0.3.0 SDK. +- Rust callers that name the public P-256 key types must also use p256 0.14. + This dependency update does not change the protocol on this branch. +- Require hickory-resolver >= 0.26.1 so downstream DNS-feature builds cannot + retain the older release affected by RUSTSEC-2026-0119. + All notable changes to the AgentPin project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), diff --git a/crates/agentpin/Cargo.toml b/crates/agentpin/Cargo.toml index 2305bea..6c647ac 100644 --- a/crates/agentpin/Cargo.toml +++ b/crates/agentpin/Cargo.toml @@ -16,9 +16,11 @@ fetch = ["reqwest", "tokio", "async-trait"] dns = ["hickory-resolver", "tokio", "async-trait"] [dependencies] -p256 = { version = "0.13", features = ["ecdsa", "pem", "jwk"] } -# OS randomness via the rand_core line p256 already depends on; the `rand` -# 0.8 facade is not needed and carries an unsoundness advisory (AP-22). +p256 = { version = "0.14", features = ["ecdsa", "pem"] } +# Keep consumers' fresh resolutions above RUSTSEC-2026-0387's patched floor. +crypto-bigint = "0.7.6" +# OS randomness for mutual-authentication challenges, without the rand facade. +# Key generation uses p256's fallible built-in OS randomness API. rand_core = { version = "0.6", features = ["getrandom"] } sha2 = "0.10" base64 = "0.21" @@ -36,7 +38,7 @@ tokio = { version = "1.0", optional = true } tempfile = { version = "3.0", optional = true } # Optional dependency for DNS TXT cross-verification (v0.3.0) -hickory-resolver = { version = "0.26", optional = true } +hickory-resolver = { version = "0.26.1", optional = true } [dev-dependencies] tempfile = "3.0" diff --git a/crates/agentpin/src/crypto.rs b/crates/agentpin/src/crypto.rs index 1d9ae18..cf06413 100644 --- a/crates/agentpin/src/crypto.rs +++ b/crates/agentpin/src/crypto.rs @@ -1,10 +1,10 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use p256::{ ecdsa::{signature::Signer, signature::Verifier, Signature, SigningKey, VerifyingKey}, + elliptic_curve::Generate, pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}, SecretKey, }; -use rand_core::OsRng; use sha2::{Digest, Sha256}; use crate::error::Error; @@ -38,7 +38,7 @@ impl Drop for KeyPair { /// Generate a new ECDSA P-256 keypair. pub fn generate_key_pair() -> Result { - let secret_key = SecretKey::random(&mut OsRng); + let secret_key = SecretKey::try_generate().map_err(|e| Error::Ecdsa(e.to_string()))?; let signing_key = SigningKey::from(&secret_key); let verifying_key = signing_key.verifying_key(); diff --git a/crates/agentpin/src/jwk.rs b/crates/agentpin/src/jwk.rs index 70cfcc3..b06d7d6 100644 --- a/crates/agentpin/src/jwk.rs +++ b/crates/agentpin/src/jwk.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD as BASE64URL, Engine}; use p256::{ ecdsa::VerifyingKey, - elliptic_curve::sec1::ToEncodedPoint, + elliptic_curve::sec1::ToSec1Point, pkcs8::{DecodePublicKey, EncodePublicKey}, PublicKey, }; @@ -28,7 +28,7 @@ pub struct Jwk { /// Convert a VerifyingKey to a JWK with the given key ID. pub fn verifying_key_to_jwk(key: &VerifyingKey, kid: &str) -> Jwk { let public_key = PublicKey::from(key); - let point = public_key.to_encoded_point(false); + let point = public_key.to_sec1_point(false); let x = BASE64URL.encode(point.x().expect("x coordinate")); let y = BASE64URL.encode(point.y().expect("y coordinate")); diff --git a/crates/agentpin/tests/fixtures/crypto-0.3.0.json b/crates/agentpin/tests/fixtures/crypto-0.3.0.json new file mode 100644 index 0000000..983649e --- /dev/null +++ b/crates/agentpin/tests/fixtures/crypto-0.3.0.json @@ -0,0 +1,37 @@ +{ + "attestation": { + "agent_id": "urn:agentpin:example.com:maker", + "attestation": "MEUCIFT5A7dibJA3BUch8jAbSusOCTRheGWTToK1nRr/KRseAiEA+FOspZnO5E6GS1cDlcRklGHWrobyTRGHJ0vrBMNMl94=", + "domain": "example.com", + "kid": "legacy-key", + "role": "maker" + }, + "jwk": { + "crv": "P-256", + "key_ops": [ + "verify" + ], + "kid": "legacy-key", + "kty": "EC", + "use": "sig", + "x": "FNB7ookKIfJ_V6gdBshjT3sVXVvNACTYjFL6WPJ2PkM", + "y": "BoIGv8P-XUN2wWzp_PyH254FKZwpnVtR3Yf1T2M3_iE" + }, + "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImFnZW50cGluLWNyZWRlbnRpYWwrand0Iiwia2lkIjoibGVnYWN5LWtleSJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsInN1YiI6InVybjphZ2VudHBpbjpleGFtcGxlLmNvbTphZ2VudCIsImF1ZCI6InNjYW5uZXIuZXhhbXBsZSIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI5MjAwLCJqdGkiOiJsZWdhY3ktdmVjdG9yIiwiYWdlbnRwaW5fdmVyc2lvbiI6IjAuMSIsImNhcGFiaWxpdGllcyI6WyJyZWFkOmRhdGEiXX0.MEYCIQCD5WjE3_2WC47mq_DOMEBLpv-cQQ2rjnUZT7ajkRqsOgIhAM8KmzPl94epp6lOB-p3BlNuHZ6S9WmzTcnIrkKb49Jr", + "key_id": "d32d750cee69fed770c11e82aa4a313036d5e9cd62eac06fd8ff325b6657196d", + "pins": [ + { + "domain": "example.com", + "pinned_keys": [ + { + "first_seen": "2026-10-29T04:04:37.980125013+00:00", + "kid": "legacy-key", + "last_seen": "2026-10-29T04:04:37.980125013+00:00", + "public_key_hash": "0f2a1007dde6ab61fb6dcc93e6e8dc162e1996cbaaeb4b43b6c07e9a25171045", + "trust_level": "tofu" + } + ] + } + ], + "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFNB7ookKIfJ/V6gdBshjT3sVXVvN\nACTYjFL6WPJ2PkMGgga/w/5dQ3bBbOn8/IfbngUpnCmdW1Hdh/VPYzf+IQ==\n-----END PUBLIC KEY-----\n" +} diff --git a/crates/agentpin/tests/legacy_crypto.rs b/crates/agentpin/tests/legacy_crypto.rs new file mode 100644 index 0000000..48a4c09 --- /dev/null +++ b/crates/agentpin/tests/legacy_crypto.rs @@ -0,0 +1,32 @@ +// Fixed public-only vectors generated by unmodified AgentPin 0.3.0 / p256 0.13.2. +// Do not regenerate with the new SDK: this is a cross-version wire-format test. +use agentpin::{ + crypto, jwk, jwt, + pinning::{KeyPinStore, PinningResult}, +}; +#[test] +fn legacy_keys_jwt_and_pins_survive_crypto_upgrade() { + let data: serde_json::Value = + serde_json::from_str(include_str!("fixtures/crypto-0.3.0.json")).unwrap(); + let pem = data["public_key_pem"].as_str().unwrap(); + let key: jwk::Jwk = serde_json::from_value(data["jwk"].clone()).unwrap(); + assert_eq!(jwk::jwk_to_pem(&key).unwrap(), pem); + assert_eq!(jwk::pem_to_jwk(pem, "legacy-key").unwrap(), key); + assert_eq!( + crypto::generate_key_id(pem).unwrap(), + data["key_id"].as_str().unwrap() + ); + let vk = crypto::load_verifying_key(pem).unwrap(); + let token = data["jwt"].as_str().unwrap(); + let (_, payload) = jwt::verify_jwt(token, &vk).unwrap(); + assert_eq!(payload.iss, "example.com"); + assert!(jwt::verify_jwt(&token.replace("eyJpc3Mi", "eyJqc3Mi"), &vk).is_err()); + let mut pins = KeyPinStore::from_json(&data["pins"].to_string()).unwrap(); + assert_eq!( + pins.check_and_pin("example.com", &key), + PinningResult::Matched + ); + let wrong = crypto::generate_key_pair().unwrap(); + let wrong = crypto::load_verifying_key(&wrong.public_key_pem).unwrap(); + assert!(jwt::verify_jwt(token, &wrong).is_err()); +}