use k256::ecdsa::SigningKey;
#[derive(Debug, thiserror::Error)]
pub enum SignError {
#[error("base64: {0}")]
Base64(String),
#[error("pubkey: {0}")]
Pubkey(String),
#[error("signature invalid")]
Invalid,
#[error("signer mismatch: neuron field differs from pubkey")]
SignerMismatch,
}
pub fn b64_encode(bytes: &[u8]) -> String {
crate::b64::encode(bytes)
}
pub fn sign_body(
key: &SigningKey,
body_bytes: &[u8],
signer_bech32: &str,
) -> (String, String) {
let sig = mudra::claim::sign_arbitrary(key, signer_bech32, body_bytes);
let pubkey = key.verifying_key().to_encoded_point(true);
(crate::b64::encode(pubkey.as_bytes()), crate::b64::encode(&sig))
}
pub fn verify(
body_bytes: &[u8],
neuron_bech32: &str,
pubkey_b64: &str,
signature_b64: &str,
hrp: &str,
) -> Result<(), SignError> {
let pubkey_bytes =
crate::b64::decode(pubkey_b64).ok_or_else(|| SignError::Base64("pubkey".into()))?;
let pubkey: [u8; 33] = pubkey_bytes
.as_slice()
.try_into()
.map_err(|_| SignError::Pubkey("expected 33 bytes".into()))?;
if mudra::cosmos::address(&pubkey, hrp).map_err(|e| SignError::Pubkey(e.to_string()))?
!= neuron_bech32
{
return Err(SignError::SignerMismatch);
}
let sig_bytes =
crate::b64::decode(signature_b64).ok_or_else(|| SignError::Base64("signature".into()))?;
let signature: [u8; 64] =
sig_bytes.as_slice().try_into().map_err(|_| SignError::Invalid)?;
if mudra::claim::verify_arbitrary(&pubkey, neuron_bech32, body_bytes, &signature) {
Ok(())
} else {
Err(SignError::Invalid)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::Seed;
const PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
#[test]
fn sign_verify_roundtrip() {
let seed = Seed::from_mnemonic(PHRASE).unwrap();
let n = seed.neuron("example.com", "lytics").unwrap();
let body = br#"{"hello":"world"}"#;
let (pubkey, sig) = sign_body(n.signing_key(), body, &n.bech32);
verify(body, &n.bech32, &pubkey, &sig, "lytics").unwrap();
}
#[test]
fn tampered_body_fails() {
let seed = Seed::from_mnemonic(PHRASE).unwrap();
let n = seed.neuron("example.com", "lytics").unwrap();
let (pubkey, sig) = sign_body(n.signing_key(), b"real", &n.bech32);
assert!(verify(b"fake", &n.bech32, &pubkey, &sig, "lytics").is_err());
}
#[test]
fn wrong_neuron_field_fails() {
let seed = Seed::from_mnemonic(PHRASE).unwrap();
let n = seed.neuron("example.com", "lytics").unwrap();
let other = seed.neuron("other.org", "lytics").unwrap();
let (pubkey, sig) = sign_body(n.signing_key(), b"data", &n.bech32);
assert!(matches!(
verify(b"data", &other.bech32, &pubkey, &sig, "lytics"),
Err(SignError::SignerMismatch)
));
}
}