Files
memona-plugin-signature/src/sign.rs
T

108 lines
3.8 KiB
Rust

//! Signing a saved Markdown file, the only place this plugin touches its
//! bytes. Everything the md-ens-signature library does for the plugin goes
//! through the interface here.
use crate::sdk::ErrorCode;
use md_ens_signature::document::{
SignatureFields, body_digest, write_signature,
};
use md_ens_signature::signature::{address_of, sign};
/// One connection's signing identity: an ENS name and the private key bound
/// to it.
pub struct Signer {
name: String,
key: [u8; 32],
}
/// Reads a 32-byte private key written as 64 hex characters, with or without
/// a `0x` prefix.
fn parse_key(text: &str) -> Option<[u8; 32]> {
let digits = text.trim();
let digits = digits.strip_prefix("0x").unwrap_or(digits).as_bytes();
if digits.len() != 64 {
return None;
}
let mut key = [0u8; 32];
for (byte, pair) in key.iter_mut().zip(digits.chunks(2)) {
let text = std::str::from_utf8(pair).ok()?;
*byte = u8::from_str_radix(text, 16).ok()?;
}
Some(key)
}
impl Signer {
/// Takes the configured name and key, failing with `InvalidInput` when the
/// key cannot sign, so a bad key refuses the connection rather than every
/// later save.
pub fn new(name: &str, private_key: &str) -> Result<Self, ErrorCode> {
let name = name.trim().to_lowercase();
let key = parse_key(private_key).ok_or(ErrorCode::InvalidInput)?;
if name.is_empty() || address_of(&key).is_err() {
return Err(ErrorCode::InvalidInput);
}
Ok(Self { name, key })
}
/// Returns the whole file with its `signer` and `signature` frontmatter set
/// for this identity, other frontmatter and the body untouched. A file that
/// is not UTF-8 is not Markdown this plugin can sign.
pub fn sign(&self, markdown: &[u8]) -> Result<Vec<u8>, ErrorCode> {
let text =
std::str::from_utf8(markdown).map_err(|_| ErrorCode::InvalidInput)?;
let signature = sign(&self.key, &self.name, &body_digest(text))
.map_err(|_| ErrorCode::Internal)?;
let fields = SignatureFields {
signer: self.name.clone(),
signature,
};
Ok(write_signature(text, &fields).into_bytes())
}
}
#[cfg(test)]
mod tests {
use super::{Signer, parse_key};
use crate::sdk::ErrorCode;
use md_ens_signature::document::{body_digest, read_signature};
use md_ens_signature::signature::{address_of, recover};
const KEY: &str =
"0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318";
#[test]
fn a_signed_file_recovers_to_the_key_and_keeps_its_body()
-> Result<(), ErrorCode> {
let signer = Signer::new(" Laptop.Alice.eth ", KEY)?;
let saved = "---\ntitle: Notes\n---\n# Notes\n\nHello\n";
let signed = String::from_utf8(signer.sign(saved.as_bytes())?)
.map_err(|_| ErrorCode::Internal)?;
assert!(signed.starts_with("---\ntitle: Notes\n"));
assert!(signed.ends_with("---\n# Notes\n\nHello\n"));
let fields = read_signature(&signed).ok_or(ErrorCode::Internal)?;
assert_eq!(fields.signer, "laptop.alice.eth");
let key = parse_key(KEY).ok_or(ErrorCode::Internal)?;
let recovered =
recover(&fields.signature, &fields.signer, &body_digest(&signed))
.map_err(|_| ErrorCode::Internal)?;
assert_eq!(Some(recovered), address_of(&key).ok());
Ok(())
}
#[test]
fn a_key_that_cannot_sign_refuses_the_connection() {
assert!(Signer::new("alice.eth", "0x1234").is_err());
assert!(Signer::new("alice.eth", &"zz".repeat(32)).is_err());
assert!(Signer::new("alice.eth", &"00".repeat(32)).is_err());
assert!(Signer::new(" ", KEY).is_err());
assert!(Signer::new("alice.eth", &KEY[2..]).is_ok());
}
#[test]
fn a_file_that_is_not_text_is_refused() -> Result<(), ErrorCode> {
let signer = Signer::new("alice.eth", KEY)?;
assert_eq!(signer.sign(&[0xff, 0xfe]), Err(ErrorCode::InvalidInput));
Ok(())
}
}