SigilGuard.Envelope (SigilGuard v0.2.0)

View Source

SIGIL envelope implementation for MCP JSON-RPC _sigil metadata.

Implements the envelope format defined by the SIGIL protocol:

  • Canonical bytes: lexicographic key order, compact JSON, no whitespace, excluding signature and reason
  • Ed25519 signature, base64url-encoded (no padding)
  • ISO 8601 timestamp with millisecond precision (UTC)
  • 16-byte cryptographically random nonce, hex-encoded

Signing

Signing requires a module implementing SigilGuard.Signer:

envelope = SigilGuard.Envelope.sign("did:sigil:abc", :allowed, signer: MySigner)

Verification

:ok = SigilGuard.Envelope.verify(envelope, public_key_b64u)

Summary

Functions

Produce the canonical byte representation for signing.

Generate a 16-byte cryptographically random nonce as hex.

Generate an ISO 8601 timestamp with millisecond precision.

Sign an envelope with the given identity and verdict.

Verify an envelope's signature against a base64url-encoded Ed25519 public key.

Types

t()

@type t() :: %{required(String.t()) => String.t()}

verdict()

@type verdict() :: :allowed | :blocked | :scanned

Functions

canonical_bytes(identity, verdict, timestamp, nonce_hex)

@spec canonical_bytes(String.t(), verdict(), String.t(), String.t()) :: binary()

Produce the canonical byte representation for signing.

Fields are serialized as compact JSON with lexicographic key order, no whitespace, excluding signature and reason. This matches the Rust sigil-protocol crate's canonical_bytes implementation.

generate_nonce()

@spec generate_nonce() :: String.t()

Generate a 16-byte cryptographically random nonce as hex.

generate_timestamp()

@spec generate_timestamp() :: String.t()

Generate an ISO 8601 timestamp with millisecond precision.

sign(identity, verdict, opts)

@spec sign(String.t(), verdict(), keyword()) :: t()

Sign an envelope with the given identity and verdict.

Options

  • :signer — module implementing SigilGuard.Signer (required)
  • :reason — optional human-readable reason string
  • :timestamp — override timestamp (for testing)
  • :nonce — override nonce hex (for testing)

Returns a map suitable for embedding as the _sigil field in MCP JSON-RPC params.

verify(envelope, public_key_b64u)

@spec verify(t(), String.t()) :: :ok | {:error, term()}

Verify an envelope's signature against a base64url-encoded Ed25519 public key.

Returns :ok if the signature is valid, or {:error, reason} otherwise. Never raises on malformed input — envelopes arrive over the wire and must be treated as adversarial.

Error reasons

  • :invalid_envelope — envelope is not a map, or the key is not a string
  • :missing_field — a required field (identity, verdict, timestamp, nonce, signature) is absent or not a string
  • :invalid_verdict — verdict is not "Allowed", "Blocked", or "Scanned"
  • :invalid_base64 — the public key or signature is not valid base64url
  • :invalid_key — the public key does not decode to 32 bytes
  • :invalid_signature — the signature has the wrong size or does not verify

Verification is stateless, matching the sigil-protocol crate: replay protection (tracking seen nonces, enforcing timestamp freshness) is the caller's responsibility.