signed-ulid 0.1.1

Like ULIDs, but based on cryptographic signatures instead of randomness.
Documentation
#![cfg(test)]

use ed25519_dalek::{PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH, Signature, SigningKey, VerifyingKey};
use indoc::indoc;
use pretty_assertions::assert_eq;
use std::{
    fmt::Display,
    ops::Range,
    thread,
    time::{Duration, SystemTime},
};

use crate::{SignedPayload, Sulid, UnsignedPayload};

#[test]
fn the_tests() {
    let message = "This message will be signed and given an sULID.";

    let secret = SigningKey::from_bytes(&[
        43u8, 124, 75, 178, 112, 26, 99, 95, 202, 162, 110, 219, 239, 94, 157, 1, 8, 94, 219, 92,
        103, 89, 196, 8, 0, 221, 163, 183, 0, 198, 101, 61,
    ]);

    let app_context = b"my-app".to_vec();

    // Lock down "now" to get a repeatable sULID for testing:
    let now_ms: u64 = 1787794007019; // Aug 26 2026, ~6:27pm US Pacific.
    let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now_ms);

    let signed = Sulid::sign(
        &secret,
        UnsignedPayload {
            timestamp: now,
            message_hash: blake3hash(message.as_bytes()),
            message_length: message.as_bytes().len() as u64,
            app_metadata: app_context,
        },
    );

    assert_eq!("01M10D78ZBGZNVWHA60G8P95W1", signed.sulid.to_string());

    assert_eq!(true, signed.is_valid());

    // Changing any bit in the signed payload breaks validation:
    let mut bits_flipped = 0;
    let modified_payloads = flip_bits(signed.clone());
    while let Some(modified) = modified_payloads.recv().expect("recv()") {
        assert_eq!(false, modified.is_valid());
        bits_flipped += 1;
    }

    assert_eq!(1008, bits_flipped, "expected bit flips");

    // Example serialization into a plaintext message with headers:
    let mut lines = vec![];
    lines.push(format!("id: {}", signed.sulid));
    lines.push(format!("by: {}", b58(signed.public_key.as_bytes())));
    lines.push(format!("sig: {}", b58(&signed.signature.to_bytes())));
    lines.push("".into());
    lines.push(message.into());

    let text = lines.join("\n");
    let expected = indoc! {"
        id: 01M10D78ZBGZNVWHA60G8P95W1
        by: WchunVqWZD7TfVoYkM1BPCzDpsrKTyN8ur2aZxWwbjQ
        sig: 46kq3wNwQTjZKH9PjSWJeX9a7SwMkSni2t7hxorRDmgNXqrkYPey7WodyLs1npHBsFcdFGCJcV7dHF2hJtWPcpKR
        
        This message will be signed and given an sULID."
    };
    assert_eq!(expected, text);
}

fn b58(value: &[u8]) -> impl Display {
    bs58::encode(value).into_string()
}

/// Returns versions of the original SignedPayload with different bits flipped
fn flip_bits(original: SignedPayload) -> crossbeam::channel::Receiver<Option<SignedPayload>> {
    // abuse channels/threads to simulate a generator.
    let (tx, rx) = crossbeam::channel::bounded(1);

    thread::spawn(move || {
        for bit in original.sulid.bit_range() {
            let mut modified = original.clone();
            modified.sulid.flip_bit(bit);
            tx.send(Some(modified)).expect("tx.send()");
        }

        // Can't flip bits of the public_key, because it'll fail validation.
        // So, we'll skip that.

        for bit in original.signature.bit_range() {
            let mut modified = original.clone();
            modified.signature.flip_bit(bit);
            tx.send(Some(modified)).expect("tx.send()");
        }

        for bit in original.app_metadata.bit_range() {
            let mut modified = original.clone();
            modified.app_metadata.flip_bit(bit);
            tx.send(Some(modified)).expect("tx.send()");
        }

        for bit in original.message_hash.bit_range() {
            let mut modified = original.clone();
            modified.message_hash.flip_bit(bit);
            tx.send(Some(modified)).expect("tx.send()");
        }

        for bit in original.message_length.bit_range() {
            let mut modified = original.clone();
            modified.message_length.flip_bit(bit);
            tx.send(Some(modified)).expect("tx.send()");
        }

        // Signal we're done:
        tx.send(None).expect("tx.send()");
    });

    return rx;
}

pub fn blake3hash(bytes: &[u8]) -> blake3::Hash {
    let mut hasher = blake3::Hasher::new();
    hasher.update(bytes);
    hasher.finalize()
}

// pub fn random_secret() -> SigningKey {
//     // Really? I've got to:
//     // 1) enable the rand_core features in ed25519_dalek
//     // 2) add 2 more dependencies
//     // 3) AND do all of this rigamarole?
//     // TODO: Look for other ed25519 crates
//     use getrandom::SysRng;
//     use rand_core::UnwrapErr;
//     let mut prng = UnwrapErr(SysRng);
//     SigningKey::generate(&mut prng)
// }

trait FlipBit {
    fn bit_range(&self) -> Range<usize>;
    fn flip_bit(&mut self, offset: usize);
}

impl FlipBit for u8 {
    fn bit_range(&self) -> Range<usize> {
        0..8
    }

    fn flip_bit(&mut self, offset: usize) {
        let mask = match offset {
            0 => 0b10000000,
            1 => 0b01000000,
            2 => 0b00100000,
            3 => 0b00010000,
            4 => 0b00001000,
            5 => 0b00000100,
            6 => 0b00000010,
            7 => 0b00000001,
            _ => panic!("Offset out of bound for u8: {offset}"),
        };
        *self ^= mask;
    }
}

impl FlipBit for [u8] {
    fn bit_range(&self) -> Range<usize> {
        0..(self.len() * 8)
    }

    fn flip_bit(&mut self, offset: usize) {
        let byte_offset = offset / 8;
        let bit_offset = offset % 8;
        self[byte_offset].flip_bit(bit_offset);
    }
}

impl FlipBit for Sulid {
    fn bit_range(&self) -> Range<usize> {
        0..128
    }

    fn flip_bit(&mut self, offset: usize) {
        let mut bytes = self.to_bytes();
        bytes.flip_bit(offset);
        *self = bytes.into();
    }
}

impl FlipBit for VerifyingKey {
    fn bit_range(&self) -> Range<usize> {
        0..(PUBLIC_KEY_LENGTH * 8)
    }
    fn flip_bit(&mut self, offset: usize) {
        let mut bytes = self.to_bytes();
        bytes.flip_bit(offset);
        *self = VerifyingKey::from_bytes(&bytes).expect("valid public key?");
        eprintln!("flipping bit on key {offset}");
    }
}

impl FlipBit for Signature {
    fn bit_range(&self) -> Range<usize> {
        0..(SIGNATURE_LENGTH * 8)
    }

    fn flip_bit(&mut self, offset: usize) {
        let mut bytes = self.to_bytes();
        bytes.flip_bit(offset);
        *self = Signature::from_bytes(&bytes);
    }
}

impl FlipBit for blake3::Hash {
    fn bit_range(&self) -> Range<usize> {
        0..(blake3::OUT_LEN * 8)
    }

    fn flip_bit(&mut self, offset: usize) {
        let mut bytes = self.as_bytes().to_owned();
        bytes.flip_bit(offset);
        *self = bytes.into();
    }
}

impl FlipBit for u64 {
    fn bit_range(&self) -> Range<usize> {
        0..64
    }

    fn flip_bit(&mut self, offset: usize) {
        let mut bytes = self.to_be_bytes();
        bytes.flip_bit(offset);
        *self = u64::from_be_bytes(bytes);
    }
}