use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
const PAYMENT_SIG_CONTEXT: &[u8] = b"dig-pex/payment-address/v1\0";
pub const PEX_MAX_PAYMENT_ADDRESS_LEN: usize = 128;
pub const PEX_MAX_PAYMENT_SPKI_LEN: usize = 512;
pub const PEX_MAX_PAYMENT_SIG_LEN: usize = 256;
pub trait SignatureVerifier {
fn verify(&self, spki_der: &[u8], message: &[u8], signature: &[u8]) -> bool;
}
impl<F> SignatureVerifier for F
where
F: Fn(&[u8], &[u8], &[u8]) -> bool,
{
fn verify(&self, spki_der: &[u8], message: &[u8], signature: &[u8]) -> bool {
self(spki_der, message, signature)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaymentClaimError {
NotPresent,
Malformed,
PeerIdMismatch,
BadSignature,
}
impl std::fmt::Display for PaymentClaimError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let reason = match self {
PaymentClaimError::NotPresent => "no payment claim on this entry",
PaymentClaimError::Malformed => "payment claim is malformed or over its caps",
PaymentClaimError::PeerIdMismatch => "payment claim key does not hash to peer_id",
PaymentClaimError::BadSignature => "payment claim signature does not verify",
};
f.write_str(reason)
}
}
impl std::error::Error for PaymentClaimError {}
#[must_use]
pub fn payment_signing_bytes(peer_id: &str, network_id: &str, address: &str) -> Vec<u8> {
let mut msg = Vec::with_capacity(
PAYMENT_SIG_CONTEXT.len() + 12 + peer_id.len() + network_id.len() + address.len(),
);
msg.extend_from_slice(PAYMENT_SIG_CONTEXT);
for field in [peer_id, network_id, address] {
let len = u32::try_from(field.len()).unwrap_or(u32::MAX);
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(field.as_bytes());
}
msg
}
#[must_use]
pub fn peer_id_for_spki(spki_der: &[u8]) -> String {
let digest = Sha256::digest(spki_der);
let mut hex = String::with_capacity(64);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(hex, "{byte:02x}");
}
hex
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PaymentClaim {
#[serde(default)]
address: String,
#[serde(default)]
spki: String,
#[serde(default)]
sig: String,
}
impl PaymentClaim {
#[must_use]
pub fn new(address: impl Into<String>, spki_der: &[u8], signature: &[u8]) -> Self {
PaymentClaim {
address: address.into(),
spki: BASE64.encode(spki_der),
sig: BASE64.encode(signature),
}
}
#[must_use]
pub fn spki_der(&self) -> Vec<u8> {
BASE64.decode(&self.spki).unwrap_or_default()
}
#[must_use]
pub fn signature(&self) -> Vec<u8> {
BASE64.decode(&self.sig).unwrap_or_default()
}
pub(crate) fn wire_parts(&self) -> [&str; 3] {
[&self.address, &self.spki, &self.sig]
}
#[must_use]
pub fn within_caps(&self) -> bool {
self.address.len() <= PEX_MAX_PAYMENT_ADDRESS_LEN
&& self.spki.len() <= PEX_MAX_PAYMENT_SPKI_LEN
&& self.sig.len() <= PEX_MAX_PAYMENT_SIG_LEN
}
pub fn verify(
&self,
peer_id: &str,
network_id: &str,
verifier: &impl SignatureVerifier,
) -> Result<&str, PaymentClaimError> {
if !self.within_caps() {
return Err(PaymentClaimError::Malformed);
}
let (Ok(spki_der), Ok(signature)) = (BASE64.decode(&self.spki), BASE64.decode(&self.sig))
else {
return Err(PaymentClaimError::Malformed);
};
if peer_id_for_spki(&spki_der) != peer_id {
return Err(PaymentClaimError::PeerIdMismatch);
}
let message = payment_signing_bytes(peer_id, network_id, &self.address);
if !verifier.verify(&spki_der, &message, &signature) {
return Err(PaymentClaimError::BadSignature);
}
Ok(&self.address)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct AcceptAll;
impl SignatureVerifier for AcceptAll {
fn verify(&self, _spki: &[u8], _msg: &[u8], _sig: &[u8]) -> bool {
true
}
}
#[test]
fn signing_bytes_are_domain_separated_and_length_prefixed() {
let bytes = payment_signing_bytes("aa", "mainnet", "xch1");
assert!(bytes.starts_with(PAYMENT_SIG_CONTEXT));
assert_eq!(
bytes,
[
PAYMENT_SIG_CONTEXT,
&0u32.to_be_bytes()[..3],
&[2],
b"aa",
&0u32.to_be_bytes()[..3],
&[7],
b"mainnet",
&0u32.to_be_bytes()[..3],
&[4],
b"xch1",
]
.concat()
);
}
#[test]
fn a_shifted_field_boundary_changes_the_signing_bytes() {
assert_ne!(
payment_signing_bytes("ab", "c", "x"),
payment_signing_bytes("a", "bc", "x")
);
}
#[test]
fn peer_id_derivation_matches_sha256_of_spki() {
assert_eq!(
peer_id_for_spki(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(peer_id_for_spki(b"spki").len(), 64);
}
#[test]
fn a_permissive_verifier_cannot_bypass_the_peer_id_binding() {
let claim = PaymentClaim::new("xch1payee", b"some-other-key", b"whatever");
assert_eq!(
claim.verify(&"a".repeat(64), "mainnet", &AcceptAll),
Err(PaymentClaimError::PeerIdMismatch)
);
assert_eq!(
claim.verify(&peer_id_for_spki(b"some-other-key"), "mainnet", &AcceptAll),
Ok("xch1payee")
);
}
#[test]
fn an_over_cap_or_undecodable_field_is_malformed() {
let spki = b"key";
let peer_id = peer_id_for_spki(spki);
let long = PaymentClaim::new("x".repeat(PEX_MAX_PAYMENT_ADDRESS_LEN + 1), spki, b"s");
assert_eq!(
long.verify(&peer_id, "mainnet", &AcceptAll),
Err(PaymentClaimError::Malformed)
);
let at_cap = PaymentClaim::new("x".repeat(PEX_MAX_PAYMENT_ADDRESS_LEN), spki, b"s");
assert!(at_cap.verify(&peer_id, "mainnet", &AcceptAll).is_ok());
let bad_b64: PaymentClaim =
serde_json::from_str(r#"{"address":"xch1","spki":"!!!","sig":"AA=="}"#).unwrap();
assert_eq!(
bad_b64.verify(&peer_id, "mainnet", &AcceptAll),
Err(PaymentClaimError::Malformed)
);
}
#[test]
fn wire_field_names_are_frozen() {
let json = serde_json::to_string(&PaymentClaim::new("xch1", b"k", b"s")).unwrap();
assert_eq!(json, r#"{"address":"xch1","spki":"aw==","sig":"cw=="}"#);
}
#[test]
fn a_closure_is_a_verifier() {
let claim = PaymentClaim::new("xch1", b"k", b"s");
let peer_id = peer_id_for_spki(b"k");
let never = |_: &[u8], _: &[u8], _: &[u8]| false;
assert_eq!(
claim.verify(&peer_id, "mainnet", &never),
Err(PaymentClaimError::BadSignature)
);
}
}