witnora 0.10.0 → 0.10.2

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 (74) hide show
  1. package/README.md +54 -9
  2. package/dist/assurance-contract.js +312 -0
  3. package/dist/assurance-loop-demo.js +245 -0
  4. package/dist/authorization-state-machine.js +35 -0
  5. package/dist/authorization-v02.js +60 -0
  6. package/dist/canonical-v02.js +197 -0
  7. package/dist/canonical.js +31 -0
  8. package/dist/cli.js +118 -0
  9. package/dist/command-help.js +76 -0
  10. package/dist/control-plane.js +3 -3
  11. package/dist/credentials.js +1 -1
  12. package/dist/design-partner-pilot.js +402 -0
  13. package/dist/design-partner-v02.js +449 -0
  14. package/dist/evidence-v02.js +118 -0
  15. package/dist/generic-eval.js +160 -0
  16. package/dist/github-design-partner-v02.js +399 -0
  17. package/dist/github-live-v02.js +45 -0
  18. package/dist/guided-setup.js +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/offline-verifier.js +375 -0
  21. package/dist/onboard.js +132 -0
  22. package/dist/onboarding-templates.js +3 -3
  23. package/dist/outcome-evaluator.js +51 -0
  24. package/dist/privacy-manifest.js +273 -0
  25. package/dist/probe-protocol-v02.js +101 -0
  26. package/dist/sandbox.js +1 -1
  27. package/dist/trust-v02.js +111 -0
  28. package/dist/try.js +1 -1
  29. package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
  30. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
  31. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
  32. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
  33. package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
  34. package/dist/vendor/witnora-probe/canonical.js +39 -0
  35. package/dist/vendor/witnora-probe/cli.d.ts +2 -0
  36. package/dist/vendor/witnora-probe/cli.js +36 -0
  37. package/dist/vendor/witnora-probe/config.d.ts +2 -0
  38. package/dist/vendor/witnora-probe/config.js +63 -0
  39. package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
  40. package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
  41. package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
  42. package/dist/vendor/witnora-probe/crypto.js +14 -0
  43. package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
  44. package/dist/vendor/witnora-probe/emulator.js +164 -0
  45. package/dist/vendor/witnora-probe/index.d.ts +11 -0
  46. package/dist/vendor/witnora-probe/index.js +11 -0
  47. package/dist/vendor/witnora-probe/logger.d.ts +3 -0
  48. package/dist/vendor/witnora-probe/logger.js +26 -0
  49. package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
  50. package/dist/vendor/witnora-probe/metrics.js +31 -0
  51. package/dist/vendor/witnora-probe/probe.d.ts +22 -0
  52. package/dist/vendor/witnora-probe/probe.js +208 -0
  53. package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
  54. package/dist/vendor/witnora-probe/request-verifier.js +84 -0
  55. package/dist/vendor/witnora-probe/server.d.ts +9 -0
  56. package/dist/vendor/witnora-probe/server.js +85 -0
  57. package/dist/vendor/witnora-probe/storage.d.ts +16 -0
  58. package/dist/vendor/witnora-probe/storage.js +114 -0
  59. package/dist/vendor/witnora-probe/types.d.ts +126 -0
  60. package/dist/vendor/witnora-probe/types.js +2 -0
  61. package/dist/vendor/witnora-verifier-python/README.md +43 -0
  62. package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
  63. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
  64. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
  65. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
  66. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
  67. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
  68. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
  69. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
  70. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
  71. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
  72. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
  73. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
  74. package/package.json +4 -4
@@ -0,0 +1,337 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import re
5
+ from collections.abc import Iterable, Mapping
6
+ from dataclasses import dataclass
7
+ from datetime import UTC, datetime
8
+ from typing import Any
9
+
10
+ from cryptography.hazmat.primitives import serialization
11
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
12
+
13
+ from .canonical import sha256_canonical
14
+ from .crypto import verify_document_signature
15
+ from .errors import VerificationError, require
16
+
17
+ TRUST_ROOT_VERSION = "witnora.trust_root.v0.2"
18
+ KEY_CERTIFICATE_VERSION = "witnora.key_certificate.v0.2"
19
+ REVOCATION_STATEMENT_VERSION = "witnora.key_revocation_statement.v0.2"
20
+ REVOCATION_JOURNAL_VERSION = "witnora.revocation_journal.v0.2"
21
+
22
+ CERTIFIED_ROLES = {
23
+ "source_collector",
24
+ "gateway_runtime",
25
+ "outcome_probe",
26
+ "resource_scope_evaluator",
27
+ "server_attestor",
28
+ "reviewer",
29
+ }
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class PinnedTrustStore:
34
+ """Caller-owned roots. Packet-carried roots are intentionally not accepted."""
35
+
36
+ roots: Mapping[str, Mapping[str, Any]]
37
+
38
+ @classmethod
39
+ def from_roots(cls, roots: Iterable[Mapping[str, Any]]) -> PinnedTrustStore:
40
+ indexed: dict[str, Mapping[str, Any]] = {}
41
+ for root in roots:
42
+ _verify_root_shape(root)
43
+ root_id = str(root["rootId"])
44
+ require(
45
+ root_id not in indexed,
46
+ "DUPLICATE_ROOT_ID",
47
+ f"Root {root_id} appears more than once.",
48
+ )
49
+ indexed[root_id] = dict(root)
50
+ require(
51
+ bool(indexed),
52
+ "PINNED_ROOT_REQUIRED",
53
+ "At least one out-of-band pinned root is required.",
54
+ )
55
+ return cls(indexed)
56
+
57
+ def get(self, root_id: str) -> Mapping[str, Any]:
58
+ root = self.roots.get(root_id)
59
+ require(
60
+ root is not None,
61
+ "UNPINNED_ROOT",
62
+ f"Root {root_id} is not pinned by the verifier caller.",
63
+ )
64
+ return root
65
+
66
+
67
+ def parse_timestamp(value: Any, field: str) -> datetime:
68
+ require(
69
+ isinstance(value, str), "INVALID_TIMESTAMP", f"{field} must be an RFC 3339 UTC timestamp."
70
+ )
71
+ require(
72
+ bool(re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z", value)),
73
+ "INVALID_TIMESTAMP",
74
+ f"{field} must use UTC Z notation with at most millisecond precision.",
75
+ )
76
+ try:
77
+ parsed = datetime.fromisoformat(value[:-1] + "+00:00")
78
+ except ValueError as exc:
79
+ raise VerificationError("INVALID_TIMESTAMP", f"{field} is not a real timestamp.") from exc
80
+ return parsed.astimezone(UTC)
81
+
82
+
83
+ def root_fingerprint(public_key_pem: str) -> str:
84
+ key = _load_ed25519_public_key(public_key_pem)
85
+ der = key.public_bytes(
86
+ serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
87
+ )
88
+ return hashlib.sha256(der).hexdigest()
89
+
90
+
91
+ def verify_key_certificate(
92
+ certificate: Mapping[str, Any],
93
+ trust_store: PinnedTrustStore,
94
+ *,
95
+ expected_role: str,
96
+ tenant_ref: str,
97
+ project_ref: str,
98
+ environment: str,
99
+ at: datetime | str,
100
+ revocation_journal: Mapping[str, Any] | None = None,
101
+ ) -> Mapping[str, Any]:
102
+ require(
103
+ certificate.get("protocolVersion") == KEY_CERTIFICATE_VERSION,
104
+ "CERTIFICATE_VERSION_UNSUPPORTED",
105
+ "Key certificate is not v0.2.",
106
+ )
107
+ role = str(certificate.get("role", ""))
108
+ require(
109
+ role in CERTIFIED_ROLES, "CERTIFICATE_ROLE_UNKNOWN", f"Unknown certificate role {role!r}."
110
+ )
111
+ require(
112
+ role == expected_role,
113
+ "CERTIFICATE_ROLE_MISMATCH",
114
+ f"Expected role {expected_role}, received {role}.",
115
+ )
116
+ root = trust_store.get(str(certificate.get("issuerRootId", "")))
117
+ _verify_root_shape(root)
118
+ require(
119
+ role in root.get("allowedRoles", []),
120
+ "ROOT_ROLE_DENIED",
121
+ "Pinned root does not authorize this role.",
122
+ )
123
+ require(
124
+ certificate.get("tenantRef") == tenant_ref
125
+ and certificate.get("projectRef") == project_ref
126
+ and certificate.get("environment") == environment,
127
+ "CERTIFICATE_SCOPE_MISMATCH",
128
+ "Certificate tenant, project, or environment scope does not match.",
129
+ )
130
+ event_at = parse_timestamp(at, "eventAt") if isinstance(at, str) else _utc(at)
131
+ not_before = parse_timestamp(certificate.get("notBefore"), "certificate.notBefore")
132
+ not_after = parse_timestamp(certificate.get("notAfter"), "certificate.notAfter")
133
+ require(
134
+ not_before <= event_at <= not_after,
135
+ "CERTIFICATE_INACTIVE",
136
+ "Certificate is outside its validity interval.",
137
+ )
138
+ root_not_before = parse_timestamp(root.get("notBefore"), "root.notBefore")
139
+ root_not_after = parse_timestamp(root.get("notAfter"), "root.notAfter")
140
+ require(
141
+ root_not_before <= event_at <= root_not_after,
142
+ "ROOT_INACTIVE",
143
+ "Pinned root is outside its validity interval.",
144
+ )
145
+
146
+ body = {
147
+ key: value
148
+ for key, value in certificate.items()
149
+ if key not in {"certificateDigest", "signature"}
150
+ }
151
+ require(
152
+ certificate.get("certificateDigest") == sha256_canonical(body),
153
+ "CERTIFICATE_DIGEST_MISMATCH",
154
+ "Certificate digest does not match its canonical body.",
155
+ )
156
+ signed = {**body, "certificateDigest": certificate["certificateDigest"]}
157
+ verify_document_signature(
158
+ signed, str(certificate.get("signature", "")), str(root.get("publicKeyPem", ""))
159
+ )
160
+ _load_ed25519_public_key(str(certificate.get("publicKeyPem", "")))
161
+
162
+ if revocation_journal is not None:
163
+ verify_revocation_journal(revocation_journal, trust_store)
164
+ assert_not_revoked(certificate, revocation_journal, event_at)
165
+ return root
166
+
167
+
168
+ def verify_revocation_journal(journal: Mapping[str, Any], trust_store: PinnedTrustStore) -> None:
169
+ require(
170
+ journal.get("protocolVersion") == REVOCATION_JOURNAL_VERSION,
171
+ "REVOCATION_JOURNAL_VERSION_UNSUPPORTED",
172
+ "Revocation journal is not v0.2.",
173
+ )
174
+ root_id = str(journal.get("rootId", ""))
175
+ root = trust_store.get(root_id)
176
+ statements = journal.get("statements")
177
+ require(
178
+ isinstance(statements, list),
179
+ "REVOCATION_JOURNAL_INVALID",
180
+ "Revocation statements must be an array.",
181
+ )
182
+
183
+ previous_checkpoint: str | None = None
184
+ seen_subjects: set[tuple[int, str]] = set()
185
+ for index, statement in enumerate(statements):
186
+ require(
187
+ isinstance(statement, dict),
188
+ "REVOCATION_STATEMENT_INVALID",
189
+ "Revocation statement must be an object.",
190
+ )
191
+ require(
192
+ statement.get("protocolVersion") == REVOCATION_STATEMENT_VERSION,
193
+ "REVOCATION_STATEMENT_VERSION_UNSUPPORTED",
194
+ "Revocation statement is not v0.2.",
195
+ )
196
+ require(
197
+ statement.get("sequence") == index + 1,
198
+ "REVOCATION_SEQUENCE_INVALID",
199
+ f"Expected revocation sequence {index + 1}.",
200
+ )
201
+ require(
202
+ statement.get("previousJournalDigest") == previous_checkpoint,
203
+ "REVOCATION_CHAIN_BROKEN",
204
+ "Revocation journal continuity is invalid.",
205
+ )
206
+ require(
207
+ statement.get("issuerRootId") == root_id,
208
+ "REVOCATION_ISSUER_MISMATCH",
209
+ "Revocation statement issuer differs from the journal root.",
210
+ )
211
+ require(
212
+ isinstance(statement.get("retroactive"), bool),
213
+ "REVOCATION_RETROACTIVE_INVALID",
214
+ "retroactive must be boolean.",
215
+ )
216
+ parse_timestamp(statement.get("effectiveAt"), "revocation.effectiveAt")
217
+ body = {
218
+ key: value
219
+ for key, value in statement.items()
220
+ if key not in {"statementDigest", "signature"}
221
+ }
222
+ require(
223
+ statement.get("statementDigest") == sha256_canonical(body),
224
+ "REVOCATION_DIGEST_MISMATCH",
225
+ "Revocation statement digest is invalid.",
226
+ )
227
+ signed = {**body, "statementDigest": statement["statementDigest"]}
228
+ verify_document_signature(
229
+ signed, str(statement.get("signature", "")), str(root.get("publicKeyPem", ""))
230
+ )
231
+ subject_key = (int(statement["sequence"]), str(statement.get("revokedId", "")))
232
+ require(
233
+ subject_key not in seen_subjects,
234
+ "REVOCATION_DUPLICATE_ENTRY",
235
+ "Duplicate revocation entry detected.",
236
+ )
237
+ seen_subjects.add(subject_key)
238
+ previous_checkpoint = sha256_canonical(statements[: index + 1])
239
+
240
+ expected_checkpoint = (
241
+ previous_checkpoint if previous_checkpoint is not None else sha256_canonical([])
242
+ )
243
+ require(
244
+ journal.get("checkpointDigest") == expected_checkpoint,
245
+ "REVOCATION_CHECKPOINT_MISMATCH",
246
+ "Revocation checkpoint digest is invalid.",
247
+ )
248
+
249
+
250
+ def assert_not_revoked(
251
+ certificate: Mapping[str, Any], journal: Mapping[str, Any], event_at: datetime
252
+ ) -> None:
253
+ for statement in journal.get("statements", []):
254
+ if statement.get("revokedId") not in {
255
+ certificate.get("certificateId"),
256
+ certificate.get("subjectKeyId"),
257
+ }:
258
+ continue
259
+ effective_at = parse_timestamp(statement.get("effectiveAt"), "revocation.effectiveAt")
260
+ if statement.get("retroactive") is True or event_at >= effective_at:
261
+ raise VerificationError(
262
+ "CERTIFICATE_REVOKED", "Certificate was revoked for the event time."
263
+ )
264
+
265
+
266
+ def assert_revocation_journal_extends(
267
+ packet_journal: Mapping[str, Any], latest_journal: Mapping[str, Any]
268
+ ) -> None:
269
+ require(
270
+ packet_journal.get("rootId") == latest_journal.get("rootId"),
271
+ "REVOCATION_ROOT_MISMATCH",
272
+ "Packet and latest revocation journals use different roots.",
273
+ )
274
+ packet_statements = packet_journal.get("statements")
275
+ latest_statements = latest_journal.get("statements")
276
+ require(
277
+ isinstance(packet_statements, list) and isinstance(latest_statements, list),
278
+ "REVOCATION_JOURNAL_INVALID",
279
+ "Revocation statements must be arrays.",
280
+ )
281
+ require(
282
+ len(latest_statements) >= len(packet_statements)
283
+ and latest_statements[: len(packet_statements)] == packet_statements,
284
+ "REVOCATION_ROLLBACK_DETECTED",
285
+ "Latest revocation journal does not extend the packet checkpoint.",
286
+ )
287
+
288
+
289
+ def _verify_root_shape(root: Mapping[str, Any]) -> None:
290
+ require(
291
+ root.get("protocolVersion") == TRUST_ROOT_VERSION,
292
+ "ROOT_VERSION_UNSUPPORTED",
293
+ "Pinned root is not v0.2.",
294
+ )
295
+ require(
296
+ root.get("algorithm") == "Ed25519",
297
+ "ROOT_ALGORITHM_UNSUPPORTED",
298
+ "Pinned root must use Ed25519.",
299
+ )
300
+ require(
301
+ isinstance(root.get("rootId"), str) and bool(root["rootId"]),
302
+ "ROOT_ID_MISSING",
303
+ "Pinned root ID is required.",
304
+ )
305
+ roles = root.get("allowedRoles")
306
+ require(
307
+ isinstance(roles, list) and all(role in CERTIFIED_ROLES for role in roles),
308
+ "ROOT_ROLES_INVALID",
309
+ "Pinned root roles are invalid.",
310
+ )
311
+ fingerprint = root_fingerprint(str(root.get("publicKeyPem", "")))
312
+ require(
313
+ root.get("rootFingerprint") == fingerprint,
314
+ "ROOT_FINGERPRINT_MISMATCH",
315
+ "Pinned root fingerprint does not match its public key.",
316
+ )
317
+ not_before = parse_timestamp(root.get("notBefore"), "root.notBefore")
318
+ not_after = parse_timestamp(root.get("notAfter"), "root.notAfter")
319
+ require(
320
+ not_after > not_before,
321
+ "ROOT_VALIDITY_INVALID",
322
+ "Pinned root validity interval is invalid.",
323
+ )
324
+
325
+
326
+ def _load_ed25519_public_key(public_key_pem: str) -> Ed25519PublicKey:
327
+ try:
328
+ key = serialization.load_pem_public_key(public_key_pem.encode("ascii"))
329
+ except (ValueError, TypeError, UnicodeError) as exc:
330
+ raise VerificationError("INVALID_PUBLIC_KEY", "Public key PEM is invalid.") from exc
331
+ require(isinstance(key, Ed25519PublicKey), "INVALID_PUBLIC_KEY", "Public key must be Ed25519.")
332
+ return key
333
+
334
+
335
+ def _utc(value: datetime) -> datetime:
336
+ require(value.tzinfo is not None, "INVALID_TIMESTAMP", "Datetime must be timezone-aware.")
337
+ return value.astimezone(UTC)
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://witnora.com/",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/Kakarottoooo/agentcert.git",
10
+ "url": "git+https://github.com/Kakarottoooo/witnora.git",
11
11
  "directory": "packages/witnora-cli"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/Kakarottoooo/agentcert/issues"
14
+ "url": "https://github.com/Kakarottoooo/witnora/issues"
15
15
  },
16
16
  "publishConfig": {
17
17
  "access": "public",
@@ -51,7 +51,7 @@
51
51
  "package.json"
52
52
  ],
53
53
  "scripts": {
54
- "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.onegent-vendor.json && tsc -p tsconfig.sdk-vendor.json",
54
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.onegent-vendor.json && tsc -p tsconfig.sdk-vendor.json && tsc -p tsconfig.probe-vendor.json && node scripts/vendor-design-partner-assets.mjs",
55
55
  "prepack": "npm run build",
56
56
  "test": "vitest run tests",
57
57
  "demo:report": "node dist/cli.js report --mcpbench ../../examples/reports/passing/results.json --out .witnora/demo --subject demo-agent"