@aiwg/cli 2026.8.11 → 2026.8.13
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.
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +6 -0
- package/dist/src/api/index.js +6 -0
- package/dist/src/cli/handlers/artifact-verify.js +171 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/setup-manifest.js +52 -3
- package/dist/src/cli/handlers/setup.js +15 -2
- package/dist/src/cli/handlers/use.js +71 -6
- package/dist/src/cli/scope-resolver.js +6 -1
- package/dist/src/cli/services/deployment-verification.js +65 -7
- package/dist/src/config/aiwg-config.js +4 -3
- package/dist/src/config/cli.js +3 -1
- package/dist/src/config/gitignore.js +67 -21
- package/dist/src/config/workspace.js +8 -1
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/extensions/project-quickref.js +9 -0
- package/dist/src/marketplace/artifact-attestation.js +195 -0
- package/dist/src/marketplace/exchange.js +437 -79
- package/dist/src/marketplace/provenance-types.js +1 -0
- package/dist/src/marketplace/provenance.js +7 -1
- package/dist/src/providers/hermes-home.js +20 -0
- package/dist/src/providers/provider-definitions.js +5 -4
- package/dist/src/providers/transformation-receipt-integration.js +448 -0
- package/dist/src/providers/transformation-receipt.js +215 -0
- package/dist/src/resources/web-release.d.ts +11 -0
- package/dist/src/resources/web-release.js +61 -6
- package/dist/src/security/artifact-attestation.js +117 -0
- package/dist/src/security/artifact-trust.js +557 -0
- package/dist/src/security/artifact-verifier.js +478 -0
- package/dist/src/skills/deployer.js +5 -1
- package/dist/src/tracker/capability-protocol.js +7 -2
- package/package.json +5 -1
- package/schemas/security/aiwg-artifact-attestation.v1.schema.json +106 -0
- package/schemas/security/aiwg-artifact-provenance.v1.schema.json +204 -0
- package/schemas/security/aiwg-artifact-trust-root.v1.schema.json +59 -0
- package/schemas/security/aiwg-artifact-trust-state.v1.schema.json +32 -0
- package/schemas/security/aiwg-artifact-verification-result.v1.schema.json +46 -0
- package/schemas/security/threat-assessment-input.v1.schema.json +57 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +104 -0
- package/tools/agents/deploy-agents.mjs +8 -2
- package/tools/agents/providers/base.mjs +29 -2
- package/tools/agents/providers/hermes.mjs +163 -19
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { ARTIFACT_TRUST_STATE_SCHEMA_VERSION, channelStateKey, decodeBase64, dssePae, isIdentityRevoked, parseTrustRoot, publicKeyFingerprint, scopeMatches, selectDelegations, sha256, validateTrustState, verifyBytes, } from './artifact-trust.js';
|
|
2
|
+
export const ARTIFACT_VERIFICATION_RESULT_SCHEMA_VERSION = 'aiwg.verify.result.v1';
|
|
3
|
+
export const ARTIFACT_VERIFICATION_EXIT_CODES = {
|
|
4
|
+
verified: 0,
|
|
5
|
+
'policy-exempt': 20,
|
|
6
|
+
unsigned: 21,
|
|
7
|
+
'unknown-signer': 22,
|
|
8
|
+
expired: 23,
|
|
9
|
+
revoked: 24,
|
|
10
|
+
stale: 25,
|
|
11
|
+
mismatched: 26,
|
|
12
|
+
malformed: 27,
|
|
13
|
+
'offline-evidence-missing': 28,
|
|
14
|
+
'policy-denied': 29,
|
|
15
|
+
};
|
|
16
|
+
function record(value) {
|
|
17
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function exactKeys(value, allowed, label) {
|
|
20
|
+
const unknown = Object.keys(value).filter(key => !allowed.includes(key));
|
|
21
|
+
if (unknown.length > 0)
|
|
22
|
+
throw new Error(`${label} contains unknown field '${unknown[0]}'`);
|
|
23
|
+
}
|
|
24
|
+
function result(status, input, diagnostics, extras = {}) {
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: ARTIFACT_VERIFICATION_RESULT_SCHEMA_VERSION,
|
|
27
|
+
status,
|
|
28
|
+
exitCode: ARTIFACT_VERIFICATION_EXIT_CODES[status],
|
|
29
|
+
artifact: { name: input.artifactName, sha256: sha256(input.artifactBytes) },
|
|
30
|
+
identities: [],
|
|
31
|
+
diagnostics,
|
|
32
|
+
...extras,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function parseEnvelope(attestation) {
|
|
36
|
+
if (!record(attestation) || attestation.mediaType !== 'application/vnd.aiwg.artifact-attestation.v1+json') {
|
|
37
|
+
throw new Error('unsupported or missing attestation mediaType');
|
|
38
|
+
}
|
|
39
|
+
exactKeys(attestation, ['mediaType', 'envelope', 'verificationMaterial'], 'attestation');
|
|
40
|
+
if (!record(attestation.envelope))
|
|
41
|
+
throw new Error('attestation envelope is missing');
|
|
42
|
+
const envelope = attestation.envelope;
|
|
43
|
+
exactKeys(envelope, ['payloadType', 'payload', 'signatures'], 'DSSE envelope');
|
|
44
|
+
if (envelope.payloadType !== 'application/vnd.in-toto+json' || typeof envelope.payload !== 'string') {
|
|
45
|
+
throw new Error('unsupported DSSE payload type or payload');
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(envelope.signatures))
|
|
48
|
+
throw new Error('DSSE signatures must be an array');
|
|
49
|
+
const signatures = envelope.signatures.map((entry, index) => {
|
|
50
|
+
if (!record(entry) || typeof entry.sig !== 'string')
|
|
51
|
+
throw new Error(`DSSE signature ${index} is malformed`);
|
|
52
|
+
exactKeys(entry, ['keyid', 'sig'], `DSSE signature ${index}`);
|
|
53
|
+
if (entry.keyid !== undefined && typeof entry.keyid !== 'string')
|
|
54
|
+
throw new Error(`DSSE signature ${index} keyid is malformed`);
|
|
55
|
+
if (decodeBase64(entry.sig, `DSSE signature ${index}`).length === 0)
|
|
56
|
+
throw new Error(`DSSE signature ${index} must not be empty`);
|
|
57
|
+
return { sig: entry.sig, ...(entry.keyid ? { keyid: entry.keyid } : {}) };
|
|
58
|
+
});
|
|
59
|
+
if (attestation.verificationMaterial !== undefined && !record(attestation.verificationMaterial)) {
|
|
60
|
+
throw new Error('verificationMaterial must be an object');
|
|
61
|
+
}
|
|
62
|
+
const material = attestation.verificationMaterial;
|
|
63
|
+
if (material) {
|
|
64
|
+
if (material.kind === 'sigstore-bundle') {
|
|
65
|
+
exactKeys(material, ['kind', 'mediaType', 'bundle'], 'Sigstore verification material');
|
|
66
|
+
if (typeof material.mediaType !== 'string' || !record(material.bundle))
|
|
67
|
+
throw new Error('Sigstore verification material is incomplete');
|
|
68
|
+
}
|
|
69
|
+
else if (material.kind === 'public-key') {
|
|
70
|
+
exactKeys(material, ['kind', 'algorithm', 'publicKey'], 'public-key verification material');
|
|
71
|
+
if (!['ed25519', 'ecdsa-p256-sha256', 'rsa-pss-sha256'].includes(String(material.algorithm)) || typeof material.publicKey !== 'string' || !material.publicKey)
|
|
72
|
+
throw new Error('public-key verification material is incomplete');
|
|
73
|
+
}
|
|
74
|
+
else if (material.kind === 'detached') {
|
|
75
|
+
exactKeys(material, ['kind', 'mediaType', 'uri', 'sha256', 'bytes'], 'detached verification material');
|
|
76
|
+
if (typeof material.mediaType !== 'string' || typeof material.uri !== 'string' || typeof material.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(material.sha256) || !Number.isInteger(material.bytes) || Number(material.bytes) < 1)
|
|
77
|
+
throw new Error('detached verification material is incomplete');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
throw new Error('verificationMaterial has an unsupported kind');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
envelope: { payloadType: envelope.payloadType, payload: envelope.payload, signatures },
|
|
85
|
+
material,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseProvenance(payload) {
|
|
89
|
+
let value;
|
|
90
|
+
try {
|
|
91
|
+
value = JSON.parse(Buffer.from(payload).toString('utf8'));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new Error(`signed payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
+
}
|
|
96
|
+
if (!record(value)
|
|
97
|
+
|| value._type !== 'https://in-toto.io/Statement/v1'
|
|
98
|
+
|| value.predicateType !== 'https://aiwg.io/attestations/artifact-provenance/v1'
|
|
99
|
+
|| !Array.isArray(value.subject) || value.subject.length === 0
|
|
100
|
+
|| !record(value.predicate))
|
|
101
|
+
throw new Error('signed payload is not an AIWG provenance Statement v1');
|
|
102
|
+
exactKeys(value, ['_type', 'subject', 'predicateType', 'predicate'], 'Statement');
|
|
103
|
+
const predicate = value.predicate;
|
|
104
|
+
exactKeys(predicate, ['schemaVersion', 'assetType', 'publisher', 'publication', 'issuedAt', 'notBefore', 'expiresAt', 'derivation', 'provenanceGraph', 'dependencies'], 'provenance predicate');
|
|
105
|
+
if (predicate.schemaVersion !== 'aiwg.artifact-provenance.v1'
|
|
106
|
+
|| typeof predicate.assetType !== 'string'
|
|
107
|
+
|| !record(predicate.publisher)
|
|
108
|
+
|| !record(predicate.publication)
|
|
109
|
+
|| typeof predicate.publisher.id !== 'string'
|
|
110
|
+
|| typeof predicate.publisher.namespace !== 'string'
|
|
111
|
+
|| typeof predicate.publication.channel !== 'string'
|
|
112
|
+
|| typeof predicate.publication.version !== 'string'
|
|
113
|
+
|| !Number.isInteger(predicate.publication.sequence) || Number(predicate.publication.sequence) < 1
|
|
114
|
+
|| typeof predicate.issuedAt !== 'string')
|
|
115
|
+
throw new Error('signed provenance predicate is incomplete');
|
|
116
|
+
if (!predicate.assetType || !predicate.publisher.id || !predicate.publisher.namespace || !predicate.publication.channel || !predicate.publication.version)
|
|
117
|
+
throw new Error('signed provenance predicate strings must not be empty');
|
|
118
|
+
exactKeys(predicate.publisher, ['id', 'namespace', 'role'], 'publisher');
|
|
119
|
+
exactKeys(predicate.publication, ['version', 'channel', 'sequence', 'sourceUri', 'collection', 'supersedes'], 'publication');
|
|
120
|
+
for (const dateField of ['issuedAt', 'notBefore', 'expiresAt']) {
|
|
121
|
+
if (predicate[dateField] !== undefined && !Number.isFinite(Date.parse(String(predicate[dateField]))))
|
|
122
|
+
throw new Error(`${dateField} is invalid`);
|
|
123
|
+
}
|
|
124
|
+
const issuedAt = Date.parse(predicate.issuedAt);
|
|
125
|
+
if (predicate.notBefore && predicate.expiresAt && Date.parse(String(predicate.expiresAt)) <= Date.parse(String(predicate.notBefore)))
|
|
126
|
+
throw new Error('provenance validity window must be ordered');
|
|
127
|
+
if (predicate.expiresAt && Date.parse(String(predicate.expiresAt)) <= issuedAt)
|
|
128
|
+
throw new Error('provenance expiry must follow issuance');
|
|
129
|
+
const validateDescriptor = (descriptor, label) => {
|
|
130
|
+
if (!record(descriptor) || typeof descriptor.name !== 'string' || !descriptor.name || !record(descriptor.digest) || typeof descriptor.digest.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(descriptor.digest.sha256)) {
|
|
131
|
+
throw new Error(`${label} is malformed`);
|
|
132
|
+
}
|
|
133
|
+
exactKeys(descriptor, ['name', 'uri', 'mediaType', 'digest'], label);
|
|
134
|
+
exactKeys(descriptor.digest, ['sha256'], `${label} digest`);
|
|
135
|
+
};
|
|
136
|
+
for (const subject of value.subject) {
|
|
137
|
+
validateDescriptor(subject, 'subject');
|
|
138
|
+
}
|
|
139
|
+
for (const field of ['collection', 'supersedes']) {
|
|
140
|
+
if (predicate.publication[field] !== undefined)
|
|
141
|
+
validateDescriptor(predicate.publication[field], `publication ${field}`);
|
|
142
|
+
}
|
|
143
|
+
if (predicate.dependencies !== undefined) {
|
|
144
|
+
if (!Array.isArray(predicate.dependencies))
|
|
145
|
+
throw new Error('dependencies must be an array');
|
|
146
|
+
predicate.dependencies.forEach((dependency, index) => validateDescriptor(dependency, `dependency ${index}`));
|
|
147
|
+
}
|
|
148
|
+
if (predicate.derivation !== undefined) {
|
|
149
|
+
if (!record(predicate.derivation) || !record(predicate.derivation.builder) || !Array.isArray(predicate.derivation.materials))
|
|
150
|
+
throw new Error('derivation is malformed');
|
|
151
|
+
exactKeys(predicate.derivation, ['builder', 'transformation', 'materials', 'reproducible', 'invocationId'], 'derivation');
|
|
152
|
+
exactKeys(predicate.derivation.builder, ['id', 'version'], 'derivation builder');
|
|
153
|
+
if (typeof predicate.derivation.builder.id !== 'string' || !predicate.derivation.builder.id || predicate.derivation.materials.length === 0)
|
|
154
|
+
throw new Error('derivation builder/materials are incomplete');
|
|
155
|
+
if (predicate.derivation.transformation !== undefined) {
|
|
156
|
+
if (!record(predicate.derivation.transformation))
|
|
157
|
+
throw new Error('derivation transformation is malformed');
|
|
158
|
+
exactKeys(predicate.derivation.transformation, ['id', 'version', 'provider'], 'derivation transformation');
|
|
159
|
+
if (typeof predicate.derivation.transformation.id !== 'string' || typeof predicate.derivation.transformation.version !== 'string')
|
|
160
|
+
throw new Error('derivation transformation is incomplete');
|
|
161
|
+
}
|
|
162
|
+
for (const material of predicate.derivation.materials) {
|
|
163
|
+
if (!record(material) || typeof material.uri !== 'string' || !record(material.digest) || typeof material.digest.sha256 !== 'string')
|
|
164
|
+
throw new Error('derivation material is malformed');
|
|
165
|
+
exactKeys(material, ['name', 'uri', 'mediaType', 'digest'], 'derivation material');
|
|
166
|
+
if (material.name !== undefined && (typeof material.name !== 'string' || !material.name))
|
|
167
|
+
throw new Error('derivation material name is malformed');
|
|
168
|
+
if (material.mediaType !== undefined && (typeof material.mediaType !== 'string' || !material.mediaType))
|
|
169
|
+
throw new Error('derivation material mediaType is malformed');
|
|
170
|
+
exactKeys(material.digest, ['sha256'], 'derivation material digest');
|
|
171
|
+
if (!/^[a-f0-9]{64}$/.test(material.digest.sha256))
|
|
172
|
+
throw new Error('derivation material SHA-256 is malformed');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (predicate.provenanceGraph !== undefined) {
|
|
176
|
+
if (!record(predicate.provenanceGraph))
|
|
177
|
+
throw new Error('provenanceGraph is malformed');
|
|
178
|
+
exactKeys(predicate.provenanceGraph, ['standard', 'uri', 'sha256'], 'provenanceGraph');
|
|
179
|
+
if (predicate.provenanceGraph.standard !== 'W3C-PROV' || typeof predicate.provenanceGraph.uri !== 'string' || typeof predicate.provenanceGraph.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(predicate.provenanceGraph.sha256))
|
|
180
|
+
throw new Error('provenanceGraph is incomplete');
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
function isExecutionAsset(assetType) {
|
|
185
|
+
return new Set([
|
|
186
|
+
'setup-manifest', 'agentic-flow', 'agent', 'skill', 'command', 'rule', 'behavior',
|
|
187
|
+
'tool', 'mcp-server', 'hook', 'workflow', 'profile', 'plugin', 'generated-provider-artifact',
|
|
188
|
+
]).has(assetType);
|
|
189
|
+
}
|
|
190
|
+
async function verifySigstore(identity, root, material, envelope) {
|
|
191
|
+
if (material?.kind !== 'sigstore-bundle' || !record(material.bundle))
|
|
192
|
+
return undefined;
|
|
193
|
+
const profile = root.signed.sigstoreProfiles.find(candidate => candidate.id === identity.profile);
|
|
194
|
+
if (!profile)
|
|
195
|
+
return undefined;
|
|
196
|
+
try {
|
|
197
|
+
const [{ bundleFromJSON, isBundleWithDsseEnvelope }, { TrustedRoot }, { Verifier, toSignedEntity, toTrustMaterial }] = await Promise.all([
|
|
198
|
+
// @ts-ignore -- runtime-optional; metadata CI intentionally installs with --omit=optional.
|
|
199
|
+
import('@sigstore/bundle'),
|
|
200
|
+
// @ts-ignore -- runtime-optional; verifySigstore fails closed when unavailable.
|
|
201
|
+
import('@sigstore/protobuf-specs'),
|
|
202
|
+
// @ts-ignore -- runtime-optional; verifySigstore fails closed when unavailable.
|
|
203
|
+
import('@sigstore/verify'),
|
|
204
|
+
]);
|
|
205
|
+
const bundle = bundleFromJSON(material.bundle);
|
|
206
|
+
if (!isBundleWithDsseEnvelope(bundle))
|
|
207
|
+
return undefined;
|
|
208
|
+
const bundled = bundle.content.dsseEnvelope;
|
|
209
|
+
if (bundled.payloadType !== envelope.payloadType || !Buffer.from(bundled.payload).equals(decodeBase64(envelope.payload, 'DSSE payload')))
|
|
210
|
+
return undefined;
|
|
211
|
+
if (bundled.signatures.length !== envelope.signatures.length)
|
|
212
|
+
return undefined;
|
|
213
|
+
for (let index = 0; index < bundled.signatures.length; index += 1) {
|
|
214
|
+
if (!Buffer.from(bundled.signatures[index].sig).equals(decodeBase64(envelope.signatures[index].sig, 'DSSE signature')))
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const trustedRoot = TrustedRoot.fromJSON(profile.trustedRoot);
|
|
218
|
+
const verifier = new Verifier(toTrustMaterial(trustedRoot), {
|
|
219
|
+
tlogThreshold: profile.tlogThreshold,
|
|
220
|
+
ctlogThreshold: profile.ctlogThreshold,
|
|
221
|
+
timestampThreshold: profile.timestampThreshold,
|
|
222
|
+
});
|
|
223
|
+
const signer = verifier.verify(toSignedEntity(bundle), {
|
|
224
|
+
subjectAlternativeName: identity.subjectAlternativeName,
|
|
225
|
+
...(identity.issuer ? { extensions: { issuer: identity.issuer } } : {}),
|
|
226
|
+
});
|
|
227
|
+
return sha256(signer.key.export({ format: 'der', type: 'spki' }));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function verifiedIdentities(identities, envelope, pae, root, material) {
|
|
234
|
+
const verified = [];
|
|
235
|
+
const proofs = new Set();
|
|
236
|
+
for (const identity of identities) {
|
|
237
|
+
let accepted = false;
|
|
238
|
+
let proof = '';
|
|
239
|
+
if (identity.kind === 'public-key') {
|
|
240
|
+
for (const signature of envelope.signatures) {
|
|
241
|
+
try {
|
|
242
|
+
if (verifyBytes(identity.algorithm, identity.publicKey, pae, decodeBase64(signature.sig, 'DSSE signature'))) {
|
|
243
|
+
accepted = true;
|
|
244
|
+
proof = `public-key:${publicKeyFingerprint(identity.publicKey)}`;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { /* malformed signatures do not authenticate */ }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const signerFingerprint = await verifySigstore(identity, root, material, envelope);
|
|
253
|
+
accepted = Boolean(signerFingerprint);
|
|
254
|
+
proof = signerFingerprint ? `sigstore:${signerFingerprint}` : '';
|
|
255
|
+
}
|
|
256
|
+
if (accepted && !proofs.has(proof)) {
|
|
257
|
+
proofs.add(proof);
|
|
258
|
+
verified.push(identity);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return verified;
|
|
262
|
+
}
|
|
263
|
+
export async function verifyArtifact(input) {
|
|
264
|
+
let root;
|
|
265
|
+
let envelope;
|
|
266
|
+
let material;
|
|
267
|
+
let payload;
|
|
268
|
+
const now = input.now ?? new Date().toISOString();
|
|
269
|
+
const nowMs = Date.parse(now);
|
|
270
|
+
try {
|
|
271
|
+
root = parseTrustRoot(input.rootBytes);
|
|
272
|
+
if (input.state)
|
|
273
|
+
validateTrustState(input.state);
|
|
274
|
+
({ envelope, material } = parseEnvelope(input.attestation));
|
|
275
|
+
payload = decodeBase64(envelope.payload, 'DSSE payload');
|
|
276
|
+
if (!Number.isFinite(nowMs))
|
|
277
|
+
throw new Error('verification time is invalid');
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
return result('malformed', input, [{ code: 'MALFORMED_INPUT', message: error instanceof Error ? error.message : String(error) }]);
|
|
281
|
+
}
|
|
282
|
+
const common = { policy: root.signed.policy.name, rootVersion: root.signed.version };
|
|
283
|
+
if (nowMs < Date.parse(root.signed.issuedAt) || nowMs > Date.parse(root.signed.expiresAt)) {
|
|
284
|
+
return result('expired', input, [{ code: 'ROOT_TIME_WINDOW', message: 'The active trust root is outside its validity window' }], common);
|
|
285
|
+
}
|
|
286
|
+
if (envelope.signatures.length === 0) {
|
|
287
|
+
const scoped = input.expectedScope && root.signed.policy.allowPolicyExempt.some(scope => scopeMatches(scope, input.expectedScope));
|
|
288
|
+
if (scoped && !isExecutionAsset(input.expectedScope.assetType)) {
|
|
289
|
+
if (!input.state)
|
|
290
|
+
return result(input.offline ? 'offline-evidence-missing' : 'policy-denied', input, [{
|
|
291
|
+
code: 'TRUST_STATE_REQUIRED', message: 'A policy exemption requires bootstrapped trust state',
|
|
292
|
+
}], common);
|
|
293
|
+
if (input.state.rootVersion !== root.signed.version || input.state.rootSha256 !== sha256(input.rootBytes)) {
|
|
294
|
+
return result('mismatched', input, [{ code: 'TRUST_STATE_MISMATCH', message: 'Persisted state does not match the exact active trust root' }], common);
|
|
295
|
+
}
|
|
296
|
+
if (nowMs < Date.parse(input.state.trustedTime))
|
|
297
|
+
return result('stale', input, [{ code: 'CLOCK_ROLLBACK', message: 'Verification time predates persisted trusted time' }], common);
|
|
298
|
+
return result('policy-exempt', input, [{ code: 'EXPLICIT_POLICY_EXEMPTION', message: 'Unsigned non-executable asset is explicitly exempted by policy' }], common);
|
|
299
|
+
}
|
|
300
|
+
return result('unsigned', input, [{ code: 'SIGNATURE_REQUIRED', message: 'The attestation has no signatures' }], common);
|
|
301
|
+
}
|
|
302
|
+
if (material?.kind === 'detached') {
|
|
303
|
+
const uri = String(material.uri);
|
|
304
|
+
const detached = input.materials?.get(uri);
|
|
305
|
+
if (!detached)
|
|
306
|
+
return result(input.offline ? 'offline-evidence-missing' : 'policy-denied', input, [{
|
|
307
|
+
code: 'DETACHED_VERIFICATION_MATERIAL_MISSING', message: `Detached verification material is unavailable: ${uri}`,
|
|
308
|
+
}], common);
|
|
309
|
+
if (detached.length !== Number(material.bytes) || sha256(detached) !== material.sha256)
|
|
310
|
+
return result('mismatched', input, [{
|
|
311
|
+
code: 'DETACHED_VERIFICATION_MATERIAL_MISMATCH', message: `Detached verification material does not match its signed descriptor: ${uri}`,
|
|
312
|
+
}], common);
|
|
313
|
+
if (!String(material.mediaType).startsWith('application/vnd.dev.sigstore.bundle.'))
|
|
314
|
+
return result('policy-denied', input, [{
|
|
315
|
+
code: 'DETACHED_VERIFICATION_MATERIAL_UNSUPPORTED', message: `Unsupported detached verification material type: ${String(material.mediaType)}`,
|
|
316
|
+
}], common);
|
|
317
|
+
try {
|
|
318
|
+
const bundle = JSON.parse(Buffer.from(detached).toString('utf8'));
|
|
319
|
+
if (!record(bundle))
|
|
320
|
+
throw new Error('bundle is not an object');
|
|
321
|
+
material = { kind: 'sigstore-bundle', mediaType: material.mediaType, bundle };
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return result('malformed', input, [{
|
|
325
|
+
code: 'DETACHED_VERIFICATION_MATERIAL_MALFORMED',
|
|
326
|
+
message: error instanceof Error ? error.message : String(error),
|
|
327
|
+
}], common);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Authenticate the exact payload bytes before parsing any signed claims. A
|
|
331
|
+
// keyid is only a lookup hint; every trust-root identity remains a candidate.
|
|
332
|
+
const allAuthenticated = await verifiedIdentities(root.signed.identities, envelope, dssePae(envelope.payloadType, payload), root, material);
|
|
333
|
+
if (allAuthenticated.length === 0) {
|
|
334
|
+
if (input.offline && !material)
|
|
335
|
+
return result('offline-evidence-missing', input, [{
|
|
336
|
+
code: 'OFFLINE_VERIFICATION_MATERIAL_MISSING',
|
|
337
|
+
message: 'Portable verification material is unavailable in offline mode',
|
|
338
|
+
}], common);
|
|
339
|
+
return result('unknown-signer', input, [{
|
|
340
|
+
code: 'UNKNOWN_SIGNER',
|
|
341
|
+
message: 'No trusted identity verified the signature; unauthenticated keyid and outer metadata were ignored',
|
|
342
|
+
}], common);
|
|
343
|
+
}
|
|
344
|
+
let statement;
|
|
345
|
+
try {
|
|
346
|
+
statement = parseProvenance(payload);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
return result('malformed', input, [{ code: 'MALFORMED_SIGNED_PAYLOAD', message: error instanceof Error ? error.message : String(error) }], {
|
|
350
|
+
...common,
|
|
351
|
+
identities: allAuthenticated.map(identity => identity.id).sort(),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const scopeInput = {
|
|
355
|
+
assetType: statement.predicate.assetType,
|
|
356
|
+
namespace: statement.predicate.publisher.namespace,
|
|
357
|
+
channel: statement.predicate.publication.channel,
|
|
358
|
+
now,
|
|
359
|
+
};
|
|
360
|
+
if (input.expectedScope && (input.expectedScope.assetType !== scopeInput.assetType
|
|
361
|
+
|| input.expectedScope.namespace !== scopeInput.namespace
|
|
362
|
+
|| input.expectedScope.channel !== scopeInput.channel))
|
|
363
|
+
return result('policy-denied', input, [{
|
|
364
|
+
code: 'EXPECTED_SCOPE_MISMATCH',
|
|
365
|
+
message: 'Authenticated provenance scope does not match the caller-required scope',
|
|
366
|
+
}], { ...common, identities: allAuthenticated.map(identity => identity.id).sort() });
|
|
367
|
+
const freshness = {
|
|
368
|
+
namespace: scopeInput.namespace,
|
|
369
|
+
channel: scopeInput.channel,
|
|
370
|
+
sequence: statement.predicate.publication.sequence,
|
|
371
|
+
version: statement.predicate.publication.version,
|
|
372
|
+
};
|
|
373
|
+
const scopedDelegations = selectDelegations(root, scopeInput);
|
|
374
|
+
if (scopedDelegations.length === 0) {
|
|
375
|
+
return result('policy-denied', input, [{ code: 'NO_AUTHORIZED_DELEGATION', message: 'No active delegation authorizes this asset scope' }], { ...common, freshness });
|
|
376
|
+
}
|
|
377
|
+
const authorizedIds = new Set(scopedDelegations.flatMap(delegation => delegation.keyIds));
|
|
378
|
+
const authenticated = allAuthenticated.filter(identity => authorizedIds.has(identity.id));
|
|
379
|
+
if (authenticated.length === 0) {
|
|
380
|
+
return result('policy-denied', input, [{
|
|
381
|
+
code: 'SIGNER_OUTSIDE_SCOPE',
|
|
382
|
+
message: 'The authenticated identity is not authorized for this asset scope',
|
|
383
|
+
}], { ...common, identities: allAuthenticated.map(identity => identity.id).sort(), freshness });
|
|
384
|
+
}
|
|
385
|
+
const satisfied = scopedDelegations.find(delegation => {
|
|
386
|
+
const groups = new Set(authenticated
|
|
387
|
+
.filter(identity => delegation.keyIds.includes(identity.id))
|
|
388
|
+
.map(identity => identity.independenceGroup));
|
|
389
|
+
return groups.size >= delegation.threshold;
|
|
390
|
+
});
|
|
391
|
+
const identities = authenticated.map(identity => identity.id).sort();
|
|
392
|
+
if (!satisfied) {
|
|
393
|
+
return result('policy-denied', input, [{ code: 'SIGNATURE_THRESHOLD', message: 'Independent signature threshold was not met' }], { ...common, identities, freshness });
|
|
394
|
+
}
|
|
395
|
+
for (const identity of authenticated) {
|
|
396
|
+
const revocation = isIdentityRevoked(root, identity.id, { ...scopeInput, issuedAt: statement.predicate.issuedAt });
|
|
397
|
+
if (revocation)
|
|
398
|
+
return result('revoked', input, [{ code: 'IDENTITY_REVOKED', message: revocation.reason }], { ...common, identities, freshness });
|
|
399
|
+
}
|
|
400
|
+
const issuedAt = Date.parse(statement.predicate.issuedAt);
|
|
401
|
+
if (!Number.isFinite(issuedAt))
|
|
402
|
+
return result('malformed', input, [{ code: 'INVALID_ISSUED_AT', message: 'issuedAt is invalid' }], { ...common, identities, freshness });
|
|
403
|
+
if ((statement.predicate.notBefore && nowMs < Date.parse(statement.predicate.notBefore))
|
|
404
|
+
|| (statement.predicate.expiresAt && nowMs > Date.parse(statement.predicate.expiresAt))) {
|
|
405
|
+
return result('expired', input, [{ code: 'ATTESTATION_TIME_WINDOW', message: 'The attestation is outside its validity window' }], { ...common, identities, freshness });
|
|
406
|
+
}
|
|
407
|
+
const artifactDigest = sha256(input.artifactBytes);
|
|
408
|
+
if (!statement.subject.some(subject => subject.name === input.artifactName && subject.digest.sha256 === artifactDigest)) {
|
|
409
|
+
return result('mismatched', input, [{ code: 'SUBJECT_DIGEST_MISMATCH', message: 'Exact artifact bytes do not match a named signed subject' }], { ...common, identities, freshness });
|
|
410
|
+
}
|
|
411
|
+
for (const signedMaterial of statement.predicate.derivation?.materials ?? []) {
|
|
412
|
+
const bytes = input.materials?.get(signedMaterial.uri);
|
|
413
|
+
if (!bytes) {
|
|
414
|
+
if (root.signed.policy.requireMaterialDigests)
|
|
415
|
+
return result(input.offline ? 'offline-evidence-missing' : 'policy-denied', input, [{ code: 'MATERIAL_MISSING', message: `Required material is unavailable: ${signedMaterial.uri}` }], { ...common, identities, freshness });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (sha256(bytes) !== signedMaterial.digest.sha256)
|
|
419
|
+
return result('mismatched', input, [{ code: 'MATERIAL_DIGEST_MISMATCH', message: `Material bytes do not match: ${signedMaterial.uri}` }], { ...common, identities, freshness });
|
|
420
|
+
}
|
|
421
|
+
if (input.offline && (!input.state || !material)) {
|
|
422
|
+
return result('offline-evidence-missing', input, [{ code: 'OFFLINE_STATE_OR_EVIDENCE_MISSING', message: 'Offline verification requires persisted state and portable verification material' }], { ...common, identities, freshness });
|
|
423
|
+
}
|
|
424
|
+
const state = input.state;
|
|
425
|
+
if (!state)
|
|
426
|
+
return result(input.offline ? 'offline-evidence-missing' : 'policy-denied', input, [{
|
|
427
|
+
code: 'TRUST_STATE_REQUIRED',
|
|
428
|
+
message: 'Persisted root and freshness state is required; bootstrap the trust root first',
|
|
429
|
+
}], { ...common, identities, freshness });
|
|
430
|
+
{
|
|
431
|
+
if (state.schemaVersion !== ARTIFACT_TRUST_STATE_SCHEMA_VERSION
|
|
432
|
+
|| state.rootVersion !== root.signed.version
|
|
433
|
+
|| state.rootSha256 !== sha256(input.rootBytes)) {
|
|
434
|
+
return result('mismatched', input, [{ code: 'TRUST_STATE_MISMATCH', message: 'Persisted state does not match the exact active trust root' }], { ...common, identities, freshness });
|
|
435
|
+
}
|
|
436
|
+
if (nowMs < Date.parse(state.trustedTime))
|
|
437
|
+
return result('stale', input, [{ code: 'CLOCK_ROLLBACK', message: 'Verification time predates persisted trusted time' }], { ...common, identities, freshness });
|
|
438
|
+
const member = { assetType: scopeInput.assetType, subject: input.artifactName };
|
|
439
|
+
const memberKey = channelStateKey(scopeInput.namespace, scopeInput.channel, member);
|
|
440
|
+
// Pre-member v1 state is conservatively inherited by the first migration.
|
|
441
|
+
// Operators must migrate/reset ambiguous legacy state before adding a
|
|
442
|
+
// distinct collection member; silently ignoring it would permit rollback.
|
|
443
|
+
const legacy = state.channels[channelStateKey(scopeInput.namespace, scopeInput.channel)];
|
|
444
|
+
const prior = state.channels[memberKey] ?? legacy;
|
|
445
|
+
if (prior) {
|
|
446
|
+
if (freshness.sequence < prior.sequence
|
|
447
|
+
|| freshness.sequence > prior.sequence + 1
|
|
448
|
+
|| (freshness.sequence === prior.sequence && artifactDigest !== prior.artifactSha256)
|
|
449
|
+
|| (freshness.sequence === prior.sequence && nowMs - Date.parse(prior.verifiedAt) > root.signed.policy.maxFreezeSeconds * 1000)) {
|
|
450
|
+
return result('stale', input, [{ code: 'SEQUENCE_FRESHNESS', message: 'Rollback, fast-forward, mix-and-match, or freeze protection rejected this artifact' }], { ...common, identities, freshness });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const nextState = {
|
|
455
|
+
...state,
|
|
456
|
+
trustedTime: new Date(Math.max(Date.parse(state.trustedTime), nowMs)).toISOString(),
|
|
457
|
+
channels: {
|
|
458
|
+
...state.channels,
|
|
459
|
+
[channelStateKey(scopeInput.namespace, scopeInput.channel, {
|
|
460
|
+
assetType: scopeInput.assetType,
|
|
461
|
+
subject: input.artifactName,
|
|
462
|
+
})]: {
|
|
463
|
+
namespace: scopeInput.namespace,
|
|
464
|
+
channel: scopeInput.channel,
|
|
465
|
+
subject: input.artifactName,
|
|
466
|
+
assetType: scopeInput.assetType,
|
|
467
|
+
sequence: freshness.sequence,
|
|
468
|
+
artifactSha256: artifactDigest,
|
|
469
|
+
version: freshness.version,
|
|
470
|
+
verifiedAt: now,
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
return result('verified', input, [{ code: 'VERIFIED', message: 'Signature, policy, exact bytes, materials, and freshness checks passed' }], {
|
|
475
|
+
...common, identities, freshness, nextState,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
//# sourceMappingURL=artifact-verifier.js.map
|
|
@@ -15,6 +15,7 @@ import { getProviderDefinition, normalizeProviderDefinitionId, } from '../provid
|
|
|
15
15
|
import { AGENT_SKILLS_SIDECAR_SCHEMA, AIWG_SKILL_CONTROL_FIELDS, createAgentSkillSidecar, projectStrictAgentSkill, } from './agent-skills.js';
|
|
16
16
|
import { getImportedAgentSkill } from './importer.js';
|
|
17
17
|
import { validateAgentSkillContent } from './validator.js';
|
|
18
|
+
import { resolveHermesHomePath } from '../providers/hermes-home.js';
|
|
18
19
|
export const AGENT_SKILL_MANAGED_MARKER = '.aiwg-managed';
|
|
19
20
|
export const AGENT_SKILL_DEPLOYMENT_SIDECAR = '.aiwg-agent-skill.json';
|
|
20
21
|
const MARKER_CONTENT = 'aiwg-agent-skill-v1\n';
|
|
@@ -78,7 +79,10 @@ function resolvePolicy(target, options) {
|
|
|
78
79
|
reasons.push('applies the Factory description guidance before strict validation');
|
|
79
80
|
break;
|
|
80
81
|
case 'hermes':
|
|
81
|
-
|
|
82
|
+
if (options.homeDir === undefined) {
|
|
83
|
+
root = resolveHermesHomePath('skills');
|
|
84
|
+
}
|
|
85
|
+
reasons.push('uses the active HERMES_HOME skills surface with strict managed ownership markers');
|
|
82
86
|
break;
|
|
83
87
|
case 'openhuman':
|
|
84
88
|
status = 'projected';
|
|
@@ -25,7 +25,7 @@ function providerFromIssueStorage(issueStorage) {
|
|
|
25
25
|
return 'unknown';
|
|
26
26
|
}
|
|
27
27
|
function normalizeProvider(provider) {
|
|
28
|
-
return provider === 'gitea' || provider === 'github' || provider === 'gitlab'
|
|
28
|
+
return provider === 'gitea' || provider === 'github' || provider === 'gitlab' || provider === 'local'
|
|
29
29
|
? provider
|
|
30
30
|
: 'unknown';
|
|
31
31
|
}
|
|
@@ -52,6 +52,7 @@ export function resolveTrackerAuthority(config, remoteUrls = {}, configPath = '.
|
|
|
52
52
|
const issueStorage = config?.delivery?.issue_storage;
|
|
53
53
|
const issueTrackerUrl = remoteUrls[remotes.issue_tracker];
|
|
54
54
|
const storageProvider = providerFromIssueStorage(issueStorage);
|
|
55
|
+
const configuredProvider = remotes.issue_provider ? normalizeProvider(remotes.issue_provider) : 'unknown';
|
|
55
56
|
const urlProvider = issueTrackerUrl ? normalizeProvider(resolveRemoteProvider(issueTrackerUrl)) : 'unknown';
|
|
56
57
|
return {
|
|
57
58
|
configPath,
|
|
@@ -60,7 +61,11 @@ export function resolveTrackerAuthority(config, remoteUrls = {}, configPath = '.
|
|
|
60
61
|
ciRemote: remotes.ci,
|
|
61
62
|
issueTrackerUrl,
|
|
62
63
|
issueStorage,
|
|
63
|
-
provider:
|
|
64
|
+
provider: configuredProvider !== 'unknown'
|
|
65
|
+
? configuredProvider
|
|
66
|
+
: storageProvider !== 'unknown'
|
|
67
|
+
? storageProvider
|
|
68
|
+
: urlProvider,
|
|
64
69
|
secondaryRemotes: remotes.secondary,
|
|
65
70
|
};
|
|
66
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cli",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.13",
|
|
4
4
|
"description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"THIRD_PARTY_NOTICES.md",
|
|
28
28
|
"bin/",
|
|
29
29
|
"agentic/code/providers/",
|
|
30
|
+
"schemas/security/",
|
|
30
31
|
"dist/src/",
|
|
31
32
|
"!dist/**/*.d.ts",
|
|
32
33
|
"!dist/**/*.map",
|
|
@@ -80,6 +81,9 @@
|
|
|
80
81
|
},
|
|
81
82
|
"optionalDependencies": {
|
|
82
83
|
"@hono/node-server": "2.0.11",
|
|
84
|
+
"@sigstore/bundle": "4.0.0",
|
|
85
|
+
"@sigstore/protobuf-specs": "0.5.0",
|
|
86
|
+
"@sigstore/verify": "3.1.1",
|
|
83
87
|
"hono": "^4.12.31",
|
|
84
88
|
"ws": "^8.21.1"
|
|
85
89
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://aiwg.io/schemas/security/aiwg-artifact-attestation.v1.schema.json",
|
|
4
|
+
"title": "AIWG cross-asset DSSE attestation envelope v1",
|
|
5
|
+
"description": "Transport wrapper for a DSSE-signed in-toto Statement. Security decisions come from the authenticated payload and verifier policy, never from outer fields.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["mediaType", "envelope"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"mediaType": {
|
|
11
|
+
"const": "application/vnd.aiwg.artifact-attestation.v1+json"
|
|
12
|
+
},
|
|
13
|
+
"envelope": {
|
|
14
|
+
"$ref": "#/$defs/dsseEnvelope"
|
|
15
|
+
},
|
|
16
|
+
"verificationMaterial": {
|
|
17
|
+
"oneOf": [
|
|
18
|
+
{ "$ref": "#/$defs/detachedMaterial" },
|
|
19
|
+
{ "$ref": "#/$defs/sigstoreMaterial" },
|
|
20
|
+
{ "$ref": "#/$defs/publicKeyMaterial" }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"$defs": {
|
|
25
|
+
"base64": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 4,
|
|
28
|
+
"pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
|
|
29
|
+
},
|
|
30
|
+
"sha256": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
33
|
+
},
|
|
34
|
+
"dsseEnvelope": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["payloadType", "payload", "signatures"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"payloadType": {
|
|
40
|
+
"const": "application/vnd.in-toto+json"
|
|
41
|
+
},
|
|
42
|
+
"payload": {
|
|
43
|
+
"$ref": "#/$defs/base64",
|
|
44
|
+
"contentEncoding": "base64",
|
|
45
|
+
"contentMediaType": "application/vnd.in-toto+json"
|
|
46
|
+
},
|
|
47
|
+
"signatures": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"minItems": 1,
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"required": ["sig"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"keyid": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"minLength": 1,
|
|
58
|
+
"maxLength": 512
|
|
59
|
+
},
|
|
60
|
+
"sig": {
|
|
61
|
+
"$ref": "#/$defs/base64"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"detachedMaterial": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"required": ["kind", "mediaType", "uri", "sha256", "bytes"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"kind": { "const": "detached" },
|
|
74
|
+
"mediaType": { "type": "string", "minLength": 1 },
|
|
75
|
+
"uri": { "type": "string", "format": "uri" },
|
|
76
|
+
"sha256": { "$ref": "#/$defs/sha256" },
|
|
77
|
+
"bytes": { "type": "integer", "minimum": 1 }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"sigstoreMaterial": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"required": ["kind", "mediaType", "bundle"],
|
|
84
|
+
"properties": {
|
|
85
|
+
"kind": { "const": "sigstore-bundle" },
|
|
86
|
+
"mediaType": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"pattern": "^application/vnd\\.dev\\.sigstore\\.bundle\\.v[0-9]+(?:\\.[0-9]+)?\\+json$"
|
|
89
|
+
},
|
|
90
|
+
"bundle": { "type": "object" }
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"publicKeyMaterial": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"additionalProperties": false,
|
|
96
|
+
"required": ["kind", "algorithm", "publicKey"],
|
|
97
|
+
"properties": {
|
|
98
|
+
"kind": { "const": "public-key" },
|
|
99
|
+
"algorithm": {
|
|
100
|
+
"enum": ["ed25519", "ecdsa-p256-sha256", "rsa-pss-sha256"]
|
|
101
|
+
},
|
|
102
|
+
"publicKey": { "type": "string", "minLength": 1 }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|