trustdex 0.9.0 → 0.11.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.
- package/README.ko.md +53 -5
- package/README.md +54 -5
- package/bin/trustdex.mjs +136 -0
- package/docs/ARCHITECTURE.ko.md +12 -0
- package/docs/ARCHITECTURE.md +12 -0
- package/docs/CLAUDE.ko.md +13 -1
- package/docs/CLAUDE.md +14 -1
- package/docs/CODEX.ko.md +15 -4
- package/docs/CODEX.md +16 -4
- package/docs/DEFENSIVE_USE_CASE.ko.md +8 -0
- package/docs/DEFENSIVE_USE_CASE.md +9 -0
- package/docs/EVALUATION.ko.md +25 -0
- package/docs/EVALUATION.md +25 -0
- package/docs/ROADMAP.ko.md +10 -4
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME_EVIDENCE.ko.md +89 -0
- package/docs/RUNTIME_EVIDENCE.md +96 -0
- package/docs/THREAT_MODEL.ko.md +9 -0
- package/docs/THREAT_MODEL.md +9 -0
- package/examples/adversarial/README.ko.md +1 -1
- package/examples/adversarial/README.md +1 -1
- package/examples/adversarial/bindings/README.ko.md +8 -0
- package/examples/adversarial/bindings/README.md +8 -0
- package/examples/adversarial/bindings/expected.json +12 -0
- package/examples/adversarial/bindings/runtime-after.json +21 -0
- package/examples/adversarial/bindings/runtime-before.json +13 -0
- package/package.json +3 -3
- package/schemas/effective-runtime-tools-diff-v1.schema.json +50 -0
- package/schemas/effective-runtime-tools-diff-v2.schema.json +51 -0
- package/schemas/effective-runtime-tools-v1.schema.json +68 -0
- package/schemas/effective-runtime-tools-v2.schema.json +98 -0
- package/schemas/runtime-tool-inventory-attestation-v1.schema.json +27 -0
- package/schemas/runtime-tool-inventory-v1.schema.json +27 -0
- package/src/bindings.mjs +4 -0
- package/src/runtime-attestation.mjs +202 -0
- package/src/runtime-evidence.mjs +392 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/effective-runtime-tools-v1.schema.json",
|
|
4
|
+
"title": "TrustDex effective runtime tool boundary v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion", "kind", "createdAt", "runtime", "capturedAt", "captureMethod",
|
|
9
|
+
"bindingDigest", "inventoryDigest", "status", "summary", "servers", "digest"
|
|
10
|
+
],
|
|
11
|
+
"properties": {
|
|
12
|
+
"schemaVersion": { "const": 1 },
|
|
13
|
+
"kind": { "const": "agent-effective-tool-boundary" },
|
|
14
|
+
"createdAt": { "type": "string", "format": "date-time" },
|
|
15
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
16
|
+
"capturedAt": { "type": "string", "format": "date-time" },
|
|
17
|
+
"captureMethod": { "enum": ["runtime-export", "runtime-adapter"] },
|
|
18
|
+
"bindingDigest": { "$ref": "#/$defs/digest" },
|
|
19
|
+
"inventoryDigest": { "$ref": "#/$defs/digest" },
|
|
20
|
+
"status": { "enum": ["matched", "restricted", "needs-review", "block"] },
|
|
21
|
+
"summary": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": [
|
|
25
|
+
"configuredServerCount", "observedServerCount", "observedToolCount", "matchedCount",
|
|
26
|
+
"reviewCount", "blockedCount", "unexpectedCount", "notObservedCount"
|
|
27
|
+
],
|
|
28
|
+
"properties": {
|
|
29
|
+
"configuredServerCount": { "$ref": "#/$defs/count" },
|
|
30
|
+
"observedServerCount": { "$ref": "#/$defs/count" },
|
|
31
|
+
"observedToolCount": { "$ref": "#/$defs/count" },
|
|
32
|
+
"matchedCount": { "$ref": "#/$defs/count" },
|
|
33
|
+
"reviewCount": { "$ref": "#/$defs/count" },
|
|
34
|
+
"blockedCount": { "$ref": "#/$defs/count" },
|
|
35
|
+
"unexpectedCount": { "$ref": "#/$defs/count" },
|
|
36
|
+
"notObservedCount": { "$ref": "#/$defs/count" }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"servers": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "$ref": "#/$defs/server" }
|
|
42
|
+
},
|
|
43
|
+
"digest": { "$ref": "#/$defs/digest" }
|
|
44
|
+
},
|
|
45
|
+
"$defs": {
|
|
46
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
47
|
+
"count": { "type": "integer", "minimum": 0 },
|
|
48
|
+
"server": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"required": [
|
|
52
|
+
"name", "configured", "observed", "toolCount", "sourceDecision", "provenanceStatus",
|
|
53
|
+
"automaticGrant", "bindingStatus", "status"
|
|
54
|
+
],
|
|
55
|
+
"properties": {
|
|
56
|
+
"name": { "type": "string", "minLength": 1 },
|
|
57
|
+
"configured": { "type": "boolean" },
|
|
58
|
+
"observed": { "type": "boolean" },
|
|
59
|
+
"toolCount": { "oneOf": [{ "$ref": "#/$defs/count" }, { "type": "null" }] },
|
|
60
|
+
"sourceDecision": { "oneOf": [{ "enum": ["allow", "ask", "block"] }, { "type": "null" }] },
|
|
61
|
+
"provenanceStatus": { "oneOf": [{ "enum": ["unknown", "verified", "mismatch"] }, { "type": "null" }] },
|
|
62
|
+
"automaticGrant": { "oneOf": [{ "enum": ["none", "scoped", "all"] }, { "type": "null" }] },
|
|
63
|
+
"bindingStatus": { "oneOf": [{ "enum": ["not-granted", "bound", "needs-review", "blocked"] }, { "type": "null" }] },
|
|
64
|
+
"status": { "enum": ["matched", "not-observed", "needs-review", "blocked", "unexpected"] }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/effective-runtime-tools-v2.schema.json",
|
|
4
|
+
"title": "TrustDex authenticated effective runtime tool boundary v2",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion", "kind", "createdAt", "runtime", "capturedAt", "captureMethod",
|
|
9
|
+
"bindingDigest", "inventoryDigest", "inventoryAuthentication", "producerKeyId",
|
|
10
|
+
"attestationDigest", "status", "summary", "servers", "digest"
|
|
11
|
+
],
|
|
12
|
+
"allOf": [
|
|
13
|
+
{
|
|
14
|
+
"if": {
|
|
15
|
+
"properties": { "inventoryAuthentication": { "const": "verified" } },
|
|
16
|
+
"required": ["inventoryAuthentication"]
|
|
17
|
+
},
|
|
18
|
+
"then": {
|
|
19
|
+
"properties": {
|
|
20
|
+
"producerKeyId": { "$ref": "#/$defs/digest" },
|
|
21
|
+
"attestationDigest": { "$ref": "#/$defs/digest" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"else": {
|
|
25
|
+
"properties": {
|
|
26
|
+
"producerKeyId": { "type": "null" },
|
|
27
|
+
"attestationDigest": { "type": "null" }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"if": {
|
|
33
|
+
"properties": { "inventoryAuthentication": { "const": "unverified" } },
|
|
34
|
+
"required": ["inventoryAuthentication"]
|
|
35
|
+
},
|
|
36
|
+
"then": {
|
|
37
|
+
"properties": { "status": { "enum": ["needs-review", "block"] } }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"properties": {
|
|
42
|
+
"schemaVersion": { "const": 2 },
|
|
43
|
+
"kind": { "const": "agent-effective-tool-boundary" },
|
|
44
|
+
"createdAt": { "type": "string", "format": "date-time" },
|
|
45
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
46
|
+
"capturedAt": { "type": "string", "format": "date-time" },
|
|
47
|
+
"captureMethod": { "enum": ["runtime-export", "runtime-adapter"] },
|
|
48
|
+
"bindingDigest": { "$ref": "#/$defs/digest" },
|
|
49
|
+
"inventoryDigest": { "$ref": "#/$defs/digest" },
|
|
50
|
+
"inventoryAuthentication": { "enum": ["unverified", "verified"] },
|
|
51
|
+
"producerKeyId": { "oneOf": [{ "$ref": "#/$defs/digest" }, { "type": "null" }] },
|
|
52
|
+
"attestationDigest": { "oneOf": [{ "$ref": "#/$defs/digest" }, { "type": "null" }] },
|
|
53
|
+
"status": { "enum": ["matched", "restricted", "needs-review", "block"] },
|
|
54
|
+
"summary": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": [
|
|
58
|
+
"configuredServerCount", "observedServerCount", "observedToolCount", "matchedCount",
|
|
59
|
+
"reviewCount", "blockedCount", "unexpectedCount", "notObservedCount"
|
|
60
|
+
],
|
|
61
|
+
"properties": {
|
|
62
|
+
"configuredServerCount": { "$ref": "#/$defs/count" },
|
|
63
|
+
"observedServerCount": { "$ref": "#/$defs/count" },
|
|
64
|
+
"observedToolCount": { "$ref": "#/$defs/count" },
|
|
65
|
+
"matchedCount": { "$ref": "#/$defs/count" },
|
|
66
|
+
"reviewCount": { "$ref": "#/$defs/count" },
|
|
67
|
+
"blockedCount": { "$ref": "#/$defs/count" },
|
|
68
|
+
"unexpectedCount": { "$ref": "#/$defs/count" },
|
|
69
|
+
"notObservedCount": { "$ref": "#/$defs/count" }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"servers": { "type": "array", "items": { "$ref": "#/$defs/server" } },
|
|
73
|
+
"digest": { "$ref": "#/$defs/digest" }
|
|
74
|
+
},
|
|
75
|
+
"$defs": {
|
|
76
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
77
|
+
"count": { "type": "integer", "minimum": 0 },
|
|
78
|
+
"server": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"required": [
|
|
82
|
+
"name", "configured", "observed", "toolCount", "sourceDecision", "provenanceStatus",
|
|
83
|
+
"automaticGrant", "bindingStatus", "status"
|
|
84
|
+
],
|
|
85
|
+
"properties": {
|
|
86
|
+
"name": { "type": "string", "minLength": 1 },
|
|
87
|
+
"configured": { "type": "boolean" },
|
|
88
|
+
"observed": { "type": "boolean" },
|
|
89
|
+
"toolCount": { "oneOf": [{ "$ref": "#/$defs/count" }, { "type": "null" }] },
|
|
90
|
+
"sourceDecision": { "oneOf": [{ "enum": ["allow", "ask", "block"] }, { "type": "null" }] },
|
|
91
|
+
"provenanceStatus": { "oneOf": [{ "enum": ["unknown", "verified", "mismatch"] }, { "type": "null" }] },
|
|
92
|
+
"automaticGrant": { "oneOf": [{ "enum": ["none", "scoped", "all"] }, { "type": "null" }] },
|
|
93
|
+
"bindingStatus": { "oneOf": [{ "enum": ["not-granted", "bound", "needs-review", "blocked"] }, { "type": "null" }] },
|
|
94
|
+
"status": { "enum": ["matched", "not-observed", "needs-review", "blocked", "unexpected"] }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/runtime-tool-inventory-attestation-v1.schema.json",
|
|
4
|
+
"title": "TrustDex signed runtime tool inventory attestation v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion", "kind", "algorithm", "inventoryDigest", "runtime", "capturedAt",
|
|
9
|
+
"captureMethod", "producerKeyId", "createdAt", "expiresAt", "signature"
|
|
10
|
+
],
|
|
11
|
+
"properties": {
|
|
12
|
+
"schemaVersion": { "const": 1 },
|
|
13
|
+
"kind": { "const": "agent-runtime-tool-inventory-attestation" },
|
|
14
|
+
"algorithm": { "const": "ed25519" },
|
|
15
|
+
"inventoryDigest": { "$ref": "#/$defs/digest" },
|
|
16
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
17
|
+
"capturedAt": { "type": "string", "format": "date-time" },
|
|
18
|
+
"captureMethod": { "enum": ["runtime-export", "runtime-adapter"] },
|
|
19
|
+
"producerKeyId": { "$ref": "#/$defs/digest" },
|
|
20
|
+
"createdAt": { "type": "string", "format": "date-time" },
|
|
21
|
+
"expiresAt": { "type": "string", "format": "date-time" },
|
|
22
|
+
"signature": { "type": "string", "pattern": "^[A-Za-z0-9+/]{86}==$" }
|
|
23
|
+
},
|
|
24
|
+
"$defs": {
|
|
25
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/runtime-tool-inventory-v1.schema.json",
|
|
4
|
+
"title": "TrustDex value-free runtime tool inventory v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "kind", "runtime", "capturedAt", "captureMethod", "servers"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"kind": { "const": "agent-runtime-tool-inventory" },
|
|
11
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
12
|
+
"capturedAt": { "type": "string", "format": "date-time" },
|
|
13
|
+
"captureMethod": { "enum": ["runtime-export", "runtime-adapter"] },
|
|
14
|
+
"servers": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["name", "toolCount"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"name": { "type": "string", "minLength": 1 },
|
|
22
|
+
"toolCount": { "type": "integer", "minimum": 0 }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/bindings.mjs
CHANGED
|
@@ -214,6 +214,10 @@ function validateSnapshot(snapshot) {
|
|
|
214
214
|
return snapshot;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
export function validateToolPermissionBindingSnapshot(snapshot) {
|
|
218
|
+
return validateSnapshot(snapshot);
|
|
219
|
+
}
|
|
220
|
+
|
|
217
221
|
function createSummary(bindings, input) {
|
|
218
222
|
return {
|
|
219
223
|
serverCount: bindings.length,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { canonicalJson, sha256Json } from './canonical.mjs';
|
|
4
|
+
import {
|
|
5
|
+
normalizeRuntimeToolInventory,
|
|
6
|
+
runtimeToolInventoryDigest
|
|
7
|
+
} from './runtime-evidence.mjs';
|
|
8
|
+
|
|
9
|
+
const KIND = 'agent-runtime-tool-inventory-attestation';
|
|
10
|
+
const ALGORITHM = 'ed25519';
|
|
11
|
+
const MAX_TTL_SECONDS = 86_400;
|
|
12
|
+
const MAX_FUTURE_SKEW_SECONDS = 300;
|
|
13
|
+
const FIELDS = new Set([
|
|
14
|
+
'schemaVersion', 'kind', 'algorithm', 'inventoryDigest', 'runtime', 'capturedAt',
|
|
15
|
+
'captureMethod', 'producerKeyId', 'createdAt', 'expiresAt', 'signature'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function requireObject(value, message) {
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(message);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireExactKeys(value, expected, message) {
|
|
24
|
+
const keys = Object.keys(requireObject(value, message));
|
|
25
|
+
if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) throw new Error(message);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requireDigest(value, field) {
|
|
30
|
+
if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value)) {
|
|
31
|
+
throw new Error(`Invalid runtime inventory attestation digest: ${field}`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireTimestamp(value, field) {
|
|
37
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value)) ||
|
|
38
|
+
new Date(value).toISOString() !== value) {
|
|
39
|
+
throw new Error(`Invalid runtime inventory attestation timestamp: ${field}`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function publicKeyId(publicKey) {
|
|
45
|
+
const key = publicKey?.type === 'public' ? publicKey : crypto.createPublicKey(publicKey);
|
|
46
|
+
if (key.asymmetricKeyType !== 'ed25519') {
|
|
47
|
+
throw new Error('Runtime inventory attestations require an Ed25519 key.');
|
|
48
|
+
}
|
|
49
|
+
const der = key.export({ type: 'spki', format: 'der' });
|
|
50
|
+
return crypto.createHash('sha256').update(der).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function payload(record) {
|
|
54
|
+
return {
|
|
55
|
+
schemaVersion: record.schemaVersion,
|
|
56
|
+
kind: record.kind,
|
|
57
|
+
algorithm: record.algorithm,
|
|
58
|
+
inventoryDigest: record.inventoryDigest,
|
|
59
|
+
runtime: record.runtime,
|
|
60
|
+
capturedAt: record.capturedAt,
|
|
61
|
+
captureMethod: record.captureMethod,
|
|
62
|
+
producerKeyId: record.producerKeyId,
|
|
63
|
+
createdAt: record.createdAt,
|
|
64
|
+
expiresAt: record.expiresAt
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateRuntimeInventoryAttestation(record) {
|
|
69
|
+
requireExactKeys(record, FIELDS, 'Invalid runtime inventory attestation.');
|
|
70
|
+
if (record.schemaVersion !== 1 || record.kind !== KIND || record.algorithm !== ALGORITHM) {
|
|
71
|
+
throw new Error('Unsupported runtime inventory attestation schema.');
|
|
72
|
+
}
|
|
73
|
+
requireDigest(record.inventoryDigest, 'inventoryDigest');
|
|
74
|
+
requireDigest(record.producerKeyId, 'producerKeyId');
|
|
75
|
+
if (!['claude', 'codex'].includes(record.runtime)) throw new Error('Invalid attested runtime.');
|
|
76
|
+
if (!['runtime-export', 'runtime-adapter'].includes(record.captureMethod)) {
|
|
77
|
+
throw new Error('Invalid attested capture method.');
|
|
78
|
+
}
|
|
79
|
+
requireTimestamp(record.capturedAt, 'capturedAt');
|
|
80
|
+
requireTimestamp(record.createdAt, 'createdAt');
|
|
81
|
+
requireTimestamp(record.expiresAt, 'expiresAt');
|
|
82
|
+
if (Date.parse(record.expiresAt) <= Date.parse(record.createdAt)) {
|
|
83
|
+
throw new Error('Runtime inventory attestation expiry must follow creation time.');
|
|
84
|
+
}
|
|
85
|
+
if (Date.parse(record.expiresAt) - Date.parse(record.createdAt) > MAX_TTL_SECONDS * 1000) {
|
|
86
|
+
throw new Error('Runtime inventory attestation lifetime exceeds 24 hours.');
|
|
87
|
+
}
|
|
88
|
+
if (Date.parse(record.capturedAt) > Date.parse(record.createdAt) + MAX_FUTURE_SKEW_SECONDS * 1000) {
|
|
89
|
+
throw new Error('Runtime inventory capture cannot postdate attestation creation by more than five minutes.');
|
|
90
|
+
}
|
|
91
|
+
if (typeof record.signature !== 'string' ||
|
|
92
|
+
!/^[A-Za-z0-9+/]{86}==$/.test(record.signature) ||
|
|
93
|
+
Buffer.from(record.signature, 'base64').toString('base64') !== record.signature) {
|
|
94
|
+
throw new Error('Invalid runtime inventory attestation signature.');
|
|
95
|
+
}
|
|
96
|
+
return record;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createRuntimeInventoryAttestation(inventoryInput, privateKeyPem, options = {}) {
|
|
100
|
+
const inventory = normalizeRuntimeToolInventory(inventoryInput);
|
|
101
|
+
const ttlSeconds = options.ttlSeconds === undefined ? 300 : options.ttlSeconds;
|
|
102
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > MAX_TTL_SECONDS) {
|
|
103
|
+
throw new Error('Runtime inventory attestation TTL must be an integer from 1 to 86400 seconds.');
|
|
104
|
+
}
|
|
105
|
+
const createdAt = options.createdAt || new Date().toISOString();
|
|
106
|
+
requireTimestamp(createdAt, 'createdAt');
|
|
107
|
+
if (Date.parse(inventory.capturedAt) > Date.parse(createdAt) + MAX_FUTURE_SKEW_SECONDS * 1000) {
|
|
108
|
+
throw new Error('Runtime inventory capture cannot postdate attestation creation by more than five minutes.');
|
|
109
|
+
}
|
|
110
|
+
const expiresAt = new Date(Date.parse(createdAt) + ttlSeconds * 1000).toISOString();
|
|
111
|
+
const publicKey = crypto.createPublicKey(privateKeyPem);
|
|
112
|
+
const record = {
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
kind: KIND,
|
|
115
|
+
algorithm: ALGORITHM,
|
|
116
|
+
inventoryDigest: runtimeToolInventoryDigest(inventory),
|
|
117
|
+
runtime: inventory.runtime,
|
|
118
|
+
capturedAt: inventory.capturedAt,
|
|
119
|
+
captureMethod: inventory.captureMethod,
|
|
120
|
+
producerKeyId: publicKeyId(publicKey),
|
|
121
|
+
createdAt,
|
|
122
|
+
expiresAt
|
|
123
|
+
};
|
|
124
|
+
const signature = crypto.sign(
|
|
125
|
+
null,
|
|
126
|
+
Buffer.from(canonicalJson(payload(record))),
|
|
127
|
+
privateKeyPem
|
|
128
|
+
);
|
|
129
|
+
return { ...record, signature: signature.toString('base64') };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function invalid(reason) {
|
|
133
|
+
return { valid: false, reason, producerKeyId: null, inventoryDigest: null, attestationDigest: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function verifyRuntimeInventoryAttestation(inventoryInput, recordInput, publicKeyPem, options = {}) {
|
|
137
|
+
let inventory;
|
|
138
|
+
let record;
|
|
139
|
+
try {
|
|
140
|
+
inventory = normalizeRuntimeToolInventory(inventoryInput);
|
|
141
|
+
record = validateRuntimeInventoryAttestation(recordInput);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return invalid(error.message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let keyId;
|
|
147
|
+
try { keyId = publicKeyId(publicKeyPem); } catch {
|
|
148
|
+
return invalid('Invalid runtime inventory attestation public key.');
|
|
149
|
+
}
|
|
150
|
+
if (record.producerKeyId !== keyId) return invalid('Runtime inventory producer key does not match.');
|
|
151
|
+
const inventoryDigest = runtimeToolInventoryDigest(inventory);
|
|
152
|
+
if (record.inventoryDigest !== inventoryDigest ||
|
|
153
|
+
record.runtime !== inventory.runtime ||
|
|
154
|
+
record.capturedAt !== inventory.capturedAt ||
|
|
155
|
+
record.captureMethod !== inventory.captureMethod) {
|
|
156
|
+
return invalid('Runtime inventory does not match its attestation.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const nowMs = options.now === undefined ? Date.now() : Date.parse(options.now);
|
|
160
|
+
if (!Number.isFinite(nowMs)) return invalid('Invalid runtime inventory verification time.');
|
|
161
|
+
if (nowMs >= Date.parse(record.expiresAt)) return invalid('Runtime inventory attestation expired.');
|
|
162
|
+
const futureSkewSeconds = options.futureSkewSeconds === undefined
|
|
163
|
+
? MAX_FUTURE_SKEW_SECONDS
|
|
164
|
+
: options.futureSkewSeconds;
|
|
165
|
+
if (!Number.isSafeInteger(futureSkewSeconds) || futureSkewSeconds < 0) {
|
|
166
|
+
return invalid('Invalid runtime inventory future-skew limit.');
|
|
167
|
+
}
|
|
168
|
+
if (Date.parse(record.createdAt) > nowMs + futureSkewSeconds * 1000) {
|
|
169
|
+
return invalid('Runtime inventory attestation creation time is too far in the future.');
|
|
170
|
+
}
|
|
171
|
+
if (Date.parse(inventory.capturedAt) > nowMs + futureSkewSeconds * 1000) {
|
|
172
|
+
return invalid('Runtime inventory capture time is too far in the future.');
|
|
173
|
+
}
|
|
174
|
+
if (options.maxAgeSeconds !== undefined) {
|
|
175
|
+
if (!Number.isSafeInteger(options.maxAgeSeconds) || options.maxAgeSeconds < 0) {
|
|
176
|
+
return invalid('Invalid runtime inventory maximum age.');
|
|
177
|
+
}
|
|
178
|
+
if (nowMs - Date.parse(inventory.capturedAt) > options.maxAgeSeconds * 1000) {
|
|
179
|
+
return invalid('Runtime inventory capture is too old.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let valid = false;
|
|
184
|
+
try {
|
|
185
|
+
valid = crypto.verify(
|
|
186
|
+
null,
|
|
187
|
+
Buffer.from(canonicalJson(payload(record))),
|
|
188
|
+
publicKeyPem,
|
|
189
|
+
Buffer.from(record.signature, 'base64')
|
|
190
|
+
);
|
|
191
|
+
} catch {
|
|
192
|
+
return invalid('Runtime inventory attestation signature verification failed.');
|
|
193
|
+
}
|
|
194
|
+
if (!valid) return invalid('Runtime inventory attestation signature verification failed.');
|
|
195
|
+
return {
|
|
196
|
+
valid: true,
|
|
197
|
+
reason: null,
|
|
198
|
+
producerKeyId: keyId,
|
|
199
|
+
inventoryDigest,
|
|
200
|
+
attestationDigest: sha256Json(record)
|
|
201
|
+
};
|
|
202
|
+
}
|