Skip to main content

blue_lang_runtime/
crypto.rs

1//! Real cryptography — BLAKE3 hashing and Ed25519 signatures — as blue's own
2//! surface.
3//!
4//! ```text
5//! blake3_hex(data)                               → 64 hex chars (BLAKE3-256)
6//! ed25519_keypair(seed_hex)                      → [secret_hex, public_hex]
7//! ed25519_sign(secret_hex, message)              → 128 hex chars
8//! ed25519_verify(public_hex, message, sig_hex)   → true / false
9//! ```
10//!
11//! `data` and `message` are a string (its UTF-8 bytes) or a list of byte
12//! integers `0..=255`. Keys and signatures are hex, in and out: blue has no
13//! byte type, and hex is the one spelling of a key that survives a JSON
14//! document, a log line and a terminal unchanged. Output is lowercase; input
15//! accepts either case.
16//!
17//! # Pure layer, not `sys` — and why that is a decision, not a default
18//!
19//! Every function here is a deterministic function of its arguments. None
20//! reads a clock, a file, the environment or an entropy source, so none is a
21//! host effect, and the `sys` layer is reserved for exactly those (it is
22//! feature-gated because each of its names is a host import). These live
23//! beside `json`: installed unconditionally, lowering to **no import**, kept by
24//! the `wasm32-unknown-unknown` consumer.
25//!
26//! The one place that could have forced them into `sys` is key generation,
27//! which normally draws from the OS. It does not here: `ed25519_keypair` takes
28//! the 32-byte seed from the CALLER. That is what keeps the layer pure, and it
29//! is also what makes it testable — RFC 8032's vectors are seeds, so a
30//! deterministic keypair is the only kind a test can check against them. Where
31//! a caller's seed comes from (a file, a secret store, a future host entropy
32//! primitive) is a host question and belongs in `sys` when it is asked.
33//!
34//! # No C, and the crates chosen for it
35//!
36//! Ed25519 is `ed25519-dalek` over `curve25519-dalek`; neither compiles C.
37//! BLAKE3 is the official `blake3` crate with its **`pure`** feature, which
38//! turns off the C and assembly SIMD builds its `build.rs` otherwise performs
39//! (on aarch64 it compiled NEON C intrinsics into this very crate before this
40//! module existed — `inputs` already hashed with it). Features are pinned
41//! explicitly in `Cargo.toml`, so a default that links C cannot arrive on a
42//! version bump. The irreducible remainder is stated rather than hidden:
43//! `cc` is still in the dependency tree, as an unconditional
44//! `[build-dependencies]` entry of `blake3` that Cargo cannot drop, and on
45//! x86 `blake3`'s build script still asks the C compiler whether it accepts
46//! `-mavx512f` before choosing the Rust implementation. No C object is built
47//! or linked on any target.
48//!
49//! # A typed error on bad input, never a silent `false`
50//!
51//! `ed25519_verify` answers `false` for exactly one thing: a well-formed
52//! signature that does not verify. A key that is not hex, a signature of the
53//! wrong length, a public key that is not a curve point, a message that is
54//! neither text nor bytes — each RAISES, naming what was wrong. A verifier that
55//! folded "you passed me garbage" into "the signature is bad" would let a
56//! wiring bug read as a forgery caught, and a forgery caught read as a wiring
57//! bug. [`CryptoError`] is the closed set of those refusals, with one
58//! `Display`.
59//!
60//! # Strict verification
61//!
62//! Verification is `verify_strict`: it rejects non-canonical signatures and
63//! small-order public keys, which the permissive `verify` accepts. A signed
64//! event log wants one valid signature per (key, message), not several, and a
65//! small-order key "verifies" signatures nobody made. A small-order key is
66//! refused as bad input ([`CryptoError::WeakPublicKey`]) rather than answered
67//! `false`, because no message could ever verify under it.
68//!
69//! # What this does not protect
70//!
71//! The secret key arrives and leaves as a blue string. `SigningKey` zeroizes
72//! its own copy on drop, but the interpreter's string values are not zeroized,
73//! so a secret passed through blue lives in ordinary heap memory until it is
74//! reclaimed. This layer computes signatures correctly; it is not a key vault.
75
76use std::sync::Arc;
77
78use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
79use hmac::{Hmac, Mac};
80use sha1::Sha1;
81use sha2::{Digest, Sha256, Sha512};
82use tatara_lisp_eval::ffi::Arity;
83use tatara_lisp_eval::{EvalError, Interpreter, Value};
84
85/// Bytes in an Ed25519 secret key (the RFC 8032 seed) and in a public key.
86pub const KEY_BYTES: usize = 32;
87/// Bytes in an Ed25519 signature.
88pub const SIGNATURE_BYTES: usize = 64;
89
90/// Every way an argument to this layer can be refused.
91///
92/// Closed, and each arm reads differently, so a reader of the raised message is
93/// sent to the one thing that was wrong.
94#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95pub enum CryptoError {
96    /// A key or signature argument that is not a string at all.
97    #[error("the {what} must be a hex string; got {got}")]
98    NotHexText {
99        what: &'static str,
100        got: &'static str,
101    },
102    /// A character outside `0-9a-fA-F`, at a character position.
103    #[error("the {what} is not hex: character {position} is `{found}`")]
104    NotHex {
105        what: &'static str,
106        position: usize,
107        found: char,
108    },
109    /// An odd number of hex digits cannot be whole bytes.
110    #[error("the {what} has an odd number of hex digits ({digits})")]
111    OddHex { what: &'static str, digits: usize },
112    /// Well-formed hex of the wrong size.
113    #[error("the {what} is {got} bytes; an Ed25519 {what} is {expected}")]
114    WrongLength {
115        what: &'static str,
116        expected: usize,
117        got: usize,
118    },
119    /// Something that is neither text nor a byte list.
120    #[error("a message is a string or a list of bytes; got {got}")]
121    NotAMessage { got: &'static str },
122    /// A byte-list element that is not an integer.
123    #[error("element {index} of the byte list is not an integer; got {got}")]
124    NotAByte { index: usize, got: &'static str },
125    /// A byte-list element outside `0..=255`.
126    #[error("element {index} of the byte list is {value}, outside 0..=255")]
127    ByteOutOfRange { index: usize, value: i64 },
128    /// 32 bytes that do not decompress to a point on the curve.
129    #[error("the public key is not a point on the Ed25519 curve")]
130    NotACurvePoint,
131    /// A point of small order: every signature under it is meaningless.
132    #[error("the public key has small order and cannot vouch for any message")]
133    WeakPublicKey,
134    #[error("the algorithm must be a string or keyword; got {got}")]
135    NotAnAlgorithm { got: &'static str },
136    #[error("unknown algorithm `{name}`; the built ones are {known}")]
137    UnknownAlgorithm { name: String, known: &'static str },
138    #[error("{name} has no keyed form here; a keyed digest is one of {known}")]
139    NoMac {
140        name: &'static str,
141        known: &'static str,
142    },
143    #[error("the key is not usable as a keyed-digest key")]
144    BadMacKey,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum Algorithm {
149    Sha1,
150    Sha256,
151    Sha512,
152    Blake3,
153}
154
155impl Algorithm {
156    pub const DIGESTS: &'static str = "sha1, sha256, sha512, blake3";
157    pub const MACS: &'static str = "sha1, sha256, sha512";
158
159    pub fn parse(name: &str) -> Result<Self, CryptoError> {
160        match name {
161            "sha1" => Ok(Algorithm::Sha1),
162            "sha256" => Ok(Algorithm::Sha256),
163            "sha512" => Ok(Algorithm::Sha512),
164            "blake3" => Ok(Algorithm::Blake3),
165            other => Err(CryptoError::UnknownAlgorithm {
166                name: other.to_string(),
167                known: Self::DIGESTS,
168            }),
169        }
170    }
171
172    pub fn from_value(v: &Value) -> Result<Self, CryptoError> {
173        match v {
174            Value::Str(s) => Self::parse(s),
175            Value::Keyword(k) => Self::parse(k),
176            other => Err(CryptoError::NotAnAlgorithm {
177                got: other.type_name(),
178            }),
179        }
180    }
181
182    pub fn name(self) -> &'static str {
183        match self {
184            Algorithm::Sha1 => "sha1",
185            Algorithm::Sha256 => "sha256",
186            Algorithm::Sha512 => "sha512",
187            Algorithm::Blake3 => "blake3",
188        }
189    }
190}
191
192/// The bytes a message or hash input stands for.
193///
194/// A string is its UTF-8 bytes, so `"r"` and `[114]` are one message — bytes
195/// are bytes, whichever way they were spelled. `nil` is refused rather than
196/// read as empty: blue has two empties (`nil != []`), and a message that
197/// silently became nil is a bug to surface, not an empty string to sign.
198pub fn message_bytes(v: &Value) -> Result<Vec<u8>, CryptoError> {
199    match v {
200        Value::Str(s) => Ok(s.as_bytes().to_vec()),
201        Value::List(items) => items
202            .iter()
203            .enumerate()
204            .map(|(index, item)| match item {
205                Value::Int(n) => {
206                    u8::try_from(*n).map_err(|_| CryptoError::ByteOutOfRange { index, value: *n })
207                }
208                other => Err(CryptoError::NotAByte {
209                    index,
210                    got: other.type_name(),
211                }),
212            })
213            .collect(),
214        other => Err(CryptoError::NotAMessage {
215            got: other.type_name(),
216        }),
217    }
218}
219
220/// Decode a hex argument of exactly `N` bytes.
221pub fn hex_array<const N: usize>(v: &Value, what: &'static str) -> Result<[u8; N], CryptoError> {
222    let text = match v {
223        Value::Str(s) => s,
224        other => {
225            return Err(CryptoError::NotHexText {
226                what,
227                got: other.type_name(),
228            })
229        }
230    };
231    let bytes = decode_hex(text, what)?;
232    <[u8; N]>::try_from(bytes.as_slice()).map_err(|_| CryptoError::WrongLength {
233        what,
234        expected: N,
235        got: bytes.len(),
236    })
237}
238
239/// Strict hex: digits only, either case, an even count. No `0x`, no spaces.
240fn decode_hex(text: &str, what: &'static str) -> Result<Vec<u8>, CryptoError> {
241    let digits: Vec<u8> = text
242        .chars()
243        .enumerate()
244        .map(|(position, found)| {
245            found
246                .to_digit(16)
247                // `to_digit(16)` answers at most 15, so the narrowing is exact.
248                .map(|d| d as u8)
249                .ok_or(CryptoError::NotHex {
250                    what,
251                    position,
252                    found,
253                })
254        })
255        .collect::<Result<_, _>>()?;
256    if !digits.len().is_multiple_of(2) {
257        return Err(CryptoError::OddHex {
258            what,
259            digits: digits.len(),
260        });
261    }
262    Ok(digits
263        .as_chunks::<2>()
264        .0
265        .iter()
266        .map(|[hi, lo]| (hi << 4) | lo)
267        .collect())
268}
269
270/// Lowercase hex, the one output spelling.
271pub fn encode_hex(bytes: &[u8]) -> String {
272    const DIGITS: &[u8; 16] = b"0123456789abcdef";
273    let mut out = String::with_capacity(bytes.len() * 2);
274    for b in bytes {
275        out.push(char::from(DIGITS[usize::from(b >> 4)]));
276        out.push(char::from(DIGITS[usize::from(b & 0x0f)]));
277    }
278    out
279}
280
281/// BLAKE3-256 of `data`, as lowercase hex.
282pub fn blake3_hex(data: &[u8]) -> String {
283    blake3::hash(data).to_hex().to_string()
284}
285
286/// The public key for an RFC 8032 secret key (the 32-byte seed).
287pub fn ed25519_public(secret: &[u8; KEY_BYTES]) -> [u8; KEY_BYTES] {
288    SigningKey::from_bytes(secret).verifying_key().to_bytes()
289}
290
291/// Sign `message` with an RFC 8032 secret key. Ed25519 signing is
292/// deterministic: one (key, message) pair has one signature.
293pub fn ed25519_sign(secret: &[u8; KEY_BYTES], message: &[u8]) -> [u8; SIGNATURE_BYTES] {
294    SigningKey::from_bytes(secret).sign(message).to_bytes()
295}
296
297/// Verify strictly. `Ok(false)` is a well-formed signature that does not
298/// verify; `Err` is input that could not have verified anything.
299pub fn ed25519_verify(
300    public: &[u8; KEY_BYTES],
301    message: &[u8],
302    signature: &[u8; SIGNATURE_BYTES],
303) -> Result<bool, CryptoError> {
304    let key = VerifyingKey::from_bytes(public).map_err(|_| CryptoError::NotACurvePoint)?;
305    if key.is_weak() {
306        return Err(CryptoError::WeakPublicKey);
307    }
308    let signature = Signature::from_bytes(signature);
309    Ok(key.verify_strict(message, &signature).is_ok())
310}
311
312pub fn digest_hex(algorithm: Algorithm, data: &[u8]) -> String {
313    match algorithm {
314        Algorithm::Sha1 => encode_hex(&Sha1::digest(data)),
315        Algorithm::Sha256 => encode_hex(&Sha256::digest(data)),
316        Algorithm::Sha512 => encode_hex(&Sha512::digest(data)),
317        Algorithm::Blake3 => blake3_hex(data),
318    }
319}
320
321fn mac_hex<M: Mac>(
322    key: &[u8],
323    data: &[u8],
324    new: fn(&[u8]) -> Result<M, hmac::digest::InvalidLength>,
325) -> Result<String, CryptoError> {
326    let mut mac = new(key).map_err(|_| CryptoError::BadMacKey)?;
327    mac.update(data);
328    Ok(encode_hex(&mac.finalize().into_bytes()))
329}
330
331pub fn hmac_hex(algorithm: Algorithm, key: &[u8], data: &[u8]) -> Result<String, CryptoError> {
332    match algorithm {
333        Algorithm::Sha1 => mac_hex(key, data, Hmac::<Sha1>::new_from_slice),
334        Algorithm::Sha256 => mac_hex(key, data, Hmac::<Sha256>::new_from_slice),
335        Algorithm::Sha512 => mac_hex(key, data, Hmac::<Sha512>::new_from_slice),
336        Algorithm::Blake3 => Err(CryptoError::NoMac {
337            name: algorithm.name(),
338            known: Algorithm::MACS,
339        }),
340    }
341}
342
343pub fn scan_differences(a: &[u8], b: &[u8]) -> (u8, usize) {
344    let width = a.len().max(b.len());
345    let mut acc = 0u8;
346    let mut visited = 0usize;
347    for i in 0..width {
348        let x = a.get(i).copied().unwrap_or(0);
349        let y = b.get(i).copied().unwrap_or(0);
350        acc |= x ^ y;
351        visited += 1;
352    }
353    (acc, visited)
354}
355
356pub fn bytes_equal(a: &[u8], b: &[u8]) -> bool {
357    let (acc, _) = scan_differences(a, b);
358    let lengths = (a.len() ^ b.len()) as u64;
359    std::hint::black_box(acc) == 0 && std::hint::black_box(lengths) == 0
360}
361
362/// Raise a refusal as the named primitive's error.
363fn refuse(name: &'static str, span: tatara_lisp::Span) -> impl Fn(CryptoError) -> EvalError {
364    move |e| EvalError::native_fn(name, e.to_string(), span)
365}
366
367fn hex_value(bytes: &[u8]) -> Value {
368    Value::Str(Arc::from(encode_hex(bytes)))
369}
370
371/// Install blue's cryptography surface.
372pub fn install_crypto_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
373    interp.register_fn(
374        "blake3_hex",
375        Arity::Exact(1),
376        |a: &[Value], _h: &mut H, span| {
377            let data = message_bytes(&a[0]).map_err(refuse("blake3_hex", span))?;
378            Ok(Value::Str(Arc::from(blake3_hex(&data))))
379        },
380    );
381
382    interp.register_fn(
383        "digest_hex",
384        Arity::Exact(2),
385        |a: &[Value], _h: &mut H, span| {
386            let algorithm = Algorithm::from_value(&a[0]).map_err(refuse("digest_hex", span))?;
387            let data = message_bytes(&a[1]).map_err(refuse("digest_hex", span))?;
388            Ok(Value::Str(Arc::from(digest_hex(algorithm, &data))))
389        },
390    );
391
392    interp.register_fn(
393        "hmac_hex",
394        Arity::Exact(3),
395        |a: &[Value], _h: &mut H, span| {
396            let algorithm = Algorithm::from_value(&a[0]).map_err(refuse("hmac_hex", span))?;
397            let key = message_bytes(&a[1]).map_err(refuse("hmac_hex", span))?;
398            let data = message_bytes(&a[2]).map_err(refuse("hmac_hex", span))?;
399            let tag = hmac_hex(algorithm, &key, &data).map_err(refuse("hmac_hex", span))?;
400            Ok(Value::Str(Arc::from(tag)))
401        },
402    );
403
404    interp.register_fn(
405        "bytes_equal?",
406        Arity::Exact(2),
407        |a: &[Value], _h: &mut H, span| {
408            let left = message_bytes(&a[0]).map_err(refuse("bytes_equal?", span))?;
409            let right = message_bytes(&a[1]).map_err(refuse("bytes_equal?", span))?;
410            Ok(Value::Bool(bytes_equal(&left, &right)))
411        },
412    );
413
414    interp.register_fn(
415        "ed25519_keypair",
416        Arity::Exact(1),
417        |a: &[Value], _h: &mut H, span| {
418            let seed: [u8; KEY_BYTES] =
419                hex_array(&a[0], "seed").map_err(refuse("ed25519_keypair", span))?;
420            Ok(Value::list([
421                hex_value(&seed),
422                hex_value(&ed25519_public(&seed)),
423            ]))
424        },
425    );
426
427    interp.register_fn(
428        "ed25519_sign",
429        Arity::Exact(2),
430        |a: &[Value], _h: &mut H, span| {
431            let secret: [u8; KEY_BYTES] =
432                hex_array(&a[0], "secret key").map_err(refuse("ed25519_sign", span))?;
433            let message = message_bytes(&a[1]).map_err(refuse("ed25519_sign", span))?;
434            Ok(hex_value(&ed25519_sign(&secret, &message)))
435        },
436    );
437
438    interp.register_fn(
439        "ed25519_verify",
440        Arity::Exact(3),
441        |a: &[Value], _h: &mut H, span| {
442            let public: [u8; KEY_BYTES] =
443                hex_array(&a[0], "public key").map_err(refuse("ed25519_verify", span))?;
444            let message = message_bytes(&a[1]).map_err(refuse("ed25519_verify", span))?;
445            let signature: [u8; SIGNATURE_BYTES] =
446                hex_array(&a[2], "signature").map_err(refuse("ed25519_verify", span))?;
447            let verdict = ed25519_verify(&public, &message, &signature)
448                .map_err(refuse("ed25519_verify", span))?;
449            Ok(Value::Bool(verdict))
450        },
451    );
452}
453
454#[cfg(test)]
455mod tests {
456    //! The oracle is never this module. Every expected value below is copied
457    //! from a document written by someone else: RFC 8032 §7.1 for Ed25519, and
458    //! BLAKE3's official `test_vectors/test_vectors.json` for hashing (whose
459    //! input for length `n` is the byte sequence `i % 251` for `i` in `0..n`).
460    //!
461    //! # Red runs, recorded
462    //!
463    //! Each was performed on 2026-09-24, observed to fail, and reverted.
464    //!
465    //! 1. **A signer that signs the wrong bytes.** `ed25519_sign` changed to
466    //!    sign `message` with one `0x00` byte appended: 4 red —
467    //!    `rfc8032_test_1`, `_2`, `_3` on `signature mismatch` (their public
468    //!    keys still matched, which is why keys and signatures are asserted
469    //!    separately), and `a_signature_round_trips_through_blue`, because the
470    //!    verifier was still honest. A round trip alone would NOT catch a
471    //!    signer and verifier broken the same way; the vectors would.
472    //! 2. **A verifier that always says yes.** `ed25519_verify` changed to
473    //!    return `Ok(true)` after the key checks: 3 red —
474    //!    `a_tampered_message_does_not_verify`, `a_wrong_key_does_not_verify`,
475    //!    `a_tampered_signature_does_not_verify`. Every vector test stayed
476    //!    green, which is the case for the controls: vectors only ever ask a
477    //!    verifier to say yes.
478    //! 3. **A hash of the wrong bytes.** `blake3_hex` changed to
479    //!    `blake3::keyed_hash` with an all-zero key: 3 red — every `blake3_*`
480    //!    vector test, the empty input included. `text_and_bytes_are_one_message`
481    //!    stayed green, as it must: it compares the function with itself, so
482    //!    it proves the byte mapping and nothing about the hash.
483    use super::*;
484
485    fn eval(src: &str) -> Value {
486        crate::run(src)
487            .unwrap_or_else(|e| panic!("{src:?}: {e}"))
488            .value
489    }
490
491    fn s(src: &str) -> String {
492        match eval(src) {
493            Value::Str(v) => v.to_string(),
494            other => panic!("{src:?} produced {other:?}"),
495        }
496    }
497
498    fn b(src: &str) -> bool {
499        match eval(src) {
500            Value::Bool(v) => v,
501            other => panic!("{src:?} produced {other:?}"),
502        }
503    }
504
505    fn err(src: &str) -> String {
506        crate::run(src)
507            .map(|r| r.value)
508            .expect_err("must raise")
509            .to_string()
510    }
511
512    /// One RFC 8032 §7.1 vector, joined from the RFC's wrapped lines.
513    struct Vector {
514        secret: &'static str,
515        public: &'static str,
516        /// The message as a blue expression.
517        message: &'static str,
518        signature: &'static str,
519    }
520
521    const TEST_1: Vector = Vector {
522        secret: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
523        public: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
524        message: "\"\"",
525        signature: "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155\
526                    5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
527    };
528
529    /// MESSAGE `72` — one byte, which is ASCII `r`, so it is written as text.
530    const TEST_2: Vector = Vector {
531        secret: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
532        public: "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
533        message: "\"r\"",
534        signature: "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da\
535                    085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
536    };
537
538    /// MESSAGE `af82` — not valid UTF-8, so it can only be written as bytes.
539    /// This is the vector that exercises the byte-list path.
540    const TEST_3: Vector = Vector {
541        secret: "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
542        public: "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
543        message: "[175, 130]",
544        signature: "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac\
545                    18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
546    };
547
548    fn check_vector(v: &Vector) {
549        let pair = eval(&["ed25519_keypair(\"", v.secret, "\")"].concat());
550        let Value::List(pair) = pair else {
551            panic!("keypair must be a list, got {pair:?}")
552        };
553        assert!(matches!(&pair[0], Value::Str(x) if &**x == v.secret));
554        assert!(
555            matches!(&pair[1], Value::Str(x) if &**x == v.public),
556            "public key mismatch: {:?}",
557            pair[1]
558        );
559        let sig = s(&["ed25519_sign(\"", v.secret, "\", ", v.message, ")"].concat());
560        assert_eq!(sig, v.signature, "signature mismatch");
561        assert!(b(&[
562            "ed25519_verify(\"",
563            v.public,
564            "\", ",
565            v.message,
566            ", \"",
567            v.signature,
568            "\")"
569        ]
570        .concat()));
571    }
572
573    #[test]
574    fn rfc8032_test_1() {
575        check_vector(&TEST_1);
576    }
577
578    #[test]
579    fn rfc8032_test_2() {
580        check_vector(&TEST_2);
581    }
582
583    #[test]
584    fn rfc8032_test_3() {
585        check_vector(&TEST_3);
586    }
587
588    /// BLAKE3's official vectors: input of length `n` is `i % 251`.
589    fn official_input(n: usize) -> Vec<u8> {
590        (0..n).map(|i| (i % 251) as u8).collect()
591    }
592
593    #[test]
594    fn blake3_the_empty_input() {
595        assert_eq!(
596            s("blake3_hex(\"\")"),
597            "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
598        );
599        // The same empty input, spelled as bytes.
600        assert_eq!(
601            s("blake3_hex([])"),
602            "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
603        );
604    }
605
606    #[test]
607    fn blake3_one_byte_through_blue() {
608        assert_eq!(
609            s("blake3_hex([0])"),
610            "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213"
611        );
612    }
613
614    /// Across the one-chunk boundary (1024 bytes) and into a multi-chunk tree,
615    /// where a SIMD or tree-hashing path would diverge first.
616    #[test]
617    fn blake3_across_the_chunk_boundary() {
618        for (n, want) in [
619            (
620                3,
621                "e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f",
622            ),
623            (
624                1023,
625                "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11",
626            ),
627            (
628                1024,
629                "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af7",
630            ),
631            (
632                1025,
633                "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444",
634            ),
635            (
636                8193,
637                "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3b",
638            ),
639        ] {
640            assert_eq!(blake3_hex(&official_input(n)), want, "input_len {n}");
641        }
642    }
643
644    /// A string hashes as its UTF-8 bytes, so text and bytes agree.
645    #[test]
646    fn text_and_bytes_are_one_message() {
647        assert_eq!(s("blake3_hex(\"abc\")"), s("blake3_hex([97, 98, 99])"));
648        // "é" is two bytes in UTF-8, not one character code.
649        assert_eq!(s("blake3_hex(\"é\")"), s("blake3_hex([195, 169])"));
650    }
651
652    #[test]
653    fn a_signature_round_trips_through_blue() {
654        let sig = s(&["ed25519_sign(\"", TEST_2.secret, "\", \"a permit\")"].concat());
655        assert!(b(&[
656            "ed25519_verify(\"",
657            TEST_2.public,
658            "\", \"a permit\", \"",
659            &sig,
660            "\")"
661        ]
662        .concat()));
663    }
664
665    // ── controls: what must NOT verify ─────────────────────────────────
666
667    #[test]
668    fn a_tampered_message_does_not_verify() {
669        // TEST 2 signs "r"; the same signature over "s" must fail.
670        assert!(!b(&[
671            "ed25519_verify(\"",
672            TEST_2.public,
673            "\", \"s\", \"",
674            TEST_2.signature,
675            "\")"
676        ]
677        .concat()));
678    }
679
680    #[test]
681    fn a_wrong_key_does_not_verify() {
682        // TEST 2's signature, checked against TEST 1's key.
683        assert!(!b(&[
684            "ed25519_verify(\"",
685            TEST_1.public,
686            "\", \"r\", \"",
687            TEST_2.signature,
688            "\")"
689        ]
690        .concat()));
691    }
692
693    #[test]
694    fn a_tampered_signature_does_not_verify() {
695        // Flip the last hex digit of TEST 1's signature.
696        let mut forged = TEST_1.signature.to_string();
697        forged.pop();
698        forged.push('a');
699        assert_ne!(forged, TEST_1.signature);
700        assert!(!b(&[
701            "ed25519_verify(\"",
702            TEST_1.public,
703            "\", \"\", \"",
704            &forged,
705            "\")"
706        ]
707        .concat()));
708    }
709
710    // ── bad input raises, and names what was wrong ─────────────────────
711
712    #[test]
713    fn a_short_key_is_a_named_error_not_false() {
714        let e = err("ed25519_verify(\"abcd\", \"m\", \"00\")");
715        assert!(e.contains("ed25519_verify"), "{e}");
716        assert!(e.contains("public key is 2 bytes"), "{e}");
717    }
718
719    #[test]
720    fn non_hex_is_a_named_error() {
721        let bad = ["\"zz", &TEST_1.secret[2..], "\""].concat();
722        let e = err(&["ed25519_sign(", &bad, ", \"m\")"].concat());
723        assert!(e.contains("not hex: character 0 is `z`"), "{e}");
724    }
725
726    #[test]
727    fn odd_hex_is_a_named_error() {
728        let e = err("ed25519_keypair(\"abc\")");
729        assert!(e.contains("odd number of hex digits (3)"), "{e}");
730    }
731
732    #[test]
733    fn a_key_that_is_not_a_string_is_a_named_error() {
734        let e = err("ed25519_keypair(42)");
735        assert!(e.contains("seed must be a hex string; got int"), "{e}");
736    }
737
738    #[test]
739    fn a_non_message_is_a_named_error() {
740        let e = err("blake3_hex(nil)");
741        assert!(
742            e.contains("a message is a string or a list of bytes"),
743            "{e}"
744        );
745        let e = err("blake3_hex(42)");
746        assert!(e.contains("; got int"), "{e}");
747    }
748
749    #[test]
750    fn a_byte_out_of_range_is_a_named_error() {
751        let e = err("blake3_hex([1, 256])");
752        assert!(e.contains("element 1 of the byte list is 256"), "{e}");
753        let e = err("blake3_hex([1, \"x\"])");
754        assert!(
755            e.contains("element 1 of the byte list is not an integer; got string"),
756            "{e}"
757        );
758    }
759
760    /// Uppercase hex is accepted; output is always lowercase.
761    #[test]
762    fn hex_input_is_case_insensitive_and_output_is_lowercase() {
763        let upper = TEST_1.secret.to_uppercase();
764        let pair = eval(&["ed25519_keypair(\"", &upper, "\")"].concat());
765        let Value::List(pair) = pair else { panic!() };
766        assert!(matches!(&pair[0], Value::Str(x) if &**x == TEST_1.secret));
767        assert!(matches!(&pair[1], Value::Str(x) if &**x == TEST_1.public));
768    }
769
770    /// A 32-byte string that is not a compressed curve point is refused, not
771    /// answered `false`. `y = 2` has no square root for `x` on Ed25519, which
772    /// is a property of the curve and not of this code.
773    #[test]
774    fn a_public_key_off_the_curve_is_a_named_error() {
775        let mut off = [0u8; KEY_BYTES];
776        off[0] = 2;
777        assert!(
778            VerifyingKey::from_bytes(&off).is_err(),
779            "control: y=2 is off-curve"
780        );
781        let e = err(&[
782            "ed25519_verify(\"",
783            &encode_hex(&off),
784            "\", \"m\", \"",
785            TEST_1.signature,
786            "\")",
787        ]
788        .concat());
789        assert!(e.contains("not a point on the Ed25519 curve"), "{e}");
790    }
791
792    /// The identity point (encoded `01 00 … 00`) has order 1 — the weakest key
793    /// there is — and must be refused before any signature is considered.
794    #[test]
795    fn a_small_order_public_key_is_a_named_error() {
796        let mut identity = [0u8; KEY_BYTES];
797        identity[0] = 1;
798        let key = VerifyingKey::from_bytes(&identity).expect("the identity decompresses");
799        assert!(key.is_weak(), "control: the identity is small-order");
800        let e = err(&[
801            "ed25519_verify(\"",
802            &encode_hex(&identity),
803            "\", \"m\", \"",
804            TEST_1.signature,
805            "\")",
806        ]
807        .concat());
808        assert!(e.contains("small order"), "{e}");
809    }
810
811    #[test]
812    fn hex_round_trips() {
813        let all: Vec<u8> = (0..=255).collect();
814        let text = encode_hex(&all);
815        assert_eq!(decode_hex(&text, "t").expect("round trip"), all);
816        assert_eq!(decode_hex("", "t").expect("empty"), Vec::<u8>::new());
817    }
818
819    const SHA: [(&str, &str, &str, &str); 3] = [
820        ("sha1", "da39a3ee5e6b4b0d3255bfef95601890afd80709", "a9993e364706816aba3e25717850c26c9cd0d89d", "6244e66451a1c8695db9731ce2c4fd5de25ccf87"),
821        ("sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "4cbc96099a6467ce002461f10549b4898265ebe6188b45efacc44293516e62c4"),
822        ("sha512", "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", "aee650458ddcbcd4557531b27f1cded4c577f576079b086f57f3c6b9e037a7c0b3cdf7ff58afa09fe279076d1107d5d554f4d34fc6f03d8fd12f93d892802ac3"),
823    ];
824
825    fn bytes_literal(byte: u8, count: usize) -> String {
826        format!("[{}]", vec![byte.to_string(); count].join(", "))
827    }
828
829    #[test]
830    fn digests_match_fips_180_for_the_empty_input_and_abc() {
831        for (name, empty, abc, _) in SHA {
832            assert_eq!(
833                s(&format!("digest_hex(\"{name}\", \"\")")),
834                empty,
835                "{name} empty"
836            );
837            assert_eq!(
838                s(&format!("digest_hex(\"{name}\", \"abc\")")),
839                abc,
840                "{name} abc"
841            );
842        }
843    }
844
845    #[test]
846    fn digests_take_text_and_bytes_and_a_keyword_name() {
847        for (name, _, abc, _) in SHA {
848            assert_eq!(
849                s(&format!("digest_hex(\"{name}\", [97, 98, 99])")),
850                abc,
851                "{name} bytes"
852            );
853            assert_eq!(
854                s(&format!("digest_hex(:{name}, \"abc\")")),
855                abc,
856                "{name} keyword"
857            );
858        }
859        assert_eq!(s("digest_hex(\"blake3\", \"\")"), s("blake3_hex(\"\")"));
860    }
861
862    #[test]
863    fn digests_differ_by_algorithm_and_by_input() {
864        let all: std::collections::BTreeSet<String> = ["sha1", "sha256", "sha512", "blake3"]
865            .iter()
866            .map(|n| s(&format!("digest_hex(\"{n}\", \"abc\")")))
867            .collect();
868        assert_eq!(all.len(), 4);
869        assert_ne!(
870            s("digest_hex(\"sha256\", \"abc\")"),
871            s("digest_hex(\"sha256\", \"abd\")")
872        );
873    }
874
875    #[test]
876    fn digest_hex_refuses_what_it_cannot_place() {
877        assert!(err("digest_hex(\"md5\", \"x\")").contains("unknown algorithm `md5`"));
878        assert!(err("digest_hex(\"SHA256\", \"x\")").contains("unknown algorithm"));
879        assert!(err("digest_hex(256, \"x\")").contains("string or keyword"));
880        assert!(err("digest_hex(nil, \"x\")").contains("string or keyword"));
881        assert!(err("digest_hex(\"sha256\", nil)").contains("string or a list of bytes"));
882        assert!(err("digest_hex(\"sha256\", [256])").contains("outside 0..=255"));
883        assert!(err("digest_hex(\"sha256\")").contains("digest_hex"));
884    }
885
886    #[test]
887    fn hmac_matches_rfc_4231_and_rfc_2202() {
888        let key = bytes_literal(0x0b, 20);
889        let long_key = bytes_literal(0xaa, 131);
890        let cases: [(&str, &str, &str, &str); 3] = [
891            ("sha1", "b617318655057264e28bc0b6fb378c8ef146be00", "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79", "90d0dace1c1bdc957339307803160335bde6df2b"),
892            ("sha256", "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"),
893            ("sha512", "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854", "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737", "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598"),
894        ];
895        for (name, one, two, long) in cases {
896            assert_eq!(
897                s(&format!("hmac_hex(\"{name}\", {key}, \"Hi There\")")),
898                one,
899                "{name} case 1"
900            );
901            assert_eq!(
902                s(&format!(
903                    "hmac_hex(:{name}, \"Jefe\", \"what do ya want for nothing?\")"
904                )),
905                two,
906                "{name} case 2"
907            );
908            assert_eq!(
909                s(&format!("hmac_hex(\"{name}\", {long_key}, \"Test Using Larger Than Block-Size Key - Hash Key First\")")),
910                long,
911                "{name} long key"
912            );
913        }
914    }
915
916    #[test]
917    fn hmac_with_an_empty_key_is_defined_and_differs_from_the_plain_digest() {
918        for (name, _, _, empty_key) in SHA {
919            assert_eq!(
920                s(&format!("hmac_hex(\"{name}\", \"\", \"x\")")),
921                empty_key,
922                "{name}"
923            );
924            assert_ne!(
925                s(&format!("hmac_hex(\"{name}\", \"\", \"x\")")),
926                s(&format!("digest_hex(\"{name}\", \"x\")"))
927            );
928        }
929        assert_eq!(
930            s("hmac_hex(\"sha256\", [], \"x\")"),
931            s("hmac_hex(\"sha256\", \"\", \"x\")")
932        );
933    }
934
935    #[test]
936    fn hmac_depends_on_the_key_and_the_data() {
937        let base = s("hmac_hex(\"sha256\", \"k\", \"m\")");
938        assert_ne!(base, s("hmac_hex(\"sha256\", \"k2\", \"m\")"));
939        assert_ne!(base, s("hmac_hex(\"sha256\", \"k\", \"m2\")"));
940    }
941
942    #[test]
943    fn hmac_refuses_blake3_unknown_names_and_bad_arguments() {
944        assert!(err("hmac_hex(\"blake3\", \"k\", \"m\")").contains("no keyed form"));
945        assert!(err("hmac_hex(\"md5\", \"k\", \"m\")").contains("unknown algorithm"));
946        assert!(err("hmac_hex(1, \"k\", \"m\")").contains("string or keyword"));
947        assert!(err("hmac_hex(\"sha256\", nil, \"m\")").contains("string or a list of bytes"));
948        assert!(err("hmac_hex(\"sha256\", \"k\", nil)").contains("string or a list of bytes"));
949        assert!(err("hmac_hex(\"sha256\", \"k\")").contains("hmac_hex"));
950    }
951
952    #[test]
953    fn bytes_equal_answers_for_text_bytes_and_empties() {
954        assert!(b("bytes_equal?(\"abc\", \"abc\")"));
955        assert!(b("bytes_equal?(\"abc\", [97, 98, 99])"));
956        assert!(b("bytes_equal?(\"\", [])"));
957        assert!(!b("bytes_equal?(\"abc\", \"abd\")"));
958        assert!(!b("bytes_equal?(\"abc\", \"ab\")"));
959        assert!(!b("bytes_equal?(\"ab\", \"abc\")"));
960        assert!(!b("bytes_equal?(\"\", \"a\")"));
961        assert!(!b("bytes_equal?([1], [1, 0])"));
962    }
963
964    #[test]
965    fn bytes_equal_refuses_what_is_not_a_message() {
966        assert!(err("bytes_equal?(nil, \"a\")").contains("string or a list of bytes"));
967        assert!(err("bytes_equal?(\"a\", 1)").contains("string or a list of bytes"));
968        assert!(err("bytes_equal?([300], \"a\")").contains("outside 0..=255"));
969        assert!(err("bytes_equal?(\"a\")").contains("bytes_equal?"));
970    }
971
972    #[test]
973    fn the_scan_visits_every_position_whatever_the_first_difference() {
974        let reference = vec![7u8; 64];
975        for first_difference in 0..64 {
976            let mut other = reference.clone();
977            other[first_difference] ^= 0xff;
978            let (acc, visited) = scan_differences(&reference, &other);
979            assert_ne!(acc, 0, "difference at {first_difference} must register");
980            assert_eq!(
981                visited, 64,
982                "stopped early when the first difference was at {first_difference}"
983            );
984        }
985        let (acc, visited) = scan_differences(&reference, &reference);
986        assert_eq!((acc, visited), (0, 64));
987    }
988
989    #[test]
990    fn the_scan_covers_the_longer_side_when_lengths_differ() {
991        assert_eq!(scan_differences(b"", b"abcd").1, 4);
992        assert_eq!(scan_differences(b"abcd", b"").1, 4);
993        assert_eq!(scan_differences(b"x", b"yzzz").1, 4);
994        assert_eq!(scan_differences(b"", b"").1, 0);
995    }
996
997    #[test]
998    fn the_scan_accumulates_every_difference_not_just_the_last() {
999        let (acc, _) = scan_differences(&[0b0001, 0b0000, 0b0000], &[0b0000, 0b0000, 0b0000]);
1000        assert_ne!(acc, 0, "a difference before equal tail bytes must survive");
1001        let (acc, _) = scan_differences(&[0b0001, 0b0010], &[0b0000, 0b0000]);
1002        assert_eq!(acc, 0b0011);
1003    }
1004}