1d36760782
Co-authored-by: Danny Kim <temeddix@gmail.com> Co-committed-by: Danny Kim <temeddix@gmail.com>
110 lines
3.9 KiB
Rust
110 lines
3.9 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"));
|
|
// YAML reads a bare 0x... as an integer, so the signature stays a string.
|
|
assert!(signed.contains("\nsignature: \"0x"));
|
|
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(())
|
|
}
|
|
}
|