Lace / spec

Lace-009 ยท Signatures

Tags: record, crypto

Purpose

Seal records need portable authorship proof that survives transport boundaries: a verifier signs a specific Plex-record digest, and any implementation can check the signature from record bytes alone. This spec defines verifier, signing-secret, and signature rules used by Seal records in the current .H3 suite. It signs a 32-byte BLAKE3 digest with secp256k1 Schnorr arithmetic and encodes public protocol values with B64A.

This document is for record and crypto implementors. After reading it, an implementor should be able to generate verifier text, derive signing material, produce signatures, and verify Seal-record signatures consistently across languages.

Defines

Key generation, signing-secret derivation, signing, and verification.

Text forms identify verifier and signing values

A verifier is public. A signing secret is private local material used to derive a signing scalar.

Parameters define the current signature system

BLAKE3 derive-key context strings:

Context Use
lace-๐Ÿ–ง/adhoc-key signing-secret derivation
lace-๐Ÿ–ง/aux signing auxiliary randomness tag
lace-๐Ÿ–ง/nonce nonce tag
lace-๐Ÿ–ง/challenge challenge tag

derive_key(context, input) means the first 32 bytes of BLAKE3 derive-key output for that context and input. Context strings are encoded as the exact UTF-8 bytes of the displayed text.

Signer generation produces an even-y verifier

Implementations MUST generate random signing scalars as follows:

  1. Choose random 32-byte d0 with 0 < d0 < n.
  2. Compute P = d0*G.
  3. If P.y is odd, set d = n - d0; otherwise set d = d0.
  4. The signing scalar is d and verifier bytes are bytes((d*G).x).

The even-y convention makes one verifier text correspond to one public point.

Signing-secret derivation maps secret text to a scalar

To derive signing material from a signing secret payload, implementations MUST:

  1. Reject payloads whose decoded length is not exactly 32 bytes.
  2. Use BLAKE3 XOF derive mode with context lace-๐Ÿ–ง/adhoc-key.
  3. Read 32-byte candidates until one satisfies 0 < d0 < n.
  4. Apply the same even-y normalization used by signer generation.

Signing binds a digest to a verifier

Inputs are signing scalar d, verifier x-coordinate Px, message digest msg32, and fresh nonzero 32-byte auxRand32.

Implementations MUST sign as follows:

  1. t = derive_key("lace-๐Ÿ–ง/aux", auxRand32).
  2. mask = t xor bytes(d).
  3. k0 = derive_key("lace-๐Ÿ–ง/nonce", mask || Px || msg32) mod n; reject zero.
  4. Compute R = k0*G; use k = n-k0 when R.y is odd, else k = k0.
  5. e = derive_key("lace-๐Ÿ–ง/challenge", bytes(R.x) || Px || msg32) mod n.
  6. s = (k + e*d) mod n.
  7. Signature bytes are bytes(R.x) || bytes(s).

auxRand32 MUST be unique per signature and MUST NOT be all zero.

Verification accepts only the matching signature

Inputs are verifier x-coordinate Px, signature r||s, and message digest msg32.

Implementations MUST verify as follows:

  1. Reject Px >= p, r >= p, or s >= n.
  2. Recover point P from Px using the even-y root; reject Px values that do not identify a secp256k1 point.
  3. Compute e = derive_key("lace-๐Ÿ–ง/challenge", bytes(r) || Px || msg32) mod n.
  4. Compute R' = s*G - e*P.
  5. Reject infinity or odd R'.y.
  6. Accept only when R'.x == r.

Implementations MUST use constant-time scalar and point operations.

Rejection examples

Reject examples include an empty signing secret, all-zero auxiliary value, Px >= p, Px with no curve point, r >= p, s >= n, odd recovered R', and any signature whose recomputed R'.x differs from r.