use std::collections::BTreeMap;
use proto_blue_crypto::{
K256Keypair, Signer as _, format_did_key, k256_compress_pubkey, parse_multikey,
verify_signature,
};
use proto_blue_lex_cbor::encode;
use proto_blue_lex_data::LexValue;
use crate::error::{Error, Result};
use crate::label::Label;
use crate::signing_key::SigningKey;
fn label_to_lex_value(label: &Label) -> LexValue {
let mut m = BTreeMap::new();
m.insert("ver".to_string(), LexValue::Integer(label.ver));
m.insert("src".to_string(), LexValue::String(label.src.clone()));
m.insert("uri".to_string(), LexValue::String(label.uri.clone()));
if let Some(cid) = &label.cid {
m.insert("cid".to_string(), LexValue::String(cid.clone()));
}
m.insert("val".to_string(), LexValue::String(label.val.clone()));
if label.neg {
m.insert("neg".to_string(), LexValue::Bool(true));
}
m.insert("cts".to_string(), LexValue::String(label.cts.clone()));
if let Some(exp) = &label.exp {
m.insert("exp".to_string(), LexValue::String(exp.clone()));
}
LexValue::Map(m)
}
pub fn canonical_bytes(label: &Label) -> Result<Vec<u8>> {
Ok(encode(&label_to_lex_value(label))?)
}
pub fn label_to_lex_value_with_sig(label: &Label) -> Result<LexValue> {
let sig = label
.sig
.ok_or_else(|| Error::Signing("wire encoding requires signed label".into()))?;
let LexValue::Map(mut m) = label_to_lex_value(label) else {
unreachable!("label_to_lex_value always returns a Map")
};
m.insert("sig".to_string(), LexValue::Bytes(sig.to_vec()));
Ok(LexValue::Map(m))
}
pub fn sign_label(key: &SigningKey, label: &Label) -> Result<[u8; 64]> {
let keypair = K256Keypair::from_private_key(key.expose_secret())?;
let bytes = canonical_bytes(label)?;
let sig = keypair.sign(&bytes)?;
sig.as_slice().try_into().map_err(|_| {
Error::Signing(format!(
"proto-blue returned non-64-byte signature ({} bytes)",
sig.len()
))
})
}
pub fn verify_label(public_key_multibase: &str, label: &Label) -> Result<()> {
let sig = label
.sig
.ok_or_else(|| Error::Signing("label has no signature".into()))?;
let parsed = parse_multikey(public_key_multibase)?;
if parsed.jwt_alg != "ES256K" {
return Err(Error::Signing(format!(
"expected ES256K label key, got {}",
parsed.jwt_alg
)));
}
let compressed = k256_compress_pubkey(&parsed.key_bytes)?;
let did = format_did_key("ES256K", &compressed);
let bytes = canonical_bytes(label)?;
let ok = verify_signature(&did, &bytes, &sig, false)?;
if !ok {
return Err(Error::Signing("signature verification failed".into()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_label() -> Label {
Label {
ver: 1,
src: "did:plc:test".to_string(),
uri: "at://did:plc:subject/app.bsky.feed.post/abc123".to_string(),
cid: None,
val: "spam".to_string(),
neg: false,
cts: "2026-04-22T12:00:00.000Z".to_string(),
exp: None,
sig: None,
}
}
#[test]
fn canonical_encoding_omits_sig_and_defaults() {
let l1 = fixture_label();
let mut l2 = fixture_label();
l2.sig = Some([0u8; 64]);
let b1 = canonical_bytes(&l1).expect("encode l1");
let b2 = canonical_bytes(&l2).expect("encode l2");
assert_eq!(b1, b2, "sig-present vs sig-absent must canonicalize equal");
assert!(
!b1.windows(3).any(|w| w == b"neg"),
"neg=false must be omitted from canonical form"
);
}
#[test]
fn wire_encoding_includes_sig_and_pre_sign_does_not() {
let marker: [u8; 64] = [0xCD; 64];
let mut label = fixture_label();
label.sig = Some(marker);
let presign = canonical_bytes(&label).expect("canonical");
let wire = encode(&label_to_lex_value_with_sig(&label).expect("with_sig"))
.expect("encode with_sig");
assert!(
wire.windows(64).any(|w| w == marker),
"wire encoding must contain the 64 sig bytes contiguously"
);
assert!(
!presign.windows(64).any(|w| w == marker),
"pre-sign encoding must NOT contain sig bytes — any match means sig leaked into the signing input"
);
let mut unsigned = fixture_label();
unsigned.sig = None;
let err = label_to_lex_value_with_sig(&unsigned)
.expect_err("unsigned label must not wire-encode");
assert!(matches!(err, Error::Signing(_)), "got {err:?}");
}
#[test]
fn sign_then_verify_roundtrip() {
use proto_blue_crypto::Keypair as _;
let key = SigningKey::from_bytes([0x42; 32]);
let mut label = fixture_label();
label.sig = Some(sign_label(&key, &label).expect("sign"));
let kp = K256Keypair::from_private_key(key.expose_secret()).unwrap();
let multibase = proto_blue_crypto::format_multikey("ES256K", &kp.public_key_compressed());
verify_label(&multibase, &label).expect("verify should pass");
}
#[test]
fn signature_is_64_bytes() {
let key = SigningKey::from_bytes([0x11; 32]);
let sig = sign_label(&key, &fixture_label()).expect("sign");
assert_eq!(sig.len(), 64);
}
}