|
| 1 | +# Design: Publisher Identity and Module Trust Chain |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +marketplace-01 established Ed25519 publisher signing infrastructure. marketplace-02 adds multi-registry support and a trust level per registry. What is missing is a publisher identity layer: who is a publisher, what tier are they, and how does the CLI verify their identity at install time without a live accounts database. |
| 6 | + |
| 7 | +**Current State:** |
| 8 | + |
| 9 | +- `crypto_validator.py`: `validate_module()` has an `official` tier branch; publisher = `nold-ai` (string comparison) |
| 10 | +- No structured publisher record; no publisher key lookup from a signed index |
| 11 | +- No `community` or `verified` tier handling |
| 12 | +- `custom_registries.py`: registry trust level exists (from marketplace-02) but not linked to publisher tier |
| 13 | + |
| 14 | +**Constraints:** |
| 15 | + |
| 16 | +- NOLD AI root key must be bundled at build time (offline-first; no runtime CA lookup) |
| 17 | +- Trust index fetch must cache gracefully (7-day TTL fallback for CDN failures) |
| 18 | +- Backward compatible: existing `official` tier install path must not be touched |
| 19 | +- `specfact-cli-modules/` repo is separate — CLI reads from its deployed trust index URL, not from local files |
| 20 | +- Must not introduce a server-side accounts database in Phase 1 |
| 21 | + |
| 22 | +## Goals / Non-Goals |
| 23 | + |
| 24 | +**Goals:** |
| 25 | + |
| 26 | +- Structured publisher identity with three tiers (official, verified, community) |
| 27 | +- CLI verifies publisher attestation + registry endorsement at install time |
| 28 | +- Trust tier displayed in search/info output |
| 29 | +- Trust override flags with audit logging (offline-safe) |
| 30 | + |
| 31 | +**Non-Goals:** |
| 32 | + |
| 33 | +- Publisher self-registration UI (specfact.io-backend-phase1 — separate change, separate repo) |
| 34 | +- Registry federation / external registry certificates (marketplace-05) |
| 35 | +- Revocation infrastructure (marketplace-04) |
| 36 | +- Paid module gating (marketplace-06, requires legal entity) |
| 37 | + |
| 38 | +## Architecture |
| 39 | + |
| 40 | +### Trust Layer (`src/specfact_cli/trust/`) |
| 41 | + |
| 42 | +Three modules with clear separation of concerns: |
| 43 | + |
| 44 | +```text |
| 45 | +trust/ |
| 46 | + key_store.py — NOLD AI root public key (Ed25519), bundled at build time |
| 47 | + publisher_registry.py — fetch + cache publishers/index.json; verify NOLD AI signature |
| 48 | + resolver.py — tier resolution order; install gate logic; audit logging |
| 49 | +``` |
| 50 | + |
| 51 | +`resolver.py` calls `crypto_validator.py` for low-level Ed25519 operations — does NOT duplicate crypto. |
| 52 | + |
| 53 | +### Trust Resolution Sequence |
| 54 | + |
| 55 | +```text |
| 56 | +specfact module install @mycompany/specfact-jira-sync |
| 57 | + │ |
| 58 | + ├─ trust/publisher_registry.py: fetch publishers/index.json (cache 7d) |
| 59 | + ├─ key_store.py: verify NOLD AI signature over publishers/index.json |
| 60 | + ├─ Resolve publisher_id from module's module-package.yaml publisher block |
| 61 | + ├─ Fetch publisher record from index (publisher_id → tier + public_key) |
| 62 | + ├─ crypto_validator.py: verify publisher Ed25519 signature on bundle |
| 63 | + ├─ trust/resolver.py: resolve effective tier (publisher tier ∩ registry tier) |
| 64 | + │ |
| 65 | + ├─ official → install without prompt |
| 66 | + ├─ verified → install without prompt |
| 67 | + ├─ community → prompt unless --trust-community (log to audit) |
| 68 | + └─ unregistered → block unless --trust-unregistered (log to audit) |
| 69 | +``` |
| 70 | + |
| 71 | +### crypto_validator.py Extension Strategy |
| 72 | + |
| 73 | +The existing `official` branch is preserved unchanged. New branches added alongside: |
| 74 | + |
| 75 | +```python |
| 76 | +# BEFORE (migration-02): single official check |
| 77 | +if publisher == "nold-ai": |
| 78 | + validate_official(bundle, signature) |
| 79 | + |
| 80 | +# AFTER (marketplace-03): tier dispatch, official path unchanged |
| 81 | +match tier: |
| 82 | + case "official": |
| 83 | + validate_official(bundle, signature) # unchanged |
| 84 | + case "verified": |
| 85 | + validate_verified(bundle, publisher_record, signature) # new |
| 86 | + case "community": |
| 87 | + validate_community(bundle, publisher_record, signature) # new |
| 88 | + case _: |
| 89 | + raise UnregisteredPublisherError(publisher) |
| 90 | +``` |
| 91 | + |
| 92 | +### Publisher Record Format |
| 93 | + |
| 94 | +`module-package.yaml` structured `publisher:` block (marketplace-03 format): |
| 95 | + |
| 96 | +```yaml |
| 97 | +publisher: |
| 98 | + publisher_id: pub_abc123 |
| 99 | + handle: mycompany |
| 100 | + tier: verified |
| 101 | + public_key_fingerprint: sha256:abcdef... |
| 102 | + publisher_signature: "<Ed25519 sig over name+version+sha256>" |
| 103 | +``` |
| 104 | +
|
| 105 | +CLI accepts both the legacy `publisher: nold-ai` string (migration-02) and the structured block during the transition window. Format detected at parse time. |
| 106 | + |
| 107 | +### Registry Endorsement Countersignature |
| 108 | + |
| 109 | +`registry/index.json` entry gains a `registry_signature` field (NOLD AI countersig over `name+version+publisher_id+checksum_sha256`). This is **distinct** from the publisher's `signature_ed25519`. Both coexist: |
| 110 | + |
| 111 | +```json |
| 112 | +{ |
| 113 | + "name": "specfact-jira-sync", |
| 114 | + "version": "1.0.0", |
| 115 | + "publisher_id": "pub_abc123", |
| 116 | + "tier": "verified", |
| 117 | + "checksum_sha256": "abcdef...", |
| 118 | + "signature_ed25519": "<publisher sig — from migration-02>", |
| 119 | + "registry_signature": "<NOLD AI countersig — new in marketplace-03>" |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +`scripts/publish-module.py` adds the countersig step after existing publisher signing. |
| 124 | + |
| 125 | +## Decisions |
| 126 | + |
| 127 | +### Decision 1: Trust Index Caching Strategy |
| 128 | + |
| 129 | +**Options:** |
| 130 | + |
| 131 | +- A: Always fetch from CDN, fail hard if unavailable |
| 132 | +- B: Cache with TTL, serve stale with warning |
| 133 | +- C: Cache only, no online refresh |
| 134 | + |
| 135 | +Choice: B (7-day TTL cache, serve stale with staleness warning) |
| 136 | + |
| 137 | +**Rationale:** |
| 138 | + |
| 139 | +- Offline-first constraint: CLI must work without internet during runs |
| 140 | +- 7-day staleness acceptable for publisher index (revocation handled by marketplace-04) |
| 141 | +- Warning informs user when cache is stale without blocking install |
| 142 | + |
| 143 | +### Decision 2: Root Key Bundling |
| 144 | + |
| 145 | +**Options:** |
| 146 | + |
| 147 | +- A: Fetch root key from well-known URL at runtime |
| 148 | +- B: Bundle root key in CLI package at build time |
| 149 | +- C: Store in user config (~/.specfact/) |
| 150 | + |
| 151 | +Choice: B (bundled at build time) |
| 152 | + |
| 153 | +**Rationale:** |
| 154 | + |
| 155 | +- Offline-first: no network required for key verification |
| 156 | +- Tamper-evident: key changes require a CLI release (auditable) |
| 157 | +- Acceptable key rotation cadence: quarterly, requires CLI update |
| 158 | + |
| 159 | +### Decision 3: Audit Log Location |
| 160 | + |
| 161 | +**Choice**: `~/.specfact/module-audit.log` — append-only, human-readable, line-per-install |
| 162 | + |
| 163 | +Captures: timestamp, module, tier, action (installed/blocked/prompted/accepted), flag used. |
| 164 | + |
| 165 | +## Sequence Diagrams |
| 166 | + |
| 167 | +### Install with Publisher Attestation |
| 168 | + |
| 169 | +```text |
| 170 | +User CLI trust/ crypto_validator CDN |
| 171 | + │ │ │ │ │ |
| 172 | + │ install @org/mod │ │ │ │ |
| 173 | + │──────────────────>│ │ │ │ |
| 174 | + │ │ fetch publishers/ │ │ │ |
| 175 | + │ │─────────────────> │ │ │ |
| 176 | + │ │ │ GET /trust/publishers/index.json │ |
| 177 | + │ │ │──────────────────────────────────>│ |
| 178 | + │ │ │<──────────────────────────────────│ |
| 179 | + │ │ │ verify NOLD AI sig │ |
| 180 | + │ │ │────────────────────> │ |
| 181 | + │ │ │<──────────────────── │ |
| 182 | + │ │ resolve publisher │ │ │ |
| 183 | + │ │<────────────────- │ │ │ |
| 184 | + │ │ verify bundle sig │ │ │ |
| 185 | + │ │────────────────────────────────────────> │ |
| 186 | + │ │<──────────────────────────────────────── │ |
| 187 | + │ │ resolve tier │ │ │ |
| 188 | + │ │─────────────────> │ │ │ |
| 189 | + │ │ tier=verified │ │ │ |
| 190 | + │ │<─────────────────│ │ │ |
| 191 | + │ install proceeds │ │ │ │ |
| 192 | + │<──────────────────│ │ │ │ |
| 193 | +``` |
0 commit comments