multimodel-dev-os 3.2.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.ai/policies/registry-policy.yaml +29 -1
  2. package/.ai/registries/trusted-keys.yaml +12 -0
  3. package/.ai/schema/registry-manifest.schema.json +31 -2
  4. package/.ai/schema/registry-policy.schema.json +37 -1
  5. package/.ai/schema/trusted-keys.schema.json +69 -0
  6. package/AGENTS.md +22 -26
  7. package/MEMORY.md +34 -11
  8. package/README.md +1 -1
  9. package/RUNBOOK.md +28 -36
  10. package/TASKS.md +15 -5
  11. package/bin/multimodel-dev-os.js +1366 -548
  12. package/docs/.vitepress/config.js +3 -1
  13. package/docs/architecture.md +3 -1
  14. package/docs/index.md +5 -5
  15. package/docs/npm-publishing.md +5 -5
  16. package/docs/package-safety.md +3 -2
  17. package/docs/public/llms-full.txt +5 -1
  18. package/docs/public/llms.txt +6 -1
  19. package/docs/public/sitemap.xml +10 -0
  20. package/docs/registry-policy.md +29 -1
  21. package/docs/registry-security.md +73 -6
  22. package/docs/registry-signing.md +70 -0
  23. package/docs/registry-sync.md +5 -2
  24. package/docs/registry-trust-store.md +66 -0
  25. package/docs/release-policy.md +1 -1
  26. package/docs/security-threat-model.md +96 -0
  27. package/docs/testing.md +15 -2
  28. package/docs/trusted-registries.md +1 -1
  29. package/docs/v3-roadmap.md +11 -6
  30. package/docs/v3.5.0-readiness.md +46 -0
  31. package/package.json +1 -1
  32. package/scripts/install.ps1 +1 -1
  33. package/scripts/install.sh +1 -1
  34. package/scripts/verify.js +206 -9
  35. package/src/cli/help.js +1 -1
  36. package/src/cli/main.js +626 -81
  37. package/src/core/policy.js +9 -1
  38. package/src/registry/provenance.js +114 -0
  39. package/src/registry/signing.js +392 -0
  40. package/src/registry/trust-store.js +41 -0
  41. package/src/registry/verdict.js +51 -0
  42. package/tests/fixtures/signed-registries/README.md +4 -0
  43. package/tests/fixtures/signed-registries/revoked-key/catalog.yaml +8 -0
  44. package/tests/fixtures/signed-registries/revoked-key/expected-verdict.json +7 -0
  45. package/tests/fixtures/signed-registries/revoked-key/registry-manifest.yaml +14 -0
  46. package/tests/fixtures/signed-registries/tampered-manifest/catalog.yaml +8 -0
  47. package/tests/fixtures/signed-registries/tampered-manifest/expected-verdict.json +7 -0
  48. package/tests/fixtures/signed-registries/tampered-manifest/registry-manifest.yaml +14 -0
  49. package/tests/fixtures/signed-registries/trusted-keys.yaml +23 -0
  50. package/tests/fixtures/signed-registries/unsigned-remote-required/catalog.yaml +8 -0
  51. package/tests/fixtures/signed-registries/unsigned-remote-required/expected-verdict.json +7 -0
  52. package/tests/fixtures/signed-registries/unsigned-remote-required/registry-manifest.yaml +9 -0
  53. package/tests/fixtures/signed-registries/unsupported-algorithm/catalog.yaml +8 -0
  54. package/tests/fixtures/signed-registries/unsupported-algorithm/expected-verdict.json +7 -0
  55. package/tests/fixtures/signed-registries/unsupported-algorithm/registry-manifest.yaml +14 -0
  56. package/tests/fixtures/signed-registries/valid-signed-registry/catalog.yaml +8 -0
  57. package/tests/fixtures/signed-registries/valid-signed-registry/expected-verdict.json +7 -0
  58. package/tests/fixtures/signed-registries/valid-signed-registry/registry-manifest.yaml +14 -0
  59. package/tests/fixtures/signed-registries/wrong-key/catalog.yaml +8 -0
  60. package/tests/fixtures/signed-registries/wrong-key/expected-verdict.json +7 -0
  61. package/tests/fixtures/signed-registries/wrong-key/registry-manifest.yaml +14 -0
  62. package/tests/unit/registry-e2e-signature-fixtures.test.js +288 -0
  63. package/tests/unit/registry-policy.test.js +6 -0
  64. package/tests/unit/registry-provenance.test.js +185 -0
  65. package/tests/unit/registry-public-signing.test.js +109 -0
  66. package/tests/unit/registry-signature-policy.test.js +100 -0
  67. package/tests/unit/registry-signing.test.js +193 -0
  68. package/tests/unit/registry-trust-store.test.js +133 -0
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ generateEd25519KeyPair,
4
+ signEd25519Payload,
5
+ verifyEd25519Payload,
6
+ createCanonicalPayload,
7
+ normalizePublicKey
8
+ } from '../../src/registry/signing.js';
9
+
10
+ describe('Registry Public Signing — Ed25519 Key Generation', () => {
11
+ it('generates valid PEM format keys', () => {
12
+ const { publicKey, privateKey } = generateEd25519KeyPair();
13
+ expect(publicKey).toContain('-----BEGIN PUBLIC KEY-----');
14
+ expect(publicKey).toContain('-----END PUBLIC KEY-----');
15
+ expect(privateKey).toContain('-----BEGIN PRIVATE KEY-----');
16
+ expect(privateKey).toContain('-----END PRIVATE KEY-----');
17
+ });
18
+ });
19
+
20
+ describe('Registry Public Signing — Ed25519 Sign/Verify', () => {
21
+ it('signs and verifies a payload successfully', () => {
22
+ const { publicKey, privateKey } = generateEd25519KeyPair();
23
+ const payload = 'test-canonical-payload-data';
24
+ const signature = signEd25519Payload(privateKey, payload);
25
+
26
+ expect(signature).toBeTruthy();
27
+ expect(typeof signature).toBe('string');
28
+ // Base64 signature check
29
+ expect(signature).toMatch(/^[a-zA-Z0-9+/=]+$/);
30
+
31
+ const verified = verifyEd25519Payload(publicKey, payload, signature);
32
+ expect(verified).toBe(true);
33
+ });
34
+
35
+ it('fails verification for a tampered payload', () => {
36
+ const { publicKey, privateKey } = generateEd25519KeyPair();
37
+ const payload = 'original-payload';
38
+ const signature = signEd25519Payload(privateKey, payload);
39
+
40
+ const verified = verifyEd25519Payload(publicKey, 'tampered-payload', signature);
41
+ expect(verified).toBe(false);
42
+ });
43
+
44
+ it('fails verification for an invalid signature', () => {
45
+ const { publicKey } = generateEd25519KeyPair();
46
+ const verified = verifyEd25519Payload(publicKey, 'payload', 'not-a-valid-base64-signature!');
47
+ expect(verified).toBe(false);
48
+ });
49
+
50
+ it('fails verification with a different key', () => {
51
+ const pairA = generateEd25519KeyPair();
52
+ const pairB = generateEd25519KeyPair();
53
+ const payload = 'some-payload';
54
+ const signature = signEd25519Payload(pairA.privateKey, payload);
55
+
56
+ const verified = verifyEd25519Payload(pairB.publicKey, payload, signature);
57
+ expect(verified).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe('Registry Public Signing — Canonical Payload Determinism', () => {
62
+ it('generates identical payload regardless of key insertion order', () => {
63
+ const obj1 = { name: 'registry-a', version: '1.0.0', catalog_hash: 'hash123' };
64
+ const obj2 = { catalog_hash: 'hash123', name: 'registry-a', version: '1.0.0' };
65
+ const fields = ['name', 'version', 'catalog_hash'];
66
+
67
+ const payload1 = createCanonicalPayload(obj1, fields);
68
+ const payload2 = createCanonicalPayload(obj2, fields);
69
+
70
+ expect(payload1).toBe(payload2);
71
+ // Keys sorted alphabetically: catalog_hash, name, version
72
+ const parsed = JSON.parse(payload1);
73
+ const keys = Object.keys(parsed);
74
+ expect(keys).toEqual(['catalog_hash', 'name', 'version']);
75
+ });
76
+
77
+ it('handles nested objects deterministically by sorting their keys', () => {
78
+ const obj1 = {
79
+ name: 'registry-b',
80
+ metadata: { z: 3, a: 1, m: 2 }
81
+ };
82
+ const obj2 = {
83
+ metadata: { a: 1, m: 2, z: 3 },
84
+ name: 'registry-b'
85
+ };
86
+ const fields = ['name', 'metadata'];
87
+
88
+ const payload1 = createCanonicalPayload(obj1, fields);
89
+ const payload2 = createCanonicalPayload(obj2, fields);
90
+
91
+ expect(payload1).toBe(payload2);
92
+ expect(payload1).toContain('"metadata":{"a":1,"m":2,"z":3}');
93
+ });
94
+ });
95
+
96
+ describe('Registry Public Signing — normalizePublicKey', () => {
97
+ it('keeps PEM headers intact if already present', () => {
98
+ const raw = '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=\n-----END PUBLIC KEY-----';
99
+ expect(normalizePublicKey(raw)).toBe(raw);
100
+ });
101
+
102
+ it('wraps a raw base64 string into PEM format', () => {
103
+ const raw = 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=';
104
+ const normalized = normalizePublicKey(raw);
105
+ expect(normalized).toContain('-----BEGIN PUBLIC KEY-----');
106
+ expect(normalized).toContain('-----END PUBLIC KEY-----');
107
+ expect(normalized.replace(/\s+/g, '')).toContain(raw);
108
+ });
109
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { verifySignatureBlock } from '../../src/registry/signing.js';
3
+
4
+ describe('Registry Signature Policy — Unsigned Registries', () => {
5
+ const manifest = {
6
+ registry_name: 'test-registry',
7
+ publisher: 'Test Publisher',
8
+ version: '1.0.0',
9
+ catalog_hash: 'sha256:abc'
10
+ };
11
+
12
+ it('allows unsigned bundled registry by default', () => {
13
+ const policy = {
14
+ allow_unsigned_bundled: true,
15
+ allow_unsigned_local: true,
16
+ allow_unsigned_remote: false,
17
+ require_signature: false
18
+ };
19
+ const source = { name: 'bundled', type: 'local' };
20
+ const res = verifySignatureBlock({ manifest, policy, source });
21
+ expect(res.verified).toBe(true);
22
+ expect(res.status).toBe('unsigned');
23
+ });
24
+
25
+ it('allows unsigned local registry by default', () => {
26
+ const policy = {
27
+ allow_unsigned_bundled: true,
28
+ allow_unsigned_local: true,
29
+ allow_unsigned_remote: false,
30
+ require_signature: false
31
+ };
32
+ const source = { name: 'my-local-registry', type: 'local' };
33
+ const res = verifySignatureBlock({ manifest, policy, source });
34
+ expect(res.verified).toBe(true);
35
+ expect(res.status).toBe('unsigned');
36
+ });
37
+
38
+ it('blocks unsigned remote registry by default if allow_unsigned_remote is false', () => {
39
+ const policy = {
40
+ allow_unsigned_bundled: true,
41
+ allow_unsigned_local: true,
42
+ allow_unsigned_remote: false,
43
+ require_signature: false
44
+ };
45
+ const source = { name: 'official', type: 'remote' };
46
+ const res = verifySignatureBlock({ manifest, policy, source });
47
+ expect(res.verified).toBe(false);
48
+ expect(res.error).toContain('Unsigned remote registries are not allowed');
49
+ });
50
+
51
+ it('allows unsigned remote registry if explicitly permitted by policy', () => {
52
+ const policy = {
53
+ allow_unsigned_bundled: true,
54
+ allow_unsigned_local: true,
55
+ allow_unsigned_remote: true,
56
+ require_signature: false
57
+ };
58
+ const source = { name: 'official', type: 'remote' };
59
+ const res = verifySignatureBlock({ manifest, policy, source });
60
+ expect(res.verified).toBe(true);
61
+ expect(res.status).toBe('unsigned');
62
+ });
63
+
64
+ it('blocks unsigned registry if require_signature is true', () => {
65
+ const policy = {
66
+ allow_unsigned_bundled: true,
67
+ allow_unsigned_local: true,
68
+ allow_unsigned_remote: true,
69
+ require_signature: true
70
+ };
71
+ const source = { name: 'bundled', type: 'local' };
72
+ const res = verifySignatureBlock({ manifest, policy, source });
73
+ expect(res.verified).toBe(false);
74
+ expect(res.error).toContain('Signature is required by policy but missing');
75
+ });
76
+ });
77
+
78
+ describe('Registry Signature Policy — Allowed Algorithms', () => {
79
+ const manifest = {
80
+ registry_name: 'test-registry',
81
+ publisher: 'Test Publisher',
82
+ version: '1.0.0',
83
+ catalog_hash: 'sha256:abc',
84
+ signature: {
85
+ algorithm: 'rsa-sha256', // Unsupported / not allowed algorithm
86
+ key_id: 'active-key',
87
+ signature: 'sig',
88
+ signed_fields: ['registry_name', 'version']
89
+ }
90
+ };
91
+
92
+ it('fails verification if algorithm is not allowed', () => {
93
+ const policy = {
94
+ allowed_signature_algorithms: ['ed25519', 'hmac-sha256']
95
+ };
96
+ const res = verifySignatureBlock({ manifest, policy });
97
+ expect(res.verified).toBe(false);
98
+ expect(res.errors[0]).toContain('not allowed by policy');
99
+ });
100
+ });
@@ -0,0 +1,193 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import {
5
+ loadSigningKey,
6
+ generateSigningKey,
7
+ saveSigningKey,
8
+ signPayload,
9
+ verifySignature,
10
+ getSigningKeyPath
11
+ } from '../../src/registry/signing.js';
12
+
13
+ const tempDir = join(process.cwd(), 'temp-signing-test');
14
+
15
+ describe('Registry Signing — loadSigningKey', () => {
16
+ beforeAll(() => {
17
+ mkdirSync(join(tempDir, '.ai'), { recursive: true });
18
+ });
19
+
20
+ afterAll(() => {
21
+ if (existsSync(tempDir)) {
22
+ rmSync(tempDir, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ it('returns null when no signing key file exists', () => {
27
+ const noKeyDir = join(tempDir, 'no-key-dir');
28
+ mkdirSync(noKeyDir, { recursive: true });
29
+ const result = loadSigningKey(noKeyDir);
30
+ expect(result).toBeNull();
31
+ });
32
+
33
+ it('returns the key string when a valid key file exists', () => {
34
+ const validKey = 'a'.repeat(64);
35
+ writeFileSync(join(tempDir, '.ai', 'registry-signing-key'), validKey + '\n', 'utf8');
36
+ const result = loadSigningKey(tempDir);
37
+ expect(result).toBe(validKey);
38
+ });
39
+
40
+ it('throws if the key file contains a malformed key', () => {
41
+ const badKeyDir = join(tempDir, 'bad-key-dir');
42
+ mkdirSync(join(badKeyDir, '.ai'), { recursive: true });
43
+ writeFileSync(join(badKeyDir, '.ai', 'registry-signing-key'), 'too-short', 'utf8');
44
+ expect(() => loadSigningKey(badKeyDir)).toThrow('malformed');
45
+ });
46
+
47
+ it('throws if the key file contains wrong-length hex', () => {
48
+ const badKeyDir2 = join(tempDir, 'bad-key-dir2');
49
+ mkdirSync(join(badKeyDir2, '.ai'), { recursive: true });
50
+ // 63 hex chars — one short
51
+ writeFileSync(join(badKeyDir2, '.ai', 'registry-signing-key'), 'a'.repeat(63), 'utf8');
52
+ expect(() => loadSigningKey(badKeyDir2)).toThrow('malformed');
53
+ });
54
+
55
+ it('getSigningKeyPath returns expected path', () => {
56
+ const expected = join(tempDir, '.ai', 'registry-signing-key');
57
+ expect(getSigningKeyPath(tempDir)).toBe(expected);
58
+ });
59
+ });
60
+
61
+ describe('Registry Signing — generateSigningKey + saveSigningKey', () => {
62
+ const genDir = join(process.cwd(), 'temp-signing-gen');
63
+
64
+ beforeAll(() => {
65
+ mkdirSync(join(genDir, '.ai'), { recursive: true });
66
+ });
67
+
68
+ afterAll(() => {
69
+ if (existsSync(genDir)) {
70
+ rmSync(genDir, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ it('generateSigningKey returns a 64-char lowercase hex string', () => {
75
+ const key = generateSigningKey();
76
+ expect(key).toMatch(/^[0-9a-f]{64}$/);
77
+ });
78
+
79
+ it('generateSigningKey returns different values each call (random)', () => {
80
+ const key1 = generateSigningKey();
81
+ const key2 = generateSigningKey();
82
+ expect(key1).not.toBe(key2);
83
+ });
84
+
85
+ it('saveSigningKey writes a valid key that loadSigningKey can read back', () => {
86
+ const key = generateSigningKey();
87
+ saveSigningKey(genDir, key);
88
+ const loaded = loadSigningKey(genDir);
89
+ expect(loaded).toBe(key);
90
+ });
91
+
92
+ it('saveSigningKey creates .ai/ directory if it does not exist', () => {
93
+ const freshDir = join(process.cwd(), 'temp-signing-fresh');
94
+ try {
95
+ const key = generateSigningKey();
96
+ saveSigningKey(freshDir, key);
97
+ expect(existsSync(join(freshDir, '.ai', 'registry-signing-key'))).toBe(true);
98
+ const loaded = loadSigningKey(freshDir);
99
+ expect(loaded).toBe(key);
100
+ } finally {
101
+ if (existsSync(freshDir)) {
102
+ rmSync(freshDir, { recursive: true, force: true });
103
+ }
104
+ }
105
+ });
106
+ });
107
+
108
+ describe('Registry Signing — signPayload', () => {
109
+ const validKey = 'f'.repeat(64);
110
+
111
+ it('returns a 64-char lowercase hex HMAC-SHA256 digest', () => {
112
+ const sig = signPayload(validKey, 'test-payload');
113
+ expect(sig).toMatch(/^[0-9a-f]{64}$/);
114
+ });
115
+
116
+ it('produces the same output for the same key and payload (deterministic)', () => {
117
+ const sig1 = signPayload(validKey, 'same-payload');
118
+ const sig2 = signPayload(validKey, 'same-payload');
119
+ expect(sig1).toBe(sig2);
120
+ });
121
+
122
+ it('produces different output for different payloads', () => {
123
+ const sig1 = signPayload(validKey, 'payload-A');
124
+ const sig2 = signPayload(validKey, 'payload-B');
125
+ expect(sig1).not.toBe(sig2);
126
+ });
127
+
128
+ it('produces different output for different keys', () => {
129
+ const key1 = 'a'.repeat(64);
130
+ const key2 = 'b'.repeat(64);
131
+ const sig1 = signPayload(key1, 'same-payload');
132
+ const sig2 = signPayload(key2, 'same-payload');
133
+ expect(sig1).not.toBe(sig2);
134
+ });
135
+
136
+ it('throws for an invalid key (non-hex)', () => {
137
+ expect(() => signPayload('not-a-valid-key'.padEnd(64, 'z'), 'payload')).toThrow('Invalid signing key');
138
+ });
139
+
140
+ it('throws for an invalid key (wrong length)', () => {
141
+ expect(() => signPayload('abc', 'payload')).toThrow('Invalid signing key');
142
+ });
143
+
144
+ it('throws if payload is not a string', () => {
145
+ expect(() => signPayload(validKey, 12345)).toThrow('Payload to sign must be a string');
146
+ });
147
+ });
148
+
149
+ describe('Registry Signing — verifySignature', () => {
150
+ const key = 'c'.repeat(64);
151
+ const payload = 'catalog-sha256-hash-string';
152
+
153
+ it('returns true for a valid signature', () => {
154
+ const sig = signPayload(key, payload);
155
+ expect(verifySignature(key, payload, sig)).toBe(true);
156
+ });
157
+
158
+ it('returns false for a tampered payload', () => {
159
+ const sig = signPayload(key, payload);
160
+ expect(verifySignature(key, 'tampered-payload', sig)).toBe(false);
161
+ });
162
+
163
+ it('returns false for a wrong key', () => {
164
+ const sig = signPayload(key, payload);
165
+ const wrongKey = 'd'.repeat(64);
166
+ expect(verifySignature(wrongKey, payload, sig)).toBe(false);
167
+ });
168
+
169
+ it('returns false for a corrupted signature', () => {
170
+ const sig = signPayload(key, payload);
171
+ // Flip first char
172
+ const corruptedSig = (sig[0] === 'a' ? 'b' : 'a') + sig.slice(1);
173
+ expect(verifySignature(key, payload, corruptedSig)).toBe(false);
174
+ });
175
+
176
+ it('returns false for non-string arguments', () => {
177
+ expect(verifySignature(null, payload, 'sig')).toBe(false);
178
+ expect(verifySignature(key, null, 'sig')).toBe(false);
179
+ expect(verifySignature(key, payload, null)).toBe(false);
180
+ });
181
+
182
+ it('returns false for mismatched-length signature', () => {
183
+ expect(verifySignature(key, payload, 'abc')).toBe(false);
184
+ });
185
+
186
+ it('uses timing-safe comparison — does not short-circuit on length match', () => {
187
+ // We can't test timing directly, but we verify the correctness of the logic:
188
+ // two sigs of equal length but different values must still return false
189
+ const sig = signPayload(key, payload);
190
+ const fakeMatchLength = 'e'.repeat(64); // Same 64-char hex length, wrong value
191
+ expect(verifySignature(key, payload, fakeMatchLength)).toBe(false);
192
+ });
193
+ });
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { loadTrustedKeys } from '../../src/registry/trust-store.js';
5
+ import { verifySignatureBlock } from '../../src/registry/signing.js';
6
+
7
+ const tempDir = join(process.cwd(), 'temp-trust-store-test');
8
+
9
+ describe('Registry Trust Store — loadTrustedKeys', () => {
10
+ beforeAll(() => {
11
+ mkdirSync(join(tempDir, '.ai', 'registries'), { recursive: true });
12
+ });
13
+
14
+ afterAll(() => {
15
+ if (existsSync(tempDir)) {
16
+ rmSync(tempDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+
20
+ it('returns empty array when no trusted-keys.yaml exists', () => {
21
+ const noKeysDir = join(tempDir, 'no-keys-dir');
22
+ mkdirSync(noKeysDir, { recursive: true });
23
+ const result = loadTrustedKeys(noKeysDir);
24
+ expect(result).toEqual([]);
25
+ });
26
+
27
+ it('returns parsed trusted keys list when trusted-keys.yaml exists', () => {
28
+ const yamlContent = `
29
+ trusted_publishers:
30
+ - key_id: official-key
31
+ name: Official Maintainer
32
+ algorithm: ed25519
33
+ public_key: "-----BEGIN PUBLIC KEY-----\\n..."
34
+ scopes:
35
+ - registry
36
+ status: active
37
+ `;
38
+ writeFileSync(join(tempDir, '.ai', 'registries', 'trusted-keys.yaml'), yamlContent, 'utf8');
39
+ const result = loadTrustedKeys(tempDir);
40
+ expect(result).toHaveLength(1);
41
+ expect(result[0].key_id).toBe('official-key');
42
+ expect(result[0].name).toBe('Official Maintainer');
43
+ });
44
+ });
45
+
46
+ describe('Registry Trust Store — Status & Scope Validation', () => {
47
+ const trustedKeys = [
48
+ {
49
+ key_id: 'active-key',
50
+ name: 'Active Key',
51
+ algorithm: 'ed25519',
52
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
53
+ scopes: ['registry'],
54
+ status: 'active'
55
+ },
56
+ {
57
+ key_id: 'disabled-key',
58
+ name: 'Disabled Key',
59
+ algorithm: 'ed25519',
60
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
61
+ scopes: ['registry'],
62
+ status: 'disabled'
63
+ },
64
+ {
65
+ key_id: 'revoked-key',
66
+ name: 'Revoked Key',
67
+ algorithm: 'ed25519',
68
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
69
+ scopes: ['registry'],
70
+ status: 'revoked'
71
+ },
72
+ {
73
+ key_id: 'wrong-scope-key',
74
+ name: 'Wrong Scope Key',
75
+ algorithm: 'ed25519',
76
+ public_key: 'MCowBQYDK2VwAyEA9vWwyE5+fY0dvEzl9S1UcvtoMkOAIDhDCzZAkP+CVNo=',
77
+ scopes: ['other'],
78
+ status: 'active'
79
+ }
80
+ ];
81
+
82
+ const manifestTemplate = {
83
+ registry_name: 'test-registry',
84
+ publisher: 'Test Publisher',
85
+ version: '1.0.0',
86
+ catalog_hash: 'sha256:abc',
87
+ signature: {
88
+ algorithm: 'ed25519',
89
+ key_id: 'active-key',
90
+ signature: 'sig',
91
+ signed_fields: ['registry_name', 'version', 'catalog_hash']
92
+ }
93
+ };
94
+
95
+ it('fails verification if key is disabled', () => {
96
+ const manifest = {
97
+ ...manifestTemplate,
98
+ signature: {
99
+ ...manifestTemplate.signature,
100
+ key_id: 'disabled-key'
101
+ }
102
+ };
103
+ const res = verifySignatureBlock({ manifest, trustedKeys });
104
+ expect(res.verified).toBe(false);
105
+ expect(res.errors[0]).toContain('must be active');
106
+ });
107
+
108
+ it('fails verification if key is revoked', () => {
109
+ const manifest = {
110
+ ...manifestTemplate,
111
+ signature: {
112
+ ...manifestTemplate.signature,
113
+ key_id: 'revoked-key'
114
+ }
115
+ };
116
+ const res = verifySignatureBlock({ manifest, trustedKeys });
117
+ expect(res.verified).toBe(false);
118
+ expect(res.errors[0]).toContain('must be active');
119
+ });
120
+
121
+ it('fails verification if scope does not include registry or catalog', () => {
122
+ const manifest = {
123
+ ...manifestTemplate,
124
+ signature: {
125
+ ...manifestTemplate.signature,
126
+ key_id: 'wrong-scope-key'
127
+ }
128
+ };
129
+ const res = verifySignatureBlock({ manifest, trustedKeys });
130
+ expect(res.verified).toBe(false);
131
+ expect(res.errors[0]).toContain('does not have required scope');
132
+ });
133
+ });