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 tatara_lisp_eval::ffi::Arity;
80use tatara_lisp_eval::{EvalError, Interpreter, Value};
81
82/// Bytes in an Ed25519 secret key (the RFC 8032 seed) and in a public key.
83pub const KEY_BYTES: usize = 32;
84/// Bytes in an Ed25519 signature.
85pub const SIGNATURE_BYTES: usize = 64;
86
87/// Every way an argument to this layer can be refused.
88///
89/// Closed, and each arm reads differently, so a reader of the raised message is
90/// sent to the one thing that was wrong.
91#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
92pub enum CryptoError {
93    /// A key or signature argument that is not a string at all.
94    #[error("the {what} must be a hex string; got {got}")]
95    NotHexText {
96        what: &'static str,
97        got: &'static str,
98    },
99    /// A character outside `0-9a-fA-F`, at a character position.
100    #[error("the {what} is not hex: character {position} is `{found}`")]
101    NotHex {
102        what: &'static str,
103        position: usize,
104        found: char,
105    },
106    /// An odd number of hex digits cannot be whole bytes.
107    #[error("the {what} has an odd number of hex digits ({digits})")]
108    OddHex { what: &'static str, digits: usize },
109    /// Well-formed hex of the wrong size.
110    #[error("the {what} is {got} bytes; an Ed25519 {what} is {expected}")]
111    WrongLength {
112        what: &'static str,
113        expected: usize,
114        got: usize,
115    },
116    /// Something that is neither text nor a byte list.
117    #[error("a message is a string or a list of bytes; got {got}")]
118    NotAMessage { got: &'static str },
119    /// A byte-list element that is not an integer.
120    #[error("element {index} of the byte list is not an integer; got {got}")]
121    NotAByte { index: usize, got: &'static str },
122    /// A byte-list element outside `0..=255`.
123    #[error("element {index} of the byte list is {value}, outside 0..=255")]
124    ByteOutOfRange { index: usize, value: i64 },
125    /// 32 bytes that do not decompress to a point on the curve.
126    #[error("the public key is not a point on the Ed25519 curve")]
127    NotACurvePoint,
128    /// A point of small order: every signature under it is meaningless.
129    #[error("the public key has small order and cannot vouch for any message")]
130    WeakPublicKey,
131}
132
133/// The bytes a message or hash input stands for.
134///
135/// A string is its UTF-8 bytes, so `"r"` and `[114]` are one message — bytes
136/// are bytes, whichever way they were spelled. `nil` is refused rather than
137/// read as empty: blue has two empties (`nil != []`), and a message that
138/// silently became nil is a bug to surface, not an empty string to sign.
139pub fn message_bytes(v: &Value) -> Result<Vec<u8>, CryptoError> {
140    match v {
141        Value::Str(s) => Ok(s.as_bytes().to_vec()),
142        Value::List(items) => items
143            .iter()
144            .enumerate()
145            .map(|(index, item)| match item {
146                Value::Int(n) => {
147                    u8::try_from(*n).map_err(|_| CryptoError::ByteOutOfRange { index, value: *n })
148                }
149                other => Err(CryptoError::NotAByte {
150                    index,
151                    got: other.type_name(),
152                }),
153            })
154            .collect(),
155        other => Err(CryptoError::NotAMessage {
156            got: other.type_name(),
157        }),
158    }
159}
160
161/// Decode a hex argument of exactly `N` bytes.
162pub fn hex_array<const N: usize>(v: &Value, what: &'static str) -> Result<[u8; N], CryptoError> {
163    let text = match v {
164        Value::Str(s) => s,
165        other => {
166            return Err(CryptoError::NotHexText {
167                what,
168                got: other.type_name(),
169            })
170        }
171    };
172    let bytes = decode_hex(text, what)?;
173    <[u8; N]>::try_from(bytes.as_slice()).map_err(|_| CryptoError::WrongLength {
174        what,
175        expected: N,
176        got: bytes.len(),
177    })
178}
179
180/// Strict hex: digits only, either case, an even count. No `0x`, no spaces.
181fn decode_hex(text: &str, what: &'static str) -> Result<Vec<u8>, CryptoError> {
182    let digits: Vec<u8> = text
183        .chars()
184        .enumerate()
185        .map(|(position, found)| {
186            found
187                .to_digit(16)
188                // `to_digit(16)` answers at most 15, so the narrowing is exact.
189                .map(|d| d as u8)
190                .ok_or(CryptoError::NotHex {
191                    what,
192                    position,
193                    found,
194                })
195        })
196        .collect::<Result<_, _>>()?;
197    if !digits.len().is_multiple_of(2) {
198        return Err(CryptoError::OddHex {
199            what,
200            digits: digits.len(),
201        });
202    }
203    Ok(digits
204        .as_chunks::<2>()
205        .0
206        .iter()
207        .map(|[hi, lo]| (hi << 4) | lo)
208        .collect())
209}
210
211/// Lowercase hex, the one output spelling.
212pub fn encode_hex(bytes: &[u8]) -> String {
213    const DIGITS: &[u8; 16] = b"0123456789abcdef";
214    let mut out = String::with_capacity(bytes.len() * 2);
215    for b in bytes {
216        out.push(char::from(DIGITS[usize::from(b >> 4)]));
217        out.push(char::from(DIGITS[usize::from(b & 0x0f)]));
218    }
219    out
220}
221
222/// BLAKE3-256 of `data`, as lowercase hex.
223pub fn blake3_hex(data: &[u8]) -> String {
224    blake3::hash(data).to_hex().to_string()
225}
226
227/// The public key for an RFC 8032 secret key (the 32-byte seed).
228pub fn ed25519_public(secret: &[u8; KEY_BYTES]) -> [u8; KEY_BYTES] {
229    SigningKey::from_bytes(secret).verifying_key().to_bytes()
230}
231
232/// Sign `message` with an RFC 8032 secret key. Ed25519 signing is
233/// deterministic: one (key, message) pair has one signature.
234pub fn ed25519_sign(secret: &[u8; KEY_BYTES], message: &[u8]) -> [u8; SIGNATURE_BYTES] {
235    SigningKey::from_bytes(secret).sign(message).to_bytes()
236}
237
238/// Verify strictly. `Ok(false)` is a well-formed signature that does not
239/// verify; `Err` is input that could not have verified anything.
240pub fn ed25519_verify(
241    public: &[u8; KEY_BYTES],
242    message: &[u8],
243    signature: &[u8; SIGNATURE_BYTES],
244) -> Result<bool, CryptoError> {
245    let key = VerifyingKey::from_bytes(public).map_err(|_| CryptoError::NotACurvePoint)?;
246    if key.is_weak() {
247        return Err(CryptoError::WeakPublicKey);
248    }
249    let signature = Signature::from_bytes(signature);
250    Ok(key.verify_strict(message, &signature).is_ok())
251}
252
253/// Raise a refusal as the named primitive's error.
254fn refuse(name: &'static str, span: tatara_lisp::Span) -> impl Fn(CryptoError) -> EvalError {
255    move |e| EvalError::native_fn(name, e.to_string(), span)
256}
257
258fn hex_value(bytes: &[u8]) -> Value {
259    Value::Str(Arc::from(encode_hex(bytes)))
260}
261
262/// Install blue's cryptography surface.
263pub fn install_crypto_stdlib<H: 'static>(interp: &mut Interpreter<H>) {
264    interp.register_fn(
265        "blake3_hex",
266        Arity::Exact(1),
267        |a: &[Value], _h: &mut H, span| {
268            let data = message_bytes(&a[0]).map_err(refuse("blake3_hex", span))?;
269            Ok(Value::Str(Arc::from(blake3_hex(&data))))
270        },
271    );
272
273    interp.register_fn(
274        "ed25519_keypair",
275        Arity::Exact(1),
276        |a: &[Value], _h: &mut H, span| {
277            let seed: [u8; KEY_BYTES] =
278                hex_array(&a[0], "seed").map_err(refuse("ed25519_keypair", span))?;
279            Ok(Value::List(Arc::new(vec![
280                hex_value(&seed),
281                hex_value(&ed25519_public(&seed)),
282            ])))
283        },
284    );
285
286    interp.register_fn(
287        "ed25519_sign",
288        Arity::Exact(2),
289        |a: &[Value], _h: &mut H, span| {
290            let secret: [u8; KEY_BYTES] =
291                hex_array(&a[0], "secret key").map_err(refuse("ed25519_sign", span))?;
292            let message = message_bytes(&a[1]).map_err(refuse("ed25519_sign", span))?;
293            Ok(hex_value(&ed25519_sign(&secret, &message)))
294        },
295    );
296
297    interp.register_fn(
298        "ed25519_verify",
299        Arity::Exact(3),
300        |a: &[Value], _h: &mut H, span| {
301            let public: [u8; KEY_BYTES] =
302                hex_array(&a[0], "public key").map_err(refuse("ed25519_verify", span))?;
303            let message = message_bytes(&a[1]).map_err(refuse("ed25519_verify", span))?;
304            let signature: [u8; SIGNATURE_BYTES] =
305                hex_array(&a[2], "signature").map_err(refuse("ed25519_verify", span))?;
306            let verdict = ed25519_verify(&public, &message, &signature)
307                .map_err(refuse("ed25519_verify", span))?;
308            Ok(Value::Bool(verdict))
309        },
310    );
311}
312
313#[cfg(test)]
314mod tests {
315    //! The oracle is never this module. Every expected value below is copied
316    //! from a document written by someone else: RFC 8032 §7.1 for Ed25519, and
317    //! BLAKE3's official `test_vectors/test_vectors.json` for hashing (whose
318    //! input for length `n` is the byte sequence `i % 251` for `i` in `0..n`).
319    //!
320    //! # Red runs, recorded
321    //!
322    //! Each was performed on 2026-09-24, observed to fail, and reverted.
323    //!
324    //! 1. **A signer that signs the wrong bytes.** `ed25519_sign` changed to
325    //!    sign `message` with one `0x00` byte appended: 4 red —
326    //!    `rfc8032_test_1`, `_2`, `_3` on `signature mismatch` (their public
327    //!    keys still matched, which is why keys and signatures are asserted
328    //!    separately), and `a_signature_round_trips_through_blue`, because the
329    //!    verifier was still honest. A round trip alone would NOT catch a
330    //!    signer and verifier broken the same way; the vectors would.
331    //! 2. **A verifier that always says yes.** `ed25519_verify` changed to
332    //!    return `Ok(true)` after the key checks: 3 red —
333    //!    `a_tampered_message_does_not_verify`, `a_wrong_key_does_not_verify`,
334    //!    `a_tampered_signature_does_not_verify`. Every vector test stayed
335    //!    green, which is the case for the controls: vectors only ever ask a
336    //!    verifier to say yes.
337    //! 3. **A hash of the wrong bytes.** `blake3_hex` changed to
338    //!    `blake3::keyed_hash` with an all-zero key: 3 red — every `blake3_*`
339    //!    vector test, the empty input included. `text_and_bytes_are_one_message`
340    //!    stayed green, as it must: it compares the function with itself, so
341    //!    it proves the byte mapping and nothing about the hash.
342    use super::*;
343
344    fn eval(src: &str) -> Value {
345        crate::run(src)
346            .unwrap_or_else(|e| panic!("{src:?}: {e}"))
347            .value
348    }
349
350    fn s(src: &str) -> String {
351        match eval(src) {
352            Value::Str(v) => v.to_string(),
353            other => panic!("{src:?} produced {other:?}"),
354        }
355    }
356
357    fn b(src: &str) -> bool {
358        match eval(src) {
359            Value::Bool(v) => v,
360            other => panic!("{src:?} produced {other:?}"),
361        }
362    }
363
364    fn err(src: &str) -> String {
365        crate::run(src)
366            .map(|r| r.value)
367            .expect_err("must raise")
368            .to_string()
369    }
370
371    /// One RFC 8032 §7.1 vector, joined from the RFC's wrapped lines.
372    struct Vector {
373        secret: &'static str,
374        public: &'static str,
375        /// The message as a blue expression.
376        message: &'static str,
377        signature: &'static str,
378    }
379
380    const TEST_1: Vector = Vector {
381        secret: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
382        public: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
383        message: "\"\"",
384        signature: "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155\
385                    5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
386    };
387
388    /// MESSAGE `72` — one byte, which is ASCII `r`, so it is written as text.
389    const TEST_2: Vector = Vector {
390        secret: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
391        public: "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
392        message: "\"r\"",
393        signature: "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da\
394                    085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
395    };
396
397    /// MESSAGE `af82` — not valid UTF-8, so it can only be written as bytes.
398    /// This is the vector that exercises the byte-list path.
399    const TEST_3: Vector = Vector {
400        secret: "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
401        public: "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
402        message: "[175, 130]",
403        signature: "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac\
404                    18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
405    };
406
407    fn check_vector(v: &Vector) {
408        let pair = eval(&["ed25519_keypair(\"", v.secret, "\")"].concat());
409        let Value::List(pair) = pair else {
410            panic!("keypair must be a list, got {pair:?}")
411        };
412        assert!(matches!(&pair[0], Value::Str(x) if &**x == v.secret));
413        assert!(
414            matches!(&pair[1], Value::Str(x) if &**x == v.public),
415            "public key mismatch: {:?}",
416            pair[1]
417        );
418        let sig = s(&["ed25519_sign(\"", v.secret, "\", ", v.message, ")"].concat());
419        assert_eq!(sig, v.signature, "signature mismatch");
420        assert!(b(&[
421            "ed25519_verify(\"",
422            v.public,
423            "\", ",
424            v.message,
425            ", \"",
426            v.signature,
427            "\")"
428        ]
429        .concat()));
430    }
431
432    #[test]
433    fn rfc8032_test_1() {
434        check_vector(&TEST_1);
435    }
436
437    #[test]
438    fn rfc8032_test_2() {
439        check_vector(&TEST_2);
440    }
441
442    #[test]
443    fn rfc8032_test_3() {
444        check_vector(&TEST_3);
445    }
446
447    /// BLAKE3's official vectors: input of length `n` is `i % 251`.
448    fn official_input(n: usize) -> Vec<u8> {
449        (0..n).map(|i| (i % 251) as u8).collect()
450    }
451
452    #[test]
453    fn blake3_the_empty_input() {
454        assert_eq!(
455            s("blake3_hex(\"\")"),
456            "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
457        );
458        // The same empty input, spelled as bytes.
459        assert_eq!(
460            s("blake3_hex([])"),
461            "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
462        );
463    }
464
465    #[test]
466    fn blake3_one_byte_through_blue() {
467        assert_eq!(
468            s("blake3_hex([0])"),
469            "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213"
470        );
471    }
472
473    /// Across the one-chunk boundary (1024 bytes) and into a multi-chunk tree,
474    /// where a SIMD or tree-hashing path would diverge first.
475    #[test]
476    fn blake3_across_the_chunk_boundary() {
477        for (n, want) in [
478            (
479                3,
480                "e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f",
481            ),
482            (
483                1023,
484                "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11",
485            ),
486            (
487                1024,
488                "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af7",
489            ),
490            (
491                1025,
492                "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444",
493            ),
494            (
495                8193,
496                "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3b",
497            ),
498        ] {
499            assert_eq!(blake3_hex(&official_input(n)), want, "input_len {n}");
500        }
501    }
502
503    /// A string hashes as its UTF-8 bytes, so text and bytes agree.
504    #[test]
505    fn text_and_bytes_are_one_message() {
506        assert_eq!(s("blake3_hex(\"abc\")"), s("blake3_hex([97, 98, 99])"));
507        // "é" is two bytes in UTF-8, not one character code.
508        assert_eq!(s("blake3_hex(\"é\")"), s("blake3_hex([195, 169])"));
509    }
510
511    #[test]
512    fn a_signature_round_trips_through_blue() {
513        let sig = s(&["ed25519_sign(\"", TEST_2.secret, "\", \"a permit\")"].concat());
514        assert!(b(&[
515            "ed25519_verify(\"",
516            TEST_2.public,
517            "\", \"a permit\", \"",
518            &sig,
519            "\")"
520        ]
521        .concat()));
522    }
523
524    // ── controls: what must NOT verify ─────────────────────────────────
525
526    #[test]
527    fn a_tampered_message_does_not_verify() {
528        // TEST 2 signs "r"; the same signature over "s" must fail.
529        assert!(!b(&[
530            "ed25519_verify(\"",
531            TEST_2.public,
532            "\", \"s\", \"",
533            TEST_2.signature,
534            "\")"
535        ]
536        .concat()));
537    }
538
539    #[test]
540    fn a_wrong_key_does_not_verify() {
541        // TEST 2's signature, checked against TEST 1's key.
542        assert!(!b(&[
543            "ed25519_verify(\"",
544            TEST_1.public,
545            "\", \"r\", \"",
546            TEST_2.signature,
547            "\")"
548        ]
549        .concat()));
550    }
551
552    #[test]
553    fn a_tampered_signature_does_not_verify() {
554        // Flip the last hex digit of TEST 1's signature.
555        let mut forged = TEST_1.signature.to_string();
556        forged.pop();
557        forged.push('a');
558        assert_ne!(forged, TEST_1.signature);
559        assert!(!b(&[
560            "ed25519_verify(\"",
561            TEST_1.public,
562            "\", \"\", \"",
563            &forged,
564            "\")"
565        ]
566        .concat()));
567    }
568
569    // ── bad input raises, and names what was wrong ─────────────────────
570
571    #[test]
572    fn a_short_key_is_a_named_error_not_false() {
573        let e = err("ed25519_verify(\"abcd\", \"m\", \"00\")");
574        assert!(e.contains("ed25519_verify"), "{e}");
575        assert!(e.contains("public key is 2 bytes"), "{e}");
576    }
577
578    #[test]
579    fn non_hex_is_a_named_error() {
580        let bad = ["\"zz", &TEST_1.secret[2..], "\""].concat();
581        let e = err(&["ed25519_sign(", &bad, ", \"m\")"].concat());
582        assert!(e.contains("not hex: character 0 is `z`"), "{e}");
583    }
584
585    #[test]
586    fn odd_hex_is_a_named_error() {
587        let e = err("ed25519_keypair(\"abc\")");
588        assert!(e.contains("odd number of hex digits (3)"), "{e}");
589    }
590
591    #[test]
592    fn a_key_that_is_not_a_string_is_a_named_error() {
593        let e = err("ed25519_keypair(42)");
594        assert!(e.contains("seed must be a hex string; got int"), "{e}");
595    }
596
597    #[test]
598    fn a_non_message_is_a_named_error() {
599        let e = err("blake3_hex(nil)");
600        assert!(
601            e.contains("a message is a string or a list of bytes"),
602            "{e}"
603        );
604        let e = err("blake3_hex(42)");
605        assert!(e.contains("; got int"), "{e}");
606    }
607
608    #[test]
609    fn a_byte_out_of_range_is_a_named_error() {
610        let e = err("blake3_hex([1, 256])");
611        assert!(e.contains("element 1 of the byte list is 256"), "{e}");
612        let e = err("blake3_hex([1, \"x\"])");
613        assert!(
614            e.contains("element 1 of the byte list is not an integer; got string"),
615            "{e}"
616        );
617    }
618
619    /// Uppercase hex is accepted; output is always lowercase.
620    #[test]
621    fn hex_input_is_case_insensitive_and_output_is_lowercase() {
622        let upper = TEST_1.secret.to_uppercase();
623        let pair = eval(&["ed25519_keypair(\"", &upper, "\")"].concat());
624        let Value::List(pair) = pair else { panic!() };
625        assert!(matches!(&pair[0], Value::Str(x) if &**x == TEST_1.secret));
626        assert!(matches!(&pair[1], Value::Str(x) if &**x == TEST_1.public));
627    }
628
629    /// A 32-byte string that is not a compressed curve point is refused, not
630    /// answered `false`. `y = 2` has no square root for `x` on Ed25519, which
631    /// is a property of the curve and not of this code.
632    #[test]
633    fn a_public_key_off_the_curve_is_a_named_error() {
634        let mut off = [0u8; KEY_BYTES];
635        off[0] = 2;
636        assert!(
637            VerifyingKey::from_bytes(&off).is_err(),
638            "control: y=2 is off-curve"
639        );
640        let e = err(&[
641            "ed25519_verify(\"",
642            &encode_hex(&off),
643            "\", \"m\", \"",
644            TEST_1.signature,
645            "\")",
646        ]
647        .concat());
648        assert!(e.contains("not a point on the Ed25519 curve"), "{e}");
649    }
650
651    /// The identity point (encoded `01 00 … 00`) has order 1 — the weakest key
652    /// there is — and must be refused before any signature is considered.
653    #[test]
654    fn a_small_order_public_key_is_a_named_error() {
655        let mut identity = [0u8; KEY_BYTES];
656        identity[0] = 1;
657        let key = VerifyingKey::from_bytes(&identity).expect("the identity decompresses");
658        assert!(key.is_weak(), "control: the identity is small-order");
659        let e = err(&[
660            "ed25519_verify(\"",
661            &encode_hex(&identity),
662            "\", \"m\", \"",
663            TEST_1.signature,
664            "\")",
665        ]
666        .concat());
667        assert!(e.contains("small order"), "{e}");
668    }
669
670    #[test]
671    fn hex_round_trips() {
672        let all: Vec<u8> = (0..=255).collect();
673        let text = encode_hex(&all);
674        assert_eq!(decode_hex(&text, "t").expect("round trip"), all);
675        assert_eq!(decode_hex("", "t").expect("empty"), Vec::<u8>::new());
676    }
677}