Skip to content

feat: license public-key registry (keys.py) — needs maintainer to fill in pubkey hex#4

Merged
typelicious merged 2 commits into
mainfrom
feat/license-public-keys
Jun 3, 2026
Merged

feat: license public-key registry (keys.py) — needs maintainer to fill in pubkey hex#4
typelicious merged 2 commits into
mainfrom
feat/license-public-keys

Conversation

@typelicious

Copy link
Copy Markdown
Contributor

Summary

Adds capacium_models.keys module that holds Ed25519 PUBLIC keys for license-token verification. Matching PRIVATE keys live ONLY in envctl on prod (capacium-exchange).

This is the DECISION-001 follow-up: license signing keypair is DISTINCT from capability-signing keypair; lic- namespace enforced.

🟡 Action required from maintainer (Andre)

The PR ships with a placeholder in src/capacium_models/keys.py:46:

"lic-2026-q2-01": "REPLACE_WITH_64_CHAR_HEX_PUBLIC_KEY",

Replace that string with the actual 64-char hex Ed25519 public key you generated locally. Then commit (web-edit works) and CI goes green.

The matching kid + private key are already in envctl on capacium-prod under:

  • CAPACIUM_LICENSE_SIGNING_KID = lic-2026-q2-01
  • CAPACIUM_LICENSE_SIGNING_KEY = (the 64-hex private)

Verify the public you paste here corresponds to that exact private — derive via:

from nacl.signing import SigningKey
sk = SigningKey(bytes.fromhex("<the-private-hex-you-have-in-keychain>"))
print(sk.verify_key.encode().hex())

(Or from the original generation output you have noted down.)

Why CI is expected to fail until the paste

tests/test_keys.py::TestGetPublicKey::test_returns_64_char_hex fails because the placeholder isn't 64 hex chars. This is by design — it's the gate preventing accidental shipping of a placeholder.

After the paste:

  • 10/10 keys-tests pass
  • 55/55 existing model-tests still pass
  • CI green
  • Ready to merge

What changed

File Change
src/capacium_models/keys.py (NEW) Registry + get_license_public_key() + known_kids() + UnknownLicenseKid exception
src/capacium_models/__init__.py Export the 4 new symbols
tests/test_keys.py (NEW) 10 tests (placeholder gate, lookup, rotation invariants)

Additive only — no breaking changes to v0.4.0 consumers.

After merge

  • Tag v0.4.1 on main
  • GitHub Release with CHANGELOG entry (will write in follow-up commit before tag)
  • capacium-exchange LicenseKeyManager consumers can then from capacium_models import get_license_public_key

Test plan

  • Maintainer replaces placeholder with real pubkey hex
  • CI green (10 keys tests pass, 55 existing tests still pass)
  • Smoke: pip install "capacium-models @ git+...@feat/license-public-keys" + python -c "from capacium_models import get_license_public_key; print(get_license_public_key('lic-2026-q2-01'))" returns the hex

André Lange added 2 commits June 3, 2026 16:38
Adds capacium_models.keys module that holds Ed25519 PUBLIC keys for
license-token verification, indexed by `kid`. Matching PRIVATE keys
live ONLY in envctl on prod (capacium-exchange).

DECISION-001 reference: license signing keypair is DISTINCT from
capability-signing keypair; license kid namespace MUST start with `lic-`.

Module contents:
- LICENSE_PUBLIC_KEYS: dict[kid -> 64-hex] registry
- get_license_public_key(kid) -> str: lookup with raise-on-unknown
- known_kids() -> list[str]: sorted newest-first
- UnknownLicenseKid: dedicated exception (subclass of KeyError) for
  consumers handling kid lookup failures cleanly

CI-gate by design: the first registry entry (`lic-2026-q2-01`) currently
holds the literal placeholder "REPLACE_WITH_64_CHAR_HEX_PUBLIC_KEY". The
test_returns_64_char_hex test fails until this placeholder is replaced
with the real Ed25519 public key hex (generated on maintainer workstation,
private uploaded to envctl on capacium-prod).

This is intentional: it prevents accidentally shipping a placeholder.
Reviewer must verify the kid matches the value of
CAPACIUM_LICENSE_SIGNING_KID on prod, and that the pubkey matches the
private set as CAPACIUM_LICENSE_SIGNING_KEY.

Rotation lifecycle for a kid:
- Active:     issues new tokens; PRIVATE held in envctl
- Retired:    no longer issues new tokens; old tokens still validate
- Removed:    entry deleted; old tokens fail validation

To add a future kid: append to LICENSE_PUBLIC_KEYS, bump minor version,
ship new capacium-models release. NEVER remove an entry whose tokens may
still be in circulation.

Tests added (tests/test_keys.py):
- 2× registry shape (kid present, namespace lic-)
- 3× get behavior (returns 64-hex, raises on unknown, error msg lists known kids)
- 2× known_kids (returns all, sorted reverse)
- 3× defensive placeholder rejection (placeholder, wrong length, non-hex)

Verified locally: 9 pass, 1 expected-fail on placeholder. test_models.py
still 55/55 pass. Existing capacium-models v0.4.0 consumers unaffected
(additive only; no breaking changes).

Next step (out of scope for this PR): after Andre commits the real
pubkey hex, this branch goes green → merge → tag v0.4.1 → GitHub Release.
capacium-exchange consumers (FIX-A-002 LicenseKeyManager) can then verify
license tokens via `from capacium_models import get_license_public_key`.
…on bump

Replaces the placeholder in src/capacium_models/keys.py:47 with the
actual 64-char Ed25519 public key hex
(8e5e33e1...ef84e978) generated on maintainer workstation. Matching
private was uploaded to envctl on capacium-prod under
CAPACIUM_LICENSE_SIGNING_KEY with kid=lic-2026-q2-01.

CI-gate test test_returns_64_char_hex now passes (was the intentional
gate preventing placeholder shipping). 10/10 keys tests + 64 other
existing tests all pass locally — 74/74 green.

Includes CHANGELOG.md v0.4.1 entry documenting the new keys module and
the additive-only contract (no v0.4.0 consumer impact). pyproject.toml
version bumped 0.4.0 → 0.4.1.

After CI green + merge: tag v0.4.1 + GitHub Release. Consumer code
(capacium-exchange LicenseKeyManager FIX-A-002) can then verify license
tokens via:

    from capacium_models import get_license_public_key
    pubkey = get_license_public_key("lic-2026-q2-01")

@dev-bot-capacium dev-bot-capacium left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review for PR #4 (license public-key registry → v0.4.1 release).

Files reviewed locally (diff vs main):

  • src/capacium_models/keys.py — new module with LICENSE_PUBLIC_KEYS registry, get_license_public_key(), known_kids(), UnknownLicenseKid exception. First entry lic-2026-q2-01 populated with a 64-char hex Ed25519 public key (verified shape: 64 chars, valid hex, last 8 chars ef84e978).
  • src/capacium_models/__init__.py — exports the 4 new symbols.
  • tests/test_keys.py — 10 tests covering: registry shape, kid namespace enforcement (lic- prefix per DECISION-001), get_license_public_key happy/error paths, known_kids ordering, defensive placeholder rejection (3 cases).
  • CHANGELOG.md — v0.4.1 entry documenting the additive-only release.
  • pyproject.toml — version 0.4.0 → 0.4.1.

Local verification:

  • PYTHONPATH=src pytest tests/ → 74 passed (10 keys tests + 55 model tests + 9 search tests)
  • Pubkey shape: int(pubkey, 16) parses successfully
  • Pubkey corresponds to kid=lic-2026-q2-01 matching CAPACIUM_LICENSE_SIGNING_KID in envctl on capacium-prod
  • CI-gate test test_returns_64_char_hex previously FAILED on the placeholder; now PASSES after maintainer paste — gate worked as designed.

Remote CI: 3/3 SUCCESS (test 3.10/3.11/3.12).

Note: Approval via dev-bot-capacium per established attended releasechain pattern. Additive-only release — no breaking changes for v0.4.0 consumers. After merge: tag v0.4.1 + GitHub Release via dev-bot; capacium-exchange LicenseKeyManager (FIX-A-002) can then import from capacium_models import get_license_public_key for local token verification.

Approved.

@typelicious typelicious merged commit 3afcd23 into main Jun 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants