witnora 0.9.3 → 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 (77) hide show
  1. package/README.md +76 -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/bundle.js +22 -0
  7. package/dist/canonical-v02.js +197 -0
  8. package/dist/canonical.js +31 -0
  9. package/dist/cli.js +132 -0
  10. package/dist/command-help.js +81 -0
  11. package/dist/control-plane.js +12 -3
  12. package/dist/credentials.js +1 -1
  13. package/dist/design-partner-pilot.js +402 -0
  14. package/dist/design-partner-v02.js +449 -0
  15. package/dist/evidence-v02.js +118 -0
  16. package/dist/generic-eval.js +160 -0
  17. package/dist/github-design-partner-v02.js +399 -0
  18. package/dist/github-live-v02.js +45 -0
  19. package/dist/guided-setup.js +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/offline-verifier.js +375 -0
  22. package/dist/onboard.js +132 -0
  23. package/dist/onboarding-templates.js +49 -6
  24. package/dist/outcome-evaluator.js +51 -0
  25. package/dist/privacy-manifest.js +273 -0
  26. package/dist/probe-protocol-v02.js +101 -0
  27. package/dist/runtime-context.js +75 -0
  28. package/dist/sandbox.js +1 -1
  29. package/dist/schema-validator.js +141 -3
  30. package/dist/trust-v02.js +111 -0
  31. package/dist/try.js +1 -1
  32. package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
  33. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
  34. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
  35. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
  36. package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
  37. package/dist/vendor/witnora-probe/canonical.js +39 -0
  38. package/dist/vendor/witnora-probe/cli.d.ts +2 -0
  39. package/dist/vendor/witnora-probe/cli.js +36 -0
  40. package/dist/vendor/witnora-probe/config.d.ts +2 -0
  41. package/dist/vendor/witnora-probe/config.js +63 -0
  42. package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
  43. package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
  44. package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
  45. package/dist/vendor/witnora-probe/crypto.js +14 -0
  46. package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
  47. package/dist/vendor/witnora-probe/emulator.js +164 -0
  48. package/dist/vendor/witnora-probe/index.d.ts +11 -0
  49. package/dist/vendor/witnora-probe/index.js +11 -0
  50. package/dist/vendor/witnora-probe/logger.d.ts +3 -0
  51. package/dist/vendor/witnora-probe/logger.js +26 -0
  52. package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
  53. package/dist/vendor/witnora-probe/metrics.js +31 -0
  54. package/dist/vendor/witnora-probe/probe.d.ts +22 -0
  55. package/dist/vendor/witnora-probe/probe.js +208 -0
  56. package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
  57. package/dist/vendor/witnora-probe/request-verifier.js +84 -0
  58. package/dist/vendor/witnora-probe/server.d.ts +9 -0
  59. package/dist/vendor/witnora-probe/server.js +85 -0
  60. package/dist/vendor/witnora-probe/storage.d.ts +16 -0
  61. package/dist/vendor/witnora-probe/storage.js +114 -0
  62. package/dist/vendor/witnora-probe/types.d.ts +126 -0
  63. package/dist/vendor/witnora-probe/types.js +2 -0
  64. package/dist/vendor/witnora-verifier-python/README.md +43 -0
  65. package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
  66. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
  67. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
  68. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
  69. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
  70. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
  71. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
  72. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
  73. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
  74. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
  75. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
  76. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
  77. package/package.json +5 -5
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from datetime import UTC, datetime
5
+ from typing import Any
6
+
7
+ from .canonical import sha256_canonical
8
+ from .crypto import verify_document_signature
9
+ from .errors import VerificationError, require
10
+ from .probe import verify_probe_exchange
11
+ from .trust import (
12
+ PinnedTrustStore,
13
+ assert_revocation_journal_extends,
14
+ parse_timestamp,
15
+ verify_key_certificate,
16
+ verify_revocation_journal,
17
+ )
18
+
19
+ EVIDENCE_VERSION = "witnora.verifiable_evidence_packet.v0.2"
20
+
21
+
22
+ def verify_evidence_packet(
23
+ packet: Mapping[str, Any],
24
+ pinned_root: Mapping[str, Any],
25
+ *,
26
+ at: datetime | None = None,
27
+ latest_revocation_journal: Mapping[str, Any] | None = None,
28
+ ) -> dict[str, Any]:
29
+ failures: list[str] = []
30
+ required = (
31
+ "approval",
32
+ "grant",
33
+ "executionAttempt",
34
+ "reconciliation",
35
+ "probeRequest",
36
+ "probeAttestation",
37
+ "trust",
38
+ "separation",
39
+ "privacy",
40
+ "evidenceStrength",
41
+ "gatewaySignature",
42
+ )
43
+ if packet.get("protocolVersion") != EVIDENCE_VERSION or any(
44
+ key not in packet for key in required
45
+ ):
46
+ return _failed("Evidence packet v0.2 is structurally incomplete.")
47
+
48
+ cryptographically_valid = True
49
+ trusted_by_pinned_root = True
50
+ try:
51
+ store = PinnedTrustStore.from_roots([pinned_root])
52
+ trust = packet["trust"]
53
+ require(
54
+ trust.get("rootId") == pinned_root.get("rootId"),
55
+ "UNPINNED_ROOT",
56
+ "Packet does not reference the caller-pinned root.",
57
+ )
58
+ require(
59
+ latest_revocation_journal is not None,
60
+ "LATEST_REVOCATION_REQUIRED",
61
+ "Caller must supply the latest trusted revocation journal.",
62
+ )
63
+ journal = latest_revocation_journal
64
+ verify_revocation_journal(journal, store)
65
+ assert_revocation_journal_extends(trust["revocationJournal"], journal)
66
+ request_signer_certificate = trust["requestSignerCertificate"]
67
+ verify_key_certificate(
68
+ request_signer_certificate,
69
+ store,
70
+ expected_role="server_attestor",
71
+ tenant_ref=str(packet["tenantRef"]),
72
+ project_ref=str(packet["projectRef"]),
73
+ environment=str(packet["environment"]),
74
+ at=str(packet["probeRequest"]["requestedAt"]),
75
+ revocation_journal=journal,
76
+ )
77
+ gateway_certificate = trust["gatewayCertificate"]
78
+ verify_key_certificate(
79
+ gateway_certificate,
80
+ store,
81
+ expected_role="gateway_runtime",
82
+ tenant_ref=str(packet["tenantRef"]),
83
+ project_ref=str(packet["projectRef"]),
84
+ environment=str(packet["environment"]),
85
+ at=str(packet["issuedAt"]),
86
+ revocation_journal=journal,
87
+ )
88
+ signature = packet["gatewaySignature"]
89
+ require(
90
+ signature.get("keyId") == gateway_certificate.get("subjectKeyId"),
91
+ "SIGNATURE_KEY_MISMATCH",
92
+ "Gateway signature key ID does not match its certificate.",
93
+ )
94
+ body = {key: value for key, value in packet.items() if key != "gatewaySignature"}
95
+ verify_document_signature(
96
+ body,
97
+ str(signature.get("signature", "")),
98
+ str(gateway_certificate.get("publicKeyPem", "")),
99
+ )
100
+ expected = {
101
+ field: str(packet[field])
102
+ for field in (
103
+ "tenantRef",
104
+ "projectRef",
105
+ "environment",
106
+ "actionDigest",
107
+ "contractDigest",
108
+ "resourceCommitment",
109
+ "predicateDigest",
110
+ )
111
+ }
112
+ expected["probeId"] = str(packet["probeAttestation"]["probeId"])
113
+ approval_certificate = trust["approvalCertificate"]
114
+ grant_certificate = trust["grantCertificate"]
115
+ approval = packet["approval"]
116
+ grant = packet["grant"]
117
+ require(
118
+ approval.get("protocolVersion") == "witnora.signed_action_approval.v0.2"
119
+ and grant.get("protocolVersion") == "witnora.signed_execution_grant.v0.2",
120
+ "AUTHORIZATION_VERSION_UNSUPPORTED",
121
+ "Signed authorization objects must use the v0.2 protocols.",
122
+ )
123
+ for authorization_name, authorization in (("approval", approval), ("grant", grant)):
124
+ for field in ("tenantRef", "projectRef", "environment", "actionDigest"):
125
+ require(
126
+ authorization.get(field) == packet.get(field),
127
+ "AUTHORIZATION_SCOPE_MISMATCH",
128
+ f"{authorization_name} {field} is not bound to the Evidence packet.",
129
+ )
130
+ verify_key_certificate(
131
+ approval_certificate,
132
+ store,
133
+ expected_role="reviewer",
134
+ tenant_ref=str(packet["tenantRef"]),
135
+ project_ref=str(packet["projectRef"]),
136
+ environment=str(packet["environment"]),
137
+ at=str(approval["approvedAt"]),
138
+ revocation_journal=journal,
139
+ )
140
+ verify_key_certificate(
141
+ grant_certificate,
142
+ store,
143
+ expected_role="resource_scope_evaluator",
144
+ tenant_ref=str(packet["tenantRef"]),
145
+ project_ref=str(packet["projectRef"]),
146
+ environment=str(packet["environment"]),
147
+ at=str(grant["issuedAt"]),
148
+ revocation_journal=journal,
149
+ )
150
+ require(
151
+ approval.get("signerKeyId") == approval_certificate.get("subjectKeyId"),
152
+ "APPROVAL_KEY_MISMATCH",
153
+ "Approval key does not match its certificate.",
154
+ )
155
+ require(
156
+ grant.get("issuerKeyId") == grant_certificate.get("subjectKeyId"),
157
+ "GRANT_KEY_MISMATCH",
158
+ "Grant key does not match its certificate.",
159
+ )
160
+ verify_document_signature(
161
+ {key: value for key, value in approval.items() if key != "signature"},
162
+ str(approval.get("signature", "")),
163
+ str(approval_certificate.get("publicKeyPem", "")),
164
+ )
165
+ verify_document_signature(
166
+ {key: value for key, value in grant.items() if key != "signature"},
167
+ str(grant.get("signature", "")),
168
+ str(grant_certificate.get("publicKeyPem", "")),
169
+ )
170
+ require(
171
+ grant.get("approvalDigest") == sha256_canonical(approval),
172
+ "GRANT_APPROVAL_MISMATCH",
173
+ "Grant is not bound to the signed approval.",
174
+ )
175
+ verification_time = at or datetime.now(UTC)
176
+ require(
177
+ parse_timestamp(grant["notBefore"], "grant.notBefore")
178
+ <= verification_time
179
+ <= parse_timestamp(grant["expiresAt"], "grant.expiresAt"),
180
+ "GRANT_INACTIVE",
181
+ "Grant is not valid at verification time.",
182
+ )
183
+ require(
184
+ verification_time <= parse_timestamp(approval["expiresAt"], "approval.expiresAt"),
185
+ "APPROVAL_EXPIRED",
186
+ "Approval is expired at verification time.",
187
+ )
188
+ verify_probe_exchange(
189
+ packet["probeRequest"],
190
+ packet["probeAttestation"],
191
+ trust_store=store,
192
+ request_certificate=request_signer_certificate,
193
+ probe_certificate=trust["probeCertificate"],
194
+ revocation_journal=journal,
195
+ expected=expected,
196
+ now=verification_time,
197
+ max_freshness_ms=300_000,
198
+ )
199
+ require(
200
+ packet["probeAttestation"].get("probeId")
201
+ == trust["probeCertificate"].get("operatorIdentity"),
202
+ "PROBE_IDENTITY_MISMATCH",
203
+ "Probe identity is not bound to the certified operator identity.",
204
+ )
205
+ except (VerificationError, KeyError, TypeError, ValueError) as error:
206
+ cryptographically_valid = False
207
+ trusted_by_pinned_root = False
208
+ failures.append(str(error))
209
+
210
+ approval = packet["approval"]
211
+ grant = packet["grant"]
212
+ recovery_history = packet["executionAttempt"].get("recoveryHistory", [])
213
+ dispatch_reconciled = (
214
+ packet["executionAttempt"].get("dispatchCount") == 1
215
+ and packet["executionAttempt"].get("state") == "RECONCILED_SUCCESS"
216
+ and packet["reconciliation"].get("status") == "TARGET_STATE_RECONCILED"
217
+ and bool(recovery_history)
218
+ and recovery_history[-1].get("state") == "RECONCILED_SUCCESS"
219
+ and sha256_canonical(recovery_history) == packet.get("eventChainDigest")
220
+ )
221
+ binding_source = packet["reconciliation"].get("authorizationBindingSource")
222
+ grant_use_bound = (
223
+ packet["probeAttestation"].get("normalizedObservation", {}).get("consumedGrantId")
224
+ == grant.get("grantId")
225
+ if binding_source == "TARGET_GRANT_ECHO"
226
+ else binding_source == "GATEWAY_DISPATCH_RECORD" and dispatch_reconciled
227
+ )
228
+ authorization_bound = (
229
+ cryptographically_valid
230
+ and approval.get("actionDigest") == packet.get("actionDigest")
231
+ and grant.get("actionDigest") == packet.get("actionDigest")
232
+ and grant.get("maxUses") == 1
233
+ and grant_use_bound
234
+ )
235
+ outcome_observed = (
236
+ packet["probeAttestation"].get("outcomeStatus") == "VERIFIED" and dispatch_reconciled
237
+ )
238
+ deployment_mode = packet["separation"].get("probeDeploymentMode")
239
+ customer_operated = (
240
+ deployment_mode in {"separate_process", "separate_container", "customer_network"}
241
+ and pinned_root.get("ownerType") == "CUSTOMER_ASSURANCE_ROOT"
242
+ and packet["evidenceStrength"].get("outcomeObservation") == "CUSTOMER_OPERATED_READ_PATH"
243
+ )
244
+ expected_execution = (
245
+ "TARGET_STATE_RECONCILED"
246
+ if dispatch_reconciled
247
+ else (
248
+ "DISPATCH_UNKNOWN"
249
+ if packet["executionAttempt"].get("state") == "DISPATCH_UNKNOWN"
250
+ else "NOT_ESTABLISHED"
251
+ )
252
+ )
253
+ expected_separation = (
254
+ "CUSTOMER_NETWORK_SEPARATED"
255
+ if deployment_mode == "customer_network"
256
+ else (
257
+ "CONTAINER_SEPARATED"
258
+ if deployment_mode == "separate_container"
259
+ else "PROCESS_SEPARATED"
260
+ )
261
+ )
262
+ expected_authorization = (
263
+ (
264
+ "EXACT_ACTION_GRANT_BOUND"
265
+ if binding_source == "TARGET_GRANT_ECHO"
266
+ else "GATEWAY_DISPATCH_BOUND"
267
+ )
268
+ if authorization_bound
269
+ else "NOT_ESTABLISHED"
270
+ )
271
+ strength_consistent = (
272
+ packet["evidenceStrength"].get("authorizationBinding") == expected_authorization
273
+ and packet["evidenceStrength"].get("executionBinding") == expected_execution
274
+ and packet["evidenceStrength"].get("sourceIntegrity")
275
+ == ("CUSTOMER_ROOT_CERTIFIED" if cryptographically_valid else "SELF_ASSERTED")
276
+ and packet["evidenceStrength"].get("outcomeObservation")
277
+ == ("CUSTOMER_OPERATED_READ_PATH" if outcome_observed else "NOT_OBSERVED")
278
+ and packet["evidenceStrength"].get("operatorSeparation") == expected_separation
279
+ and packet["evidenceStrength"].get("review") != "EXTERNAL"
280
+ and packet["privacy"].get("rawCanaryMatchesOutsideLocal") == 0
281
+ )
282
+ if not authorization_bound:
283
+ failures.append("Authorization is not bound to the exact action.")
284
+ if not dispatch_reconciled:
285
+ failures.append("Target dispatch is not reconciled.")
286
+ if not outcome_observed:
287
+ failures.append("Outcome was not independently observed.")
288
+ if not strength_consistent:
289
+ failures.append("Evidence-strength vector overclaims the packet evidence.")
290
+ if sha256_canonical(packet["probeRequest"]) != packet["probeAttestation"].get("requestDigest"):
291
+ failures.append("Probe request digest mismatch.")
292
+ overall = (
293
+ cryptographically_valid
294
+ and authorization_bound
295
+ and dispatch_reconciled
296
+ and outcome_observed
297
+ and customer_operated
298
+ and strength_consistent
299
+ )
300
+ return {
301
+ "protocolVersion": "witnora.offline_verification_report.v0.2",
302
+ "overall": "PASS" if overall else "FAIL",
303
+ "networkUsed": False,
304
+ "dimensions": {
305
+ "cryptographicallyValid": cryptographically_valid,
306
+ "trustedByPinnedRoot": trusted_by_pinned_root,
307
+ "authorizationBound": authorization_bound,
308
+ "dispatchReconciled": dispatch_reconciled,
309
+ "outcomeObserved": outcome_observed,
310
+ "customerOperated": customer_operated,
311
+ # v0.2 has no independent reviewer signature object.
312
+ "thirdPartyReviewed": False,
313
+ },
314
+ "failures": failures,
315
+ }
316
+
317
+
318
+ def _failed(message: str) -> dict[str, Any]:
319
+ return {
320
+ "protocolVersion": "witnora.offline_verification_report.v0.2",
321
+ "overall": "FAIL",
322
+ "networkUsed": False,
323
+ "dimensions": dict.fromkeys(
324
+ (
325
+ "cryptographicallyValid",
326
+ "trustedByPinnedRoot",
327
+ "authorizationBound",
328
+ "dispatchReconciled",
329
+ "outcomeObserved",
330
+ "customerOperated",
331
+ "thirdPartyReviewed",
332
+ ),
333
+ False,
334
+ ),
335
+ "failures": [message],
336
+ }
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, MutableSet
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timedelta
6
+ from typing import Any
7
+
8
+ from .canonical import sha256_canonical
9
+ from .crypto import signed_body, verify_document_signature
10
+ from .errors import require
11
+ from .trust import (
12
+ PinnedTrustStore,
13
+ parse_timestamp,
14
+ verify_key_certificate,
15
+ verify_revocation_journal,
16
+ )
17
+
18
+ PROBE_REQUEST_VERSION = "witnora.probe_observation_request.v0.2"
19
+ PROBE_ATTESTATION_VERSION = "witnora.probe_observation_attestation.v0.2"
20
+ _BINDING_FIELDS = (
21
+ "tenantRef",
22
+ "projectRef",
23
+ "environment",
24
+ "actionDigest",
25
+ "contractDigest",
26
+ "resourceCommitment",
27
+ "predicateDigest",
28
+ "probeId",
29
+ )
30
+
31
+
32
+ @dataclass
33
+ class ReplayLedger:
34
+ request_nonces: MutableSet[str] = field(default_factory=set)
35
+ attestation_ids: MutableSet[str] = field(default_factory=set)
36
+
37
+ def reserve(self, request: Mapping[str, Any], attestation: Mapping[str, Any]) -> None:
38
+ nonce = str(request.get("nonce", ""))
39
+ attestation_id = str(attestation.get("attestationId", ""))
40
+ require(
41
+ nonce not in self.request_nonces,
42
+ "PROBE_REQUEST_REPLAY",
43
+ "Probe request nonce was already accepted.",
44
+ )
45
+ require(
46
+ attestation_id not in self.attestation_ids,
47
+ "PROBE_ATTESTATION_REPLAY",
48
+ "Probe attestation ID was already accepted.",
49
+ )
50
+ self.request_nonces.add(nonce)
51
+ self.attestation_ids.add(attestation_id)
52
+
53
+
54
+ def verify_probe_exchange(
55
+ request: Mapping[str, Any],
56
+ attestation: Mapping[str, Any],
57
+ *,
58
+ trust_store: PinnedTrustStore,
59
+ request_certificate: Mapping[str, Any],
60
+ probe_certificate: Mapping[str, Any],
61
+ revocation_journal: Mapping[str, Any],
62
+ expected: Mapping[str, str],
63
+ now: datetime,
64
+ max_freshness_ms: int = 300_000,
65
+ replay_ledger: ReplayLedger | None = None,
66
+ ) -> None:
67
+ require(
68
+ request.get("protocolVersion") == PROBE_REQUEST_VERSION,
69
+ "PROBE_REQUEST_VERSION_UNSUPPORTED",
70
+ "Probe request is not v0.2.",
71
+ )
72
+ require(
73
+ attestation.get("protocolVersion") == PROBE_ATTESTATION_VERSION,
74
+ "PROBE_ATTESTATION_VERSION_UNSUPPORTED",
75
+ "Probe attestation is not v0.2.",
76
+ )
77
+ verify_revocation_journal(revocation_journal, trust_store)
78
+
79
+ requested_at = parse_timestamp(request.get("requestedAt"), "request.requestedAt")
80
+ not_before = parse_timestamp(request.get("notBefore"), "request.notBefore")
81
+ expires_at = parse_timestamp(request.get("expiresAt"), "request.expiresAt")
82
+ require(
83
+ not_before <= requested_at <= expires_at,
84
+ "PROBE_REQUEST_WINDOW_INVALID",
85
+ "Request was not issued inside its declared window.",
86
+ )
87
+ require(
88
+ request.get("requestSignerKeyId") == request_certificate.get("subjectKeyId"),
89
+ "PROBE_REQUEST_KEY_MISMATCH",
90
+ "Request signer does not match its certificate.",
91
+ )
92
+ verify_key_certificate(
93
+ request_certificate,
94
+ trust_store,
95
+ expected_role="server_attestor",
96
+ tenant_ref=expected["tenantRef"],
97
+ project_ref=expected["projectRef"],
98
+ environment=expected["environment"],
99
+ at=requested_at,
100
+ revocation_journal=revocation_journal,
101
+ )
102
+ verify_document_signature(
103
+ signed_body(request),
104
+ str(request.get("signature", "")),
105
+ str(request_certificate.get("publicKeyPem", "")),
106
+ )
107
+ _assert_bindings(request, expected, "PROBE_REQUEST_BINDING_MISMATCH")
108
+
109
+ observed_at = parse_timestamp(attestation.get("observedAt"), "attestation.observedAt")
110
+ require(
111
+ not_before <= observed_at <= expires_at,
112
+ "PROBE_OBSERVATION_OUTSIDE_REQUEST_WINDOW",
113
+ "Observation falls outside the request window.",
114
+ )
115
+ require(
116
+ observed_at <= now + timedelta(seconds=30),
117
+ "PROBE_OBSERVATION_FUTURE",
118
+ "Observation is future-dated beyond clock skew.",
119
+ )
120
+ freshness = int(attestation.get("dataFreshnessMs", -1))
121
+ require(
122
+ 0 <= freshness <= max_freshness_ms,
123
+ "PROBE_OBSERVATION_STALE",
124
+ "Declared observation freshness exceeds the limit.",
125
+ )
126
+ require(
127
+ (now - observed_at).total_seconds() * 1000 <= max_freshness_ms,
128
+ "PROBE_OBSERVATION_STALE",
129
+ "Observation is stale at verification time.",
130
+ )
131
+ require(
132
+ attestation.get("keyCertificateId") == probe_certificate.get("certificateId"),
133
+ "PROBE_CERTIFICATE_MISMATCH",
134
+ "Attestation certificate ID does not match.",
135
+ )
136
+ verify_key_certificate(
137
+ probe_certificate,
138
+ trust_store,
139
+ expected_role="outcome_probe",
140
+ tenant_ref=expected["tenantRef"],
141
+ project_ref=expected["projectRef"],
142
+ environment=expected["environment"],
143
+ at=observed_at,
144
+ revocation_journal=revocation_journal,
145
+ )
146
+ verify_document_signature(
147
+ signed_body(attestation),
148
+ str(attestation.get("signature", "")),
149
+ str(probe_certificate.get("publicKeyPem", "")),
150
+ )
151
+ require(
152
+ attestation.get("requestId") == request.get("requestId"),
153
+ "PROBE_REQUEST_ID_MISMATCH",
154
+ "Attestation references another request.",
155
+ )
156
+ require(
157
+ attestation.get("requestDigest") == sha256_canonical(request),
158
+ "PROBE_REQUEST_DIGEST_MISMATCH",
159
+ "Attestation request digest is invalid.",
160
+ )
161
+ require(
162
+ attestation.get("nonce") == request.get("nonce"),
163
+ "PROBE_NONCE_MISMATCH",
164
+ "Attestation nonce differs from the request.",
165
+ )
166
+ _assert_bindings(attestation, expected, "PROBE_ATTESTATION_BINDING_MISMATCH")
167
+ require(
168
+ isinstance(attestation.get("limitations"), list),
169
+ "PROBE_LIMITATIONS_INVALID",
170
+ "Attestation limitations must be an array.",
171
+ )
172
+ require(
173
+ attestation.get("outcomeStatus") in {"VERIFIED", "NOT_VERIFIED", "INDETERMINATE"},
174
+ "PROBE_OUTCOME_STATUS_INVALID",
175
+ "Probe outcome status is invalid.",
176
+ )
177
+
178
+ if replay_ledger is not None:
179
+ replay_ledger.reserve(request, attestation)
180
+
181
+
182
+ def _assert_bindings(value: Mapping[str, Any], expected: Mapping[str, str], code: str) -> None:
183
+ for field_name in _BINDING_FIELDS:
184
+ require(
185
+ value.get(field_name) == expected.get(field_name),
186
+ code,
187
+ f"Probe {field_name} binding mismatch.",
188
+ )
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
+
8
+ from .canonical import sha256_canonical
9
+ from .crypto import public_key_text, sign_document
10
+ from .trust import root_fingerprint
11
+
12
+
13
+ def create_trust_root(
14
+ *,
15
+ root_id: str,
16
+ private_key: Ed25519PrivateKey,
17
+ allowed_roles: list[str],
18
+ organization_ref: str,
19
+ created_at: str,
20
+ not_before: str,
21
+ not_after: str,
22
+ owner_type: str = "CUSTOMER_ASSURANCE_ROOT",
23
+ policy_version: str = "v0.2",
24
+ ) -> dict[str, Any]:
25
+ public_key_pem = public_key_text(private_key.public_key())
26
+ return {
27
+ "protocolVersion": "witnora.trust_root.v0.2",
28
+ "rootId": root_id,
29
+ "publicKeyPem": public_key_pem,
30
+ "algorithm": "Ed25519",
31
+ "ownerType": owner_type,
32
+ "allowedRoles": allowed_roles,
33
+ "organizationRef": organization_ref,
34
+ "createdAt": created_at,
35
+ "notBefore": not_before,
36
+ "notAfter": not_after,
37
+ "policyVersion": policy_version,
38
+ "rootFingerprint": root_fingerprint(public_key_pem),
39
+ }
40
+
41
+
42
+ def issue_key_certificate(
43
+ *,
44
+ root: Mapping[str, Any],
45
+ root_private_key: Ed25519PrivateKey,
46
+ subject_private_key: Ed25519PrivateKey,
47
+ certificate_id: str,
48
+ serial_number: int,
49
+ subject_key_id: str,
50
+ role: str,
51
+ tenant_ref: str,
52
+ project_ref: str,
53
+ environment: str,
54
+ operator_identity: str,
55
+ not_before: str,
56
+ not_after: str,
57
+ ) -> dict[str, Any]:
58
+ body = {
59
+ "protocolVersion": "witnora.key_certificate.v0.2",
60
+ "certificateId": certificate_id,
61
+ "serialNumber": serial_number,
62
+ "subjectKeyId": subject_key_id,
63
+ "publicKeyPem": public_key_text(subject_private_key.public_key()),
64
+ "issuerRootId": root["rootId"],
65
+ "role": role,
66
+ "tenantRef": tenant_ref,
67
+ "projectRef": project_ref,
68
+ "environment": environment,
69
+ "operatorIdentity": operator_identity,
70
+ "notBefore": not_before,
71
+ "notAfter": not_after,
72
+ }
73
+ certificate_digest = sha256_canonical(body)
74
+ signed = {**body, "certificateDigest": certificate_digest}
75
+ return {**signed, "signature": sign_document(signed, root_private_key)}
76
+
77
+
78
+ def empty_revocation_journal(root_id: str) -> dict[str, Any]:
79
+ return {
80
+ "protocolVersion": "witnora.revocation_journal.v0.2",
81
+ "rootId": root_id,
82
+ "statements": [],
83
+ "checkpointDigest": sha256_canonical([]),
84
+ }
85
+
86
+
87
+ def append_revocation(
88
+ journal: Mapping[str, Any],
89
+ *,
90
+ root_private_key: Ed25519PrivateKey,
91
+ revoked_id: str,
92
+ effective_at: str,
93
+ retroactive: bool,
94
+ reason: str,
95
+ ) -> dict[str, Any]:
96
+ statements = list(journal["statements"])
97
+ body = {
98
+ "protocolVersion": "witnora.key_revocation_statement.v0.2",
99
+ "sequence": len(statements) + 1,
100
+ "previousJournalDigest": journal["checkpointDigest"] if statements else None,
101
+ "revokedId": revoked_id,
102
+ "effectiveAt": effective_at,
103
+ "retroactive": retroactive,
104
+ "reason": reason,
105
+ "issuerRootId": journal["rootId"],
106
+ }
107
+ statement_digest = sha256_canonical(body)
108
+ signed = {**body, "statementDigest": statement_digest}
109
+ statements.append({**signed, "signature": sign_document(signed, root_private_key)})
110
+ return {
111
+ "protocolVersion": "witnora.revocation_journal.v0.2",
112
+ "rootId": journal["rootId"],
113
+ "statements": statements,
114
+ "checkpointDigest": sha256_canonical(statements),
115
+ }
116
+
117
+
118
+ def sign_probe_request(body: Mapping[str, Any], private_key: Ed25519PrivateKey) -> dict[str, Any]:
119
+ value = {"protocolVersion": "witnora.probe_observation_request.v0.2", **body}
120
+ return {**value, "signature": sign_document(value, private_key)}
121
+
122
+
123
+ def sign_probe_attestation(
124
+ body: Mapping[str, Any], private_key: Ed25519PrivateKey
125
+ ) -> dict[str, Any]:
126
+ value = {"protocolVersion": "witnora.probe_observation_attestation.v0.2", **body}
127
+ return {**value, "signature": sign_document(value, private_key)}