use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use p256::ecdsa::{
signature::Signer as _, signature::Verifier as _, Signature, SigningKey, VerifyingKey,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use super::ViError;
pub fn b64u(bytes: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(bytes)
}
pub fn b64u_decode(s: &str) -> Result<Vec<u8>, ViError> {
let trimmed = s.trim_end_matches('=');
URL_SAFE_NO_PAD
.decode(trimmed)
.map_err(|e| ViError::Malformed(format!("base64url: {e}")))
}
pub fn sha256_b64u(bytes: &[u8]) -> String {
b64u(&Sha256::digest(bytes))
}
pub fn json_compact_ascii(v: &Value) -> String {
let raw = serde_json::to_string(v).expect("serde_json::Value serializes");
if raw.is_ascii() {
return raw;
}
let mut out = String::with_capacity(raw.len() + 16);
for ch in raw.chars() {
if ch.is_ascii() {
out.push(ch);
} else {
let mut buf = [0u16; 2];
for unit in ch.encode_utf16(&mut buf) {
use std::fmt::Write;
let _ = write!(out, "\\u{unit:04x}");
}
}
}
out
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Jwk {
pub kty: String,
pub crv: String,
pub x: String,
pub y: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
}
impl Jwk {
pub fn from_value(v: &Value) -> Result<Self, ViError> {
let get = |k: &str| -> Result<String, ViError> {
v.get(k)
.and_then(Value::as_str)
.map(str::to_string)
.ok_or_else(|| ViError::Key(format!("jwk missing '{k}'")))
};
let kty = get("kty")?;
let crv = get("crv")?;
if kty != "EC" || crv != "P-256" {
return Err(ViError::Key(format!(
"jwk must be EC/P-256, got {kty}/{crv}"
)));
}
Ok(Self {
kty,
crv,
x: get("x")?,
y: get("y")?,
kid: v.get("kid").and_then(Value::as_str).map(str::to_string),
})
}
pub fn to_value(&self) -> Value {
serde_json::to_value(self).expect("Jwk serializes")
}
pub fn verifying_key(&self) -> Result<VerifyingKey, ViError> {
let x = b64u_decode(&self.x)?;
let y = b64u_decode(&self.y)?;
if x.len() != 32 || y.len() != 32 {
return Err(ViError::Key("jwk x/y must be 32 bytes each".into()));
}
let mut sec1 = Vec::with_capacity(65);
sec1.push(0x04);
sec1.extend_from_slice(&x);
sec1.extend_from_slice(&y);
VerifyingKey::from_sec1_bytes(&sec1).map_err(|e| ViError::Key(format!("jwk point: {e}")))
}
pub fn verify(&self, signing_input: &[u8], sig: &[u8]) -> Result<(), ViError> {
let vk = self.verifying_key()?;
let sig = Signature::from_slice(sig)
.map_err(|e| ViError::Signature(format!("ES256 signature bytes: {e}")))?;
vk.verify(signing_input, &sig)
.map_err(|_| ViError::Signature("ES256 signature did not verify".into()))
}
}
#[derive(Clone)]
pub struct AgentKey {
signing: SigningKey,
pub kid: String,
}
impl std::fmt::Debug for AgentKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentKey").field("kid", &self.kid).finish()
}
}
impl AgentKey {
pub fn generate() -> Self {
let signing = SigningKey::random(&mut rand::rngs::OsRng);
let kid = kid_for(signing.verifying_key());
Self { signing, kid }
}
pub fn from_secret(d: &[u8], kid: impl Into<String>) -> Result<Self, ViError> {
let signing =
SigningKey::from_slice(d).map_err(|e| ViError::Key(format!("P-256 scalar: {e}")))?;
Ok(Self {
signing,
kid: kid.into(),
})
}
pub fn from_private_jwk(v: &Value) -> Result<Self, ViError> {
let d = v
.get("d")
.and_then(Value::as_str)
.ok_or_else(|| ViError::Key("private jwk missing 'd'".into()))?;
let d = b64u_decode(d)?;
let signing =
SigningKey::from_slice(&d).map_err(|e| ViError::Key(format!("P-256 scalar: {e}")))?;
let kid = v
.get("kid")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| kid_for(signing.verifying_key()));
let key = Self { signing, kid };
if v.get("x").is_some() {
let claimed = Jwk::from_value(v)?;
let ours = key.public_jwk();
if claimed.x != ours.x || claimed.y != ours.y {
return Err(ViError::Key("private jwk x/y do not match d".into()));
}
}
Ok(key)
}
pub fn secret_bytes(&self) -> [u8; 32] {
let b = self.signing.to_bytes();
let mut out = [0u8; 32];
out.copy_from_slice(&b);
out
}
pub fn public_jwk(&self) -> Jwk {
let point = self.signing.verifying_key().to_encoded_point(false);
Jwk {
kty: "EC".into(),
crv: "P-256".into(),
x: b64u(point.x().expect("uncompressed point has x")),
y: b64u(point.y().expect("uncompressed point has y")),
kid: Some(self.kid.clone()),
}
}
pub fn private_jwk(&self) -> Value {
let mut v = self.public_jwk().to_value();
v["d"] = Value::String(b64u(&self.secret_bytes()));
v
}
pub fn sign(&self, signing_input: &[u8]) -> Vec<u8> {
let sig: Signature = self.signing.sign(signing_input);
sig.to_bytes().to_vec()
}
}
fn kid_for(vk: &VerifyingKey) -> String {
let compressed = vk.to_encoded_point(true);
let h = Sha256::digest(compressed.as_bytes());
format!("vik_{}", hex::encode(&h[..8]))
}
#[derive(Debug, Clone)]
pub struct CompactJws {
pub header: Value,
pub payload: Value,
pub raw_header_b64: String,
pub raw_payload_b64: String,
pub signature: Vec<u8>,
}
impl CompactJws {
pub fn sign(header: &Value, payload: &Value, key: &AgentKey) -> Self {
let h = b64u(json_compact_ascii(header).as_bytes());
let p = b64u(json_compact_ascii(payload).as_bytes());
let signing_input = format!("{h}.{p}");
let signature = key.sign(signing_input.as_bytes());
Self {
header: header.clone(),
payload: payload.clone(),
raw_header_b64: h,
raw_payload_b64: p,
signature,
}
}
pub fn parse(token: &str) -> Result<Self, ViError> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(ViError::Malformed(format!(
"jwt: expected 3 parts, got {}",
parts.len()
)));
}
let header: Value = serde_json::from_slice(&b64u_decode(parts[0])?)
.map_err(|e| ViError::Malformed(format!("jwt header json: {e}")))?;
let payload: Value = serde_json::from_slice(&b64u_decode(parts[1])?)
.map_err(|e| ViError::Malformed(format!("jwt payload json: {e}")))?;
Ok(Self {
header,
payload,
raw_header_b64: parts[0].to_string(),
raw_payload_b64: parts[1].to_string(),
signature: b64u_decode(parts[2])?,
})
}
pub fn serialize(&self) -> String {
format!(
"{}.{}.{}",
self.raw_header_b64,
self.raw_payload_b64,
b64u(&self.signature)
)
}
pub fn signing_input(&self) -> Vec<u8> {
format!("{}.{}", self.raw_header_b64, self.raw_payload_b64).into_bytes()
}
pub fn verify(&self, jwk: &Jwk) -> Result<(), ViError> {
jwk.verify(&self.signing_input(), &self.signature)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compact_ascii_matches_python_ensure_ascii() {
let v = serde_json::json!({"a": "caf\u{e9} \u{1F600}", "n": 1});
assert_eq!(
json_compact_ascii(&v),
r#"{"a":"caf\u00e9 \ud83d\ude00","n":1}"#
);
}
#[test]
fn sign_and_verify_round_trip() {
let key = AgentKey::generate();
let header = serde_json::json!({"alg":"ES256","typ":"kb-sd-jwt","kid":key.kid});
let payload = serde_json::json!({"nonce":"n","aud":"https://m.example","iat":1});
let jws = CompactJws::sign(&header, &payload, &key);
let parsed = CompactJws::parse(&jws.serialize()).unwrap();
assert_eq!(parsed.header["kid"], key.kid);
parsed.verify(&key.public_jwk()).unwrap();
let other = AgentKey::generate();
assert!(parsed.verify(&other.public_jwk()).is_err());
}
#[test]
fn private_jwk_round_trip_keeps_kid_and_checks_point() {
let key = AgentKey::generate();
let priv_jwk = key.private_jwk();
let back = AgentKey::from_private_jwk(&priv_jwk).unwrap();
assert_eq!(back.kid, key.kid);
assert_eq!(back.public_jwk(), key.public_jwk());
let mut bad = priv_jwk.clone();
bad["x"] = Value::String(b64u(&[7u8; 32]));
assert!(AgentKey::from_private_jwk(&bad).is_err());
}
}