Skip to content
Merged
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
@@ -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/),
Expand Down
10 changes: 6 additions & 4 deletions crates/agentpin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions crates/agentpin/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,7 +38,7 @@ impl Drop for KeyPair {

/// Generate a new ECDSA P-256 keypair.
pub fn generate_key_pair() -> Result<KeyPair, Error> {
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();

Expand Down
4 changes: 2 additions & 2 deletions crates/agentpin/src/jwk.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -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"));

Expand Down
37 changes: 37 additions & 0 deletions crates/agentpin/tests/fixtures/crypto-0.3.0.json
Original file line number Diff line number Diff line change
@@ -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"
}
32 changes: 32 additions & 0 deletions crates/agentpin/tests/legacy_crypto.rs
Original file line number Diff line number Diff line change
@@ -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());
}
Loading