Skip to main content

Crate signed_ulid

Crate signed_ulid 

Source
Expand description

An sUILD is a “signed ULID”. It’s like a ULID, but works better in distributed systems.

§The Problem

Normal ULIDs have two parts:

  1. A 48-bit timestamp
  2. A random 80-bit suffix.

Together, these should be globally unique. However, in a distributed system composed of peers of varying trustworthiness, things can go wrong. Malicious peers are free to assign their own ULIDs which conflict with those that already exist in other systems. This could be used as a form of Denial of Service attack, if the attacker can cause their ULIDs to supersede or replace an existing ULID.

We need something like ULIDs, but with the following properties:

  1. Malicious users can not (easily) cause duplicate ULIDs to enter the system.
  2. System administrators can not modify the ULID for a message.

§The Solution

sULIDs solve each of the above needs.

  1. The “random” 80 bits of a ULID are replaced by 80 bits derived from a cryptographic signature. It is non-trivial to generate a signature that has a collision on these bits.
  2. The timestamp portion of the ULID is part of the signed payload, so an admin can not change the timestamp without breaking the sULID/signature relationship.

Additionally, the payload signed by sULIDs contains a blake3 hash of the content being signed. If the content is large, systems can take advantage of blake3 “verified streaming” to verify content bytes as they are being fetched.

§Example

let message = "This message will be signed and given an sULID.";
let app_context = b"my-app".to_vec();

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

println!("Generated sULID: {}", signed.sulid);
assert!(signed.is_valid());

This crate doesn’t dictate how you serialize the SignedPayload, only that you must be able to reconstruct it to validate that the sULID and signature are in agreement. For example, the above message and SignedPayload might be serialized into plaintext, with an inline message, like this:

id: 01M10D78ZBGZNVWHA60G8P95W1
by: WchunVqWZD7TfVoYkM1BPCzDpsrKTyN8ur2aZxWwbjQ
sig: 46kq3wNwQTjZKH9PjSWJeX9a7SwMkSni2t7hxorRDmgNXqrkYPey7WodyLs1npHBsFcdFGCJcV7dHF2hJtWPcpKR

This message will be signed and given an sULID.

You can reconstructed the SignedPayload fields sulid, public_key, and signature directly from the first 3 lines.

The message, which begins after the empty line, can be used to recalculate message_hash and message_length.

And app_metadata in this case is just hard-coded by our application to distinguish it from other signing schemes. But, it could be extended to allow more (signed!) fields in the header.

Re-exports§

pub use ed25519_dalek;

Structs§

SignedPayload
Output of Sulid::sign, also used to verify sULIDs.
Sulid
A “signed” ULID, whose “random” portion is generated by a cryptographic signature.
UnsignedPayload
Passed to Sulid::sign to create an sULID.

Enums§

UlidDecodeError
An error that can occur when decoding a base32 string