use std::sync::Arc;
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use tatara_lisp_eval::ffi::Arity;
use tatara_lisp_eval::{EvalError, Interpreter, Value};
pub const KEY_BYTES: usize = 32;
pub const SIGNATURE_BYTES: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CryptoError {
#[error("the {what} must be a hex string; got {got}")]
NotHexText {
what: &'static str,
got: &'static str,
},
#[error("the {what} is not hex: character {position} is `{found}`")]
NotHex {
what: &'static str,
position: usize,
found: char,
},
#[error("the {what} has an odd number of hex digits ({digits})")]
OddHex { what: &'static str, digits: usize },
#[error("the {what} is {got} bytes; an Ed25519 {what} is {expected}")]
WrongLength {
what: &'static str,
expected: usize,
got: usize,
},
#[error("a message is a string or a list of bytes; got {got}")]
NotAMessage { got: &'static str },
#[error("element {index} of the byte list is not an integer; got {got}")]
NotAByte { index: usize, got: &'static str },
#[error("element {index} of the byte list is {value}, outside 0..=255")]
ByteOutOfRange { index: usize, value: i64 },
#[error("the public key is not a point on the Ed25519 curve")]
NotACurvePoint,
#[error("the public key has small order and cannot vouch for any message")]
WeakPublicKey,
}
pub fn message_bytes(v: &Value) -> Result<Vec<u8>, CryptoError> {
match v {
Value::Str(s) => Ok(s.as_bytes().to_vec()),
Value::List(items) => items
.iter()
.enumerate()
.map(|(index, item)| match item {
Value::Int(n) => {
u8::try_from(*n).map_err(|_| CryptoError::ByteOutOfRange { index, value: *n })
}
other => Err(CryptoError::NotAByte {
index,
got: other.type_name(),
}),
})
.collect(),
other => Err(CryptoError::NotAMessage {
got: other.type_name(),
}),
}
}
pub fn hex_array<const N: usize>(v: &Value, what: &'static str) -> Result<[u8; N], CryptoError> {
let text = match v {
Value::Str(s) => s,
other => {
return Err(CryptoError::NotHexText {
what,
got: other.type_name(),
})
}
};
let bytes = decode_hex(text, what)?;
<[u8; N]>::try_from(bytes.as_slice()).map_err(|_| CryptoError::WrongLength {
what,
expected: N,
got: bytes.len(),
})
}
fn decode_hex(text: &str, what: &'static str) -> Result<Vec<u8>, CryptoError> {
let digits: Vec<u8> = text
.chars()
.enumerate()
.map(|(position, found)| {
found
.to_digit(16)
.map(|d| d as u8)
.ok_or(CryptoError::NotHex {
what,
position,
found,
})
})
.collect::<Result<_, _>>()?;
if !digits.len().is_multiple_of(2) {
return Err(CryptoError::OddHex {
what,
digits: digits.len(),
});
}
Ok(digits
.as_chunks::<2>()
.0
.iter()
.map(|[hi, lo]| (hi << 4) | lo)
.collect())
}
pub fn encode_hex(bytes: &[u8]) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(char::from(DIGITS[usize::from(b >> 4)]));
out.push(char::from(DIGITS[usize::from(b & 0x0f)]));
}
out
}
pub fn blake3_hex(data: &[u8]) -> String {
blake3::hash(data).to_hex().to_string()
}
pub fn ed25519_public(secret: &[u8; KEY_BYTES]) -> [u8; KEY_BYTES] {
SigningKey::from_bytes(secret).verifying_key().to_bytes()
}
pub fn ed25519_sign(secret: &[u8; KEY_BYTES], message: &[u8]) -> [u8; SIGNATURE_BYTES] {
SigningKey::from_bytes(secret).sign(message).to_bytes()
}
pub fn ed25519_verify(
public: &[u8; KEY_BYTES],
message: &[u8],
signature: &[u8; SIGNATURE_BYTES],
) -> Result<bool, CryptoError> {
let key = VerifyingKey::from_bytes(public).map_err(|_| CryptoError::NotACurvePoint)?;
if key.is_weak() {
return Err(CryptoError::WeakPublicKey);
}
let signature = Signature::from_bytes(signature);
Ok(key.verify_strict(message, &signature).is_ok())
}
fn refuse(name: &'static str, span: tatara_lisp::Span) -> impl Fn(CryptoError) -> EvalError {
move |e| EvalError::native_fn(name, e.to_string(), span)
}
fn hex_value(bytes: &[u8]) -> Value {
Value::Str(Arc::from(encode_hex(bytes)))
}
pub fn install_crypto_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
interp.register_fn(
"blake3_hex",
Arity::Exact(1),
|a: &[Value], _h: &mut H, span| {
let data = message_bytes(&a[0]).map_err(refuse("blake3_hex", span))?;
Ok(Value::Str(Arc::from(blake3_hex(&data))))
},
);
interp.register_fn(
"ed25519_keypair",
Arity::Exact(1),
|a: &[Value], _h: &mut H, span| {
let seed: [u8; KEY_BYTES] =
hex_array(&a[0], "seed").map_err(refuse("ed25519_keypair", span))?;
Ok(Value::list([
hex_value(&seed),
hex_value(&ed25519_public(&seed)),
]))
},
);
interp.register_fn(
"ed25519_sign",
Arity::Exact(2),
|a: &[Value], _h: &mut H, span| {
let secret: [u8; KEY_BYTES] =
hex_array(&a[0], "secret key").map_err(refuse("ed25519_sign", span))?;
let message = message_bytes(&a[1]).map_err(refuse("ed25519_sign", span))?;
Ok(hex_value(&ed25519_sign(&secret, &message)))
},
);
interp.register_fn(
"ed25519_verify",
Arity::Exact(3),
|a: &[Value], _h: &mut H, span| {
let public: [u8; KEY_BYTES] =
hex_array(&a[0], "public key").map_err(refuse("ed25519_verify", span))?;
let message = message_bytes(&a[1]).map_err(refuse("ed25519_verify", span))?;
let signature: [u8; SIGNATURE_BYTES] =
hex_array(&a[2], "signature").map_err(refuse("ed25519_verify", span))?;
let verdict = ed25519_verify(&public, &message, &signature)
.map_err(refuse("ed25519_verify", span))?;
Ok(Value::Bool(verdict))
},
);
}
#[cfg(test)]
mod tests {
use super::*;
fn eval(src: &str) -> Value {
crate::run(src)
.unwrap_or_else(|e| panic!("{src:?}: {e}"))
.value
}
fn s(src: &str) -> String {
match eval(src) {
Value::Str(v) => v.to_string(),
other => panic!("{src:?} produced {other:?}"),
}
}
fn b(src: &str) -> bool {
match eval(src) {
Value::Bool(v) => v,
other => panic!("{src:?} produced {other:?}"),
}
}
fn err(src: &str) -> String {
crate::run(src)
.map(|r| r.value)
.expect_err("must raise")
.to_string()
}
struct Vector {
secret: &'static str,
public: &'static str,
message: &'static str,
signature: &'static str,
}
const TEST_1: Vector = Vector {
secret: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
public: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
message: "\"\"",
signature: "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155\
5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
};
const TEST_2: Vector = Vector {
secret: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
public: "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
message: "\"r\"",
signature: "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da\
085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
};
const TEST_3: Vector = Vector {
secret: "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
public: "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
message: "[175, 130]",
signature: "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac\
18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
};
fn check_vector(v: &Vector) {
let pair = eval(&["ed25519_keypair(\"", v.secret, "\")"].concat());
let Value::List(pair) = pair else {
panic!("keypair must be a list, got {pair:?}")
};
assert!(matches!(&pair[0], Value::Str(x) if &**x == v.secret));
assert!(
matches!(&pair[1], Value::Str(x) if &**x == v.public),
"public key mismatch: {:?}",
pair[1]
);
let sig = s(&["ed25519_sign(\"", v.secret, "\", ", v.message, ")"].concat());
assert_eq!(sig, v.signature, "signature mismatch");
assert!(b(&[
"ed25519_verify(\"",
v.public,
"\", ",
v.message,
", \"",
v.signature,
"\")"
]
.concat()));
}
#[test]
fn rfc8032_test_1() {
check_vector(&TEST_1);
}
#[test]
fn rfc8032_test_2() {
check_vector(&TEST_2);
}
#[test]
fn rfc8032_test_3() {
check_vector(&TEST_3);
}
fn official_input(n: usize) -> Vec<u8> {
(0..n).map(|i| (i % 251) as u8).collect()
}
#[test]
fn blake3_the_empty_input() {
assert_eq!(
s("blake3_hex(\"\")"),
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
);
assert_eq!(
s("blake3_hex([])"),
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
);
}
#[test]
fn blake3_one_byte_through_blue() {
assert_eq!(
s("blake3_hex([0])"),
"2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213"
);
}
#[test]
fn blake3_across_the_chunk_boundary() {
for (n, want) in [
(
3,
"e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f",
),
(
1023,
"10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11",
),
(
1024,
"42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af7",
),
(
1025,
"d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444",
),
(
8193,
"bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3b",
),
] {
assert_eq!(blake3_hex(&official_input(n)), want, "input_len {n}");
}
}
#[test]
fn text_and_bytes_are_one_message() {
assert_eq!(s("blake3_hex(\"abc\")"), s("blake3_hex([97, 98, 99])"));
assert_eq!(s("blake3_hex(\"é\")"), s("blake3_hex([195, 169])"));
}
#[test]
fn a_signature_round_trips_through_blue() {
let sig = s(&["ed25519_sign(\"", TEST_2.secret, "\", \"a permit\")"].concat());
assert!(b(&[
"ed25519_verify(\"",
TEST_2.public,
"\", \"a permit\", \"",
&sig,
"\")"
]
.concat()));
}
#[test]
fn a_tampered_message_does_not_verify() {
assert!(!b(&[
"ed25519_verify(\"",
TEST_2.public,
"\", \"s\", \"",
TEST_2.signature,
"\")"
]
.concat()));
}
#[test]
fn a_wrong_key_does_not_verify() {
assert!(!b(&[
"ed25519_verify(\"",
TEST_1.public,
"\", \"r\", \"",
TEST_2.signature,
"\")"
]
.concat()));
}
#[test]
fn a_tampered_signature_does_not_verify() {
let mut forged = TEST_1.signature.to_string();
forged.pop();
forged.push('a');
assert_ne!(forged, TEST_1.signature);
assert!(!b(&[
"ed25519_verify(\"",
TEST_1.public,
"\", \"\", \"",
&forged,
"\")"
]
.concat()));
}
#[test]
fn a_short_key_is_a_named_error_not_false() {
let e = err("ed25519_verify(\"abcd\", \"m\", \"00\")");
assert!(e.contains("ed25519_verify"), "{e}");
assert!(e.contains("public key is 2 bytes"), "{e}");
}
#[test]
fn non_hex_is_a_named_error() {
let bad = ["\"zz", &TEST_1.secret[2..], "\""].concat();
let e = err(&["ed25519_sign(", &bad, ", \"m\")"].concat());
assert!(e.contains("not hex: character 0 is `z`"), "{e}");
}
#[test]
fn odd_hex_is_a_named_error() {
let e = err("ed25519_keypair(\"abc\")");
assert!(e.contains("odd number of hex digits (3)"), "{e}");
}
#[test]
fn a_key_that_is_not_a_string_is_a_named_error() {
let e = err("ed25519_keypair(42)");
assert!(e.contains("seed must be a hex string; got int"), "{e}");
}
#[test]
fn a_non_message_is_a_named_error() {
let e = err("blake3_hex(nil)");
assert!(
e.contains("a message is a string or a list of bytes"),
"{e}"
);
let e = err("blake3_hex(42)");
assert!(e.contains("; got int"), "{e}");
}
#[test]
fn a_byte_out_of_range_is_a_named_error() {
let e = err("blake3_hex([1, 256])");
assert!(e.contains("element 1 of the byte list is 256"), "{e}");
let e = err("blake3_hex([1, \"x\"])");
assert!(
e.contains("element 1 of the byte list is not an integer; got string"),
"{e}"
);
}
#[test]
fn hex_input_is_case_insensitive_and_output_is_lowercase() {
let upper = TEST_1.secret.to_uppercase();
let pair = eval(&["ed25519_keypair(\"", &upper, "\")"].concat());
let Value::List(pair) = pair else { panic!() };
assert!(matches!(&pair[0], Value::Str(x) if &**x == TEST_1.secret));
assert!(matches!(&pair[1], Value::Str(x) if &**x == TEST_1.public));
}
#[test]
fn a_public_key_off_the_curve_is_a_named_error() {
let mut off = [0u8; KEY_BYTES];
off[0] = 2;
assert!(
VerifyingKey::from_bytes(&off).is_err(),
"control: y=2 is off-curve"
);
let e = err(&[
"ed25519_verify(\"",
&encode_hex(&off),
"\", \"m\", \"",
TEST_1.signature,
"\")",
]
.concat());
assert!(e.contains("not a point on the Ed25519 curve"), "{e}");
}
#[test]
fn a_small_order_public_key_is_a_named_error() {
let mut identity = [0u8; KEY_BYTES];
identity[0] = 1;
let key = VerifyingKey::from_bytes(&identity).expect("the identity decompresses");
assert!(key.is_weak(), "control: the identity is small-order");
let e = err(&[
"ed25519_verify(\"",
&encode_hex(&identity),
"\", \"m\", \"",
TEST_1.signature,
"\")",
]
.concat());
assert!(e.contains("small order"), "{e}");
}
#[test]
fn hex_round_trips() {
let all: Vec<u8> = (0..=255).collect();
let text = encode_hex(&all);
assert_eq!(decode_hex(&text, "t").expect("round trip"), all);
assert_eq!(decode_hex("", "t").expect("empty"), Vec::<u8>::new());
}
}