identity

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

der_gate.go — strict-DER + low-S parser for secp256k1 ECDSA signatures.

Mirrors reef-core's `ReefCore.PublishedPods.DerGate` byte for byte against the shared P1.4 conformance vectors at `testdata/der_gate/vectors.json`. Both sides MUST report the same outcome on the same bytes — wire-format identity is the load-bearing property of P1.4.

Enforces the four PRD rules:

  1. Outer SEQUENCE tag (0x30) with minimal definite-length encoding (short form for ≤127 body bytes; one-byte long-form 0x81 only when body ≥128 bytes). All 0x82+ long-forms reject — a secp256k1 ECDSA SEQUENCE never exceeds 255 body bytes.
  2. Two INTEGERs (r, s) minimally encoded: no superfluous leading 0x00 (a leading 0x00 is permitted only when the next byte has its high bit set, to keep the signed INTEGER positive).
  3. No trailing bytes after the outer SEQUENCE.
  4. s ≤ n/2 where n is the secp256k1 curve order. konareef rejects high-S — never normalises — so the wire form a third party observed is the same form the verifier checked.

Surface:

  • ErrDerGateReject — sentinel returned by every rule violation.
  • ParseStrict(der) — (r, s, err); r, s are positive *big.Int on accept, nil on reject.

Package identity — env initializer for feature flags.

LoadEnv reads process environment variables and applies them to package-level identity flags. It MUST be called from main() early, before any signing or verification operation, so that integration tests can cover the env-var path by calling LoadEnv() directly without needing to invoke the application binary.

Design note: the initializer lives in the identity package (not in main) so that go test ./internal/identity/... exercises it. A bare os.Getenv block in main() is untestable from package-level tests, which was the Hermes round-3 B2 blocker.

Package identity implements the konareef publisher signing identity: a secp256k1 keypair plus a publisher handle, persisted as `~/.konareef/identity.json` (mode 0600). It is the local credential `konareef pod publish` signs manifests with and the only thing that proves a re-publish of `<handle>/<pod>` came from the same publisher.

The on-disk format is the publisher-signing-design-v0 §D3 layout:

{ "version": "konareef-identity/v1",
  "scheme":  "secp256k1-ecdsa-der",
  "handle":  "alice",
  "public_key_hex":  "02…",  // 33-byte compressed point, hex
  "private_key_hex": "8c…",  // 32-byte scalar, hex
  "created_at": "…Z" }

Index

Constants

View Source
const BackupVersion = "konareef-identity-backup/v1"

BackupVersion is the on-disk schema tag for v1 backup envelopes.

View Source
const Scheme = "secp256k1-ecdsa-der"

Scheme names the signature algorithm bound to this identity. Today every v1 identity is secp256k1 ECDSA with DER-encoded signatures — the future BRC-100 wallet-backed identity (design §D3) reuses the same scheme string and only swaps where the private key lives.

View Source
const Version = "konareef-identity/v1"

Version is the on-disk schema tag for the v1 identity file. Bumping this string is a breaking format change; new keys are introduced as optional JSON fields under the same tag until a v2 is required.

Variables

View Source
var ErrDerGateReject = errors.New("der_gate_reject")

ErrDerGateReject is returned for any signature that fails strict-DER or low-S. Callers MUST surface this verbatim — no normalisation, no fallback, no lenient retry. The reef-core Elixir side reports the structurally-identical `{:error, :der_gate_reject}` on the same bytes.

View Source
var StrictDerGateEnabled bool

StrictDerGateEnabled gates the strict-DER + low-S enforcement in Verify and VerifyRotationSignature (P1.4). When false (the default), both functions accept any signature that dcrec/secp256k1 can parse and verify — matching pre-P1.4 behaviour. When true, an additional byte-level DER check plus low-S assertion is applied before the ECDSA verification, and any non-conforming signature returns ERR_DER_GATE_REJECT.

The flag is set by LoadEnv() from KONAREEF_STRICT_DER_GATE=true or directly in tests. It MUST NOT be set in init() — test binaries that import this package should not auto-enable the gate without an explicit call or env-var.

Functions

func Canonical

func Canonical(att RotationAttestation) []byte

Canonical returns the byte sequence that is signed and verified.

Encoding rules (a minimal JCS slice for this fixed six-field shape — RFC 8785 compatible):

  • Keys sorted ascending bytewise.
  • No whitespace anywhere outside string values.
  • `reason` omitted entirely when empty (not emitted as `"reason":""` or `"reason":null`).
  • String values escaped via encoding/json — the same rules Elixir's Jason library uses, so the byte form is identical across the two signers.

func ClassifyStrictError

func ClassifyStrictError(der []byte) string

ClassifyStrictError returns a granular reason string for a DER byte slice that is known to have failed ParseStrict. It attempts a permissive integer extraction to distinguish the three normative failure reasons:

  • "high_s" — strict-DER structure OK but s > n/2.
  • "rs_out_of_range" — r or s is zero, negative, or ≥ n (structural parse succeeded but range violated).
  • "strict_der_invalid" — DER structure itself is malformed.

This function is used by Check 4 (PRD 1 §4 / PRD 3 §11.2) to emit granular divergence reasons without requiring ParseStrict to grow a richer error type. Called only on the error path.

func LoadEnv

func LoadEnv()

LoadEnv reads KONAREEF_STRICT_DER_GATE and enables StrictDerGateEnabled when the value is exactly "true". Any other value (unset, "1", "yes", etc.) leaves the flag at its current value, which defaults to false.

Idempotent: calling LoadEnv multiple times has the same effect as calling it once — the flag is only ever set, never cleared.

func ParseStrict

func ParseStrict(der []byte) (*big.Int, *big.Int, error)

ParseStrict validates the DER-encoded signature against the P1.4 rules and returns r, s as positive *big.Int on success. Any rule violation yields (nil, nil, ErrDerGateReject).

The two-return-value-plus-error shape matches the rest of the `identity` package; callers compose it with the existing ECDSA verify primitives without translation.

func Path

func Path(home string) string

Path returns the canonical identity-file path under home: `<home>/.konareef/identity.json`. The CLI passes os.UserHomeDir(); tests pass t.TempDir().

func Verify

func Verify(pubKeyHex string, msg, sig []byte) (bool, error)

Verify reports whether sig is a valid secp256k1 ECDSA signature of SHA-256(msg) under the 33-byte compressed public key encoded as pubKeyHex.

The (bool, error) split is deliberate:

  • (true, nil) — signature checks out.
  • (false, nil) — inputs parsed fine; the signature is wrong.
  • (false, non-nil) — inputs themselves are malformed (bad hex, unparseable DER, non-secp256k1 pubkey). Callers that only care "is this signature good?" can treat both false cases as failure.

This is the verification step `pod install` runs locally before accepting a manifest, and that reef-core re-runs server-side as authentication for `POST /api/pods` (no separate auth header is needed — the signature *is* the credential).

func VerifyDigest

func VerifyDigest(pubKeyHex string, digest, sig []byte) (bool, error)

VerifyDigest verifies a DER ECDSA signature sig directly over the raw 32-byte digest (NOT over a preimage) under the compressed secp256k1 public key pubKeyHex. Used by PRD 3 §11.2 Check 4, where the signed message IS h_manifest (already a SHA-256 digest), so re-hashing (as Verify does at line 84) would produce the wrong result.

The digest argument MUST be exactly 32 bytes; any other length is rejected immediately to prevent accidental preimage misuse.

When StrictDerGateEnabled, the same strict-DER + low-S gate as Verify is applied before the ECDSA check (via ParseStrict in der_gate.go). The (bool, error) semantics are identical to Verify:

  • (true, nil) — signature checks out.
  • (false, nil) — inputs parsed fine; the signature is wrong.
  • (false, non-nil) — inputs are malformed or the gate rejected.

func VerifyRotationSignature

func VerifyRotationSignature(pubKeyHex string, att RotationAttestation, sig []byte) (bool, error)

VerifyRotationSignature reports whether sig is a valid signature of Canonical(att) under the compressed-secp256k1 public key encoded as pubKeyHex (typically the *old* key in a rotation hop, i.e. att.OldPubkeyHex itself).

The (bool, error) split mirrors identity.Verify:

  • (true, nil) — valid signature.
  • (false, nil) — well-formed inputs; signature is wrong.
  • (false, non-nil) — inputs malformed (bad hex, unparseable DER, non-secp256k1 pubkey).

Types

type Identity

type Identity struct {
	Version       string    `json:"version"`
	Scheme        string    `json:"scheme"`
	Handle        string    `json:"handle"`
	PublicKeyHex  string    `json:"public_key_hex"`
	PrivateKeyHex string    `json:"private_key_hex"`
	CreatedAt     time.Time `json:"created_at"`
}

Identity is a publisher's signing identity. The JSON tags are the stable on-disk schema; renaming a field is a breaking format change.

PrivateKeyHex is the raw 32-byte scalar, hex-encoded. It MUST be stored only in mode-0600 files and MUST NEVER be logged or printed — the CLI's `identity show` command prints Handle and PublicKeyHex only, never PrivateKeyHex.

func Generate

func Generate(handle string) (*Identity, error)

Generate creates a fresh secp256k1 keypair and binds it to handle. The underlying secp256k1.GeneratePrivateKey draws from crypto/rand.

The returned Identity is in-memory only; persist it with Save before any signing operation, otherwise the key is lost when the process exits.

func Import

func Import(path string, passphrase []byte) (*Identity, error)

Import reads an encrypted backup from path, verifies the version + KDF + cipher tags, and decrypts the Identity using passphrase. A wrong passphrase, a malformed envelope, or any post-write tamper surfaces as an Import error — the AES-GCM auth tag does the integrity check; we never return partial / unverified plaintext.

func Load

func Load(home string) (*Identity, error)

Load reads the identity file at Path(home). It returns an error rather than the parsed Identity in three cases:

  • the file does not exist (run `konareef pod identity create` first);
  • the file's permissions are anything other than exactly 0600;
  • the file's contents are not valid Identity JSON.

The permission check is strict: a 0644 file is refused, not honored, because the private key inside it is a credential and a careless chmod is the most likely silent compromise vector.

func (*Identity) Export

func (id *Identity) Export(path string, passphrase []byte) error

Export writes a passphrase-encrypted backup of id to path. The passphrase MUST be non-empty (an empty passphrase is rejected rather than silently downgraded to "no encryption").

The output file is mode 0600 and written atomically (temp file + rename), so a crash mid-write never leaves a partial backup and a successful Export never widens the file's permissions.

func (*Identity) Save

func (id *Identity) Save(home string) error

Save writes id to <home>/.konareef/identity.json with mode 0600. The parent directory is created (mode 0700) on first save.

The write is atomic: id is serialized to a sibling temp file (with the target permission bits already set), then renamed onto the final path. A crash mid-write therefore never leaves a half-written identity, and a successful Save never widens the file's permissions.

func (*Identity) Sign

func (id *Identity) Sign(msg []byte) ([]byte, error)

Sign returns the DER-encoded secp256k1 ECDSA signature of SHA-256(msg) under id's private key. The output is the byte form that flows through `pod publish`, the `signature` column of the reef-core `pods` table, and the spawn-time verifier — bit-exact across every consumer.

Errors here mean the identity itself is malformed (private key is not 32 hex-encoded bytes); a fresh Generate() never produces one.

func (*Identity) SignRotation

func (id *Identity) SignRotation(att RotationAttestation) ([]byte, error)

SignRotation produces the DER-encoded secp256k1 ECDSA signature of SHA-256(Canonical(att)) under id's private key. The returned bytes are what the producer POSTs to `/api/publishers/:handle/rotate` as `signature_by_old_key` (base64-encoded on the wire).

The wrapper exists so the rotation flow doesn't need to assemble canonical bytes itself; callers pass the typed attestation and receive a signature that is by construction over the right input.

type RotationAttestation

type RotationAttestation struct {
	// Kind is the format discriminator. Always
	// "konareef-key-rotation/v1" for v1 attestations; the verifier
	// rejects anything else.
	Kind string

	// Handle is the publisher handle this rotation applies to.
	Handle string

	// OldPubkeyHex / NewPubkeyHex are the compressed-secp256k1 keys
	// as 66-char lowercase hex (33 bytes, leading 02 or 03 prefix).
	// OldPubkeyHex MUST be the key currently bound to Handle in
	// reef-core; the verifier compares against the registered identity.
	OldPubkeyHex string
	NewPubkeyHex string

	// RotatedAt is the publisher-stated time of rotation as a
	// microsecond-precision UTC ISO-8601 string
	// (e.g. "2026-05-19T11:22:00.000000Z"). Inside the signed bytes
	// — server-side overrides would invalidate the signature.
	RotatedAt string

	// Reason is optional free-text. Empty string is treated as
	// absent: omitted from the canonical bytes entirely (not
	// serialized as "" or null) so the byte form matches reef-core's
	// `Enum.reject(is_nil/1)`-style filter.
	Reason string
}

RotationAttestation is the typed view of a `konareef-key-rotation/v1` document.

All fields except Reason are required; Reason is a free-text human note (e.g. "scheduled key rotation", "hardware wallet migration") and is omitted from the canonical bytes when empty.

type RotationResponse

type RotationResponse struct {
	RotationID   string `json:"rotation_id"`
	RegisteredAt string `json:"registered_at"`
}

RotationResponse is the parsed 201 body from `POST /api/publishers/:handle/rotate`.

func SubmitRotation

func SubmitRotation(serverURL, handle string, att RotationAttestation, sig []byte) (*RotationResponse, error)

SubmitRotation POSTs a signed key-rotation attestation to reef-core.

The `attestation` object in the request body uses the same field names as the canonical bytes Canonical/1 emits — so the server can reconstruct those bytes deterministically and re-verify `signature_by_old_key` under the registered publisher pubkey. `reason` is dropped from the request when empty; this matches Canonical's omit-when-nil rule and prevents a `"reason":""` field from sneaking into the JSON the server hashes.

On 201 returns the decoded RotationResponse. On any non-2xx the server's `{"error": "<atom>"}` body is surfaced verbatim in the returned error so the CLI can show it without re-parsing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL