@7h3/protocol 0.5.0 → 0.5.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.
- package/README.md +6 -1317
- package/agentAdapter.d.ts +26 -0
- package/auditLog.d.ts +54 -0
- package/capability.d.ts +67 -0
- package/cborCodec.d.ts +34 -0
- package/conformanceVectors.d.ts +20 -0
- package/encryption.d.ts +85 -0
- package/envelopeCbor.d.ts +33 -0
- package/frameworkAdapters.d.ts +72 -0
- package/gateway.d.ts +53 -0
- package/grpcBinding.d.ts +29 -0
- package/httpBinding.d.ts +50 -0
- package/index.d.ts +39 -0
- package/index.js +3670 -0
- package/keyInfra.d.ts +64 -0
- package/keyRegistry.d.ts +9 -0
- package/keyRotation.d.ts +20 -0
- package/mcpGateway.d.ts +37 -0
- package/mcpTransports.d.ts +62 -0
- package/mcpWrapper.d.ts +83 -0
- package/otel.d.ts +65 -0
- package/package.json +13 -121
- package/policyEnforcer.d.ts +50 -0
- package/policyTelemetryFeedback.d.ts +11 -0
- package/{src/protocol.d.ts → protocol.d.ts} +6 -2
- package/protocolAgent.d.ts +58 -0
- package/protocolBinary.d.ts +8 -0
- package/protocolCapabilities.d.ts +24 -0
- package/protocolReplay.d.ts +35 -0
- package/protocolTransport.d.ts +73 -0
- package/queueBinding.d.ts +43 -0
- package/rateLimiter.d.ts +18 -0
- package/{src/redisClient.ts → redisClient.d.ts} +25 -75
- package/replayStores.d.ts +98 -0
- package/revocation.d.ts +71 -0
- package/routePolicy.d.ts +30 -0
- package/runtimePolicy.d.ts +24 -0
- package/runtimePolicyManager.d.ts +15 -0
- package/runtimePolicyPresets.d.ts +11 -0
- package/signedResponse.d.ts +21 -0
- package/stream.d.ts +59 -0
- package/telemetry.d.ts +79 -0
- package/webhookBinding.d.ts +30 -0
- package/wsBinding.d.ts +52 -0
- package/.dockerignore +0 -19
- package/.github/dependabot.yml +0 -32
- package/.github/workflows/ci.yml +0 -31
- package/.github/workflows/publish.yml +0 -59
- package/.github/workflows/scorecard.yml +0 -37
- package/7h3.example.yaml +0 -125
- package/CHANGELOG.md +0 -152
- package/CONTRIBUTING.md +0 -82
- package/Dockerfile +0 -73
- package/GOVERNANCE.md +0 -62
- package/SECURITY.md +0 -70
- package/bench-results/replay-cache-full-1777891033256.json +0 -10
- package/bench-results/replay-cache-full-1777896317488.json +0 -10
- package/bench-results/replay-cache-full-1777900993184.json +0 -10
- package/bench-results/replay-cache-full-1777901019285.json +0 -10
- package/bench-results/replay-cache-quick-1777870170126.json +0 -10
- package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
- package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
- package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
- package/bench-results/wire-codecs-full-1777891019803.json +0 -93
- package/bench-results/wire-codecs-full-1777896260964.json +0 -93
- package/bench-results/wire-codecs-full-1777901004247.json +0 -93
- package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
- package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
- package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
- package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
- package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
- package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
- package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
- package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
- package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
- package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
- package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
- package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
- package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
- package/bin/7h3.ts +0 -406
- package/conformance/7h3_v0_1.json +0 -77
- package/conformance/7h3_v0_1_binary.json +0 -20
- package/conformance/aip_v0_1_binary.json +0 -20
- package/docker-compose.yaml +0 -77
- package/docs/ADOPTION_PLAN.md +0 -120
- package/docs/AGENTS.md +0 -77
- package/docs/AIP_RFC_v0.1.md +0 -97
- package/docs/AI_DECISION_CARD.md +0 -122
- package/docs/AI_RUNTIME_POLICY.json +0 -126
- package/docs/AI_RUNTIME_POLICY.yaml +0 -110
- package/docs/BACKPRESSURE_TUNING.md +0 -65
- package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
- package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
- package/docs/BINARY_CODEC_BENCH.md +0 -23
- package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
- package/docs/CLOCK_SKEW_POLICY.md +0 -30
- package/docs/DISTRIBUTED_REPLAY.md +0 -142
- package/docs/FUZZ_CAMPAIGN.md +0 -121
- package/docs/GATEWAY.md +0 -195
- package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
- package/docs/KEY_REVOCATION.md +0 -69
- package/docs/MCP_WRAPPER.md +0 -159
- package/docs/MIGRATION_GUIDE.md +0 -40
- package/docs/OPERATORS.md +0 -184
- package/docs/PERF_REGRESSION_POLICY.md +0 -34
- package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
- package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
- package/docs/RELEASE_GATE.md +0 -25
- package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
- package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
- package/docs/TELEMETRY.md +0 -41
- package/docs/THREAT_MODEL.md +0 -89
- package/docs/VERSIONING_POLICY.md +0 -30
- package/docs/assets/banner-github.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/banner.svg +0 -123
- package/eslint.config.js +0 -15
- package/fuzz/ts/harness-decode.ts +0 -136
- package/fuzz/ts/harness-verify.ts +0 -121
- package/fuzz/ts/run.ts +0 -35
- package/mcp-server/README.md +0 -38
- package/mcp-server/package-lock.json +0 -1187
- package/mcp-server/package.json +0 -35
- package/mcp-server/src/index.ts +0 -236
- package/mcp-server/tsconfig.json +0 -14
- package/scripts/aip-framework-quickstart.ts +0 -110
- package/scripts/aip-mcp-gateway.ts +0 -38
- package/scripts/aip-mcp-wrap-demo.ts +0 -72
- package/scripts/aip-quickstart.ts +0 -60
- package/scripts/bench-diff.ts +0 -118
- package/scripts/bench-protocol-e2e.ts +0 -937
- package/scripts/bench-protocol-openloop.ts +0 -1397
- package/scripts/bench-replay-cache.ts +0 -76
- package/scripts/bench-signature-profiles.ts +0 -180
- package/scripts/bench-wire-codecs.ts +0 -161
- package/scripts/build-binary-conformance.ts +0 -36
- package/scripts/build-release-dashboard.ts +0 -175
- package/scripts/canary-rollout.ts +0 -38
- package/scripts/mcpGatewayCli.test.ts +0 -116
- package/scripts/prepare-aip-package.ts +0 -88
- package/scripts/regen-conformance-sigs.ts +0 -18
- package/scripts/release-gate.ts +0 -19
- package/scripts/validate-runtime-policy.ts +0 -18
- package/sdk/browser/index.test.ts +0 -162
- package/sdk/browser/index.ts +0 -257
- package/sdk/browser/package.json +0 -13
- package/sdk/go/cbor.go +0 -551
- package/sdk/go/cbor_test.go +0 -232
- package/sdk/go/encryption.go +0 -280
- package/sdk/go/encryption_test.go +0 -318
- package/sdk/go/go.mod +0 -7
- package/sdk/go/go.sum +0 -4
- package/sdk/go/http.go +0 -135
- package/sdk/go/protocol.go +0 -324
- package/sdk/go/protocol_test.go +0 -334
- package/sdk/go/replay.go +0 -121
- package/sdk/go/replay_test.go +0 -149
- package/sdk/go/webhook.go +0 -136
- package/sdk/pq/package-lock.json +0 -1358
- package/sdk/pq/package.json +0 -42
- package/sdk/pq/src/index.test.ts +0 -143
- package/sdk/pq/src/index.ts +0 -166
- package/sdk/pq/tsconfig.json +0 -14
- package/sdk/pq/vitest.config.ts +0 -7
- package/sdk/python/README.md +0 -18
- package/sdk/python/protocol_7h3/__init__.py +0 -46
- package/sdk/python/protocol_7h3/encryption.py +0 -252
- package/sdk/python/protocol_7h3/http.py +0 -212
- package/sdk/python/protocol_7h3/keys.py +0 -149
- package/sdk/python/protocol_7h3/pq.py +0 -244
- package/sdk/python/protocol_7h3/protocol.py +0 -525
- package/sdk/python/protocol_7h3/queue.py +0 -118
- package/sdk/python/protocol_7h3/replay.py +0 -98
- package/sdk/python/protocol_7h3/webhook.py +0 -116
- package/sdk/python/pyproject.toml +0 -40
- package/sdk/python/tests/test_conformance.py +0 -110
- package/sdk/python/tests/test_encryption.py +0 -206
- package/sdk/python/tests/test_http.py +0 -305
- package/sdk/python/tests/test_keys.py +0 -417
- package/sdk/python/tests/test_queue.py +0 -120
- package/sdk/python/tests/test_webhook.py +0 -345
- package/sdk/rust/Cargo.lock +0 -371
- package/sdk/rust/Cargo.toml +0 -25
- package/sdk/rust/README.md +0 -31
- package/sdk/rust/fuzz/Cargo.toml +0 -29
- package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
- package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
- package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
- package/sdk/rust/src/http.rs +0 -145
- package/sdk/rust/src/keys.rs +0 -161
- package/sdk/rust/src/lib.rs +0 -688
- package/sdk/rust/src/queue.rs +0 -79
- package/sdk/rust/src/webhook.rs +0 -86
- package/sdk/rust/tests/conformance.rs +0 -148
- package/sdk/rust/tests/gateway.rs +0 -130
- package/sdk/rust/tests/http_webhook_queue.rs +0 -201
- package/sdk/rust/tests/keys.rs +0 -189
- package/sdk/threshold/index.d.ts +0 -68
- package/sdk/threshold/index.d.ts.map +0 -1
- package/sdk/threshold/index.js +0 -254
- package/sdk/threshold/package-lock.json +0 -1361
- package/sdk/threshold/package.json +0 -39
- package/sdk/threshold/src/index.d.ts +0 -68
- package/sdk/threshold/src/index.d.ts.map +0 -1
- package/sdk/threshold/src/index.js +0 -254
- package/sdk/threshold/src/index.test.ts +0 -238
- package/sdk/threshold/src/index.ts +0 -355
- package/sdk/threshold/tsconfig.json +0 -19
- package/sdk/threshold/vitest.config.ts +0 -12
- package/src/agentAdapter.test.ts +0 -48
- package/src/agentAdapter.ts +0 -56
- package/src/auditLog.test.ts +0 -145
- package/src/auditLog.ts +0 -147
- package/src/capability.test.ts +0 -504
- package/src/capability.ts +0 -380
- package/src/cborCodec.test.ts +0 -263
- package/src/cborCodec.ts +0 -339
- package/src/conformance.test.ts +0 -136
- package/src/conformanceVectors.ts +0 -99
- package/src/encryption.test.ts +0 -206
- package/src/encryption.ts +0 -245
- package/src/envelopeCbor.ts +0 -140
- package/src/frameworkAdapters.test.ts +0 -290
- package/src/frameworkAdapters.ts +0 -261
- package/src/gateway.test.ts +0 -343
- package/src/gateway.ts +0 -246
- package/src/grpcBinding.test.ts +0 -211
- package/src/grpcBinding.ts +0 -103
- package/src/httpBinding.test.ts +0 -376
- package/src/httpBinding.ts +0 -189
- package/src/index.ts +0 -39
- package/src/keyInfra.test.ts +0 -278
- package/src/keyInfra.ts +0 -228
- package/src/keyRegistry.ts +0 -59
- package/src/keyRotation.test.ts +0 -78
- package/src/keyRotation.ts +0 -72
- package/src/mcpGateway.test.ts +0 -129
- package/src/mcpGateway.ts +0 -250
- package/src/mcpTransports.test.ts +0 -92
- package/src/mcpTransports.ts +0 -169
- package/src/mcpWrapper.test.ts +0 -179
- package/src/mcpWrapper.ts +0 -206
- package/src/otel.ts +0 -136
- package/src/policyEnforcer.test.ts +0 -99
- package/src/policyEnforcer.ts +0 -169
- package/src/policyTelemetryFeedback.test.ts +0 -25
- package/src/policyTelemetryFeedback.ts +0 -38
- package/src/protocol.bench.ts +0 -37
- package/src/protocol.d.ts.map +0 -1
- package/src/protocol.js +0 -294
- package/src/protocol.test.ts +0 -155
- package/src/protocol.ts +0 -414
- package/src/protocolAgent.test.ts +0 -105
- package/src/protocolAgent.ts +0 -169
- package/src/protocolBinary.test.ts +0 -165
- package/src/protocolBinary.ts +0 -312
- package/src/protocolCapabilities.ts +0 -70
- package/src/protocolFuzz.advanced.test.ts +0 -235
- package/src/protocolFuzz.test.ts +0 -111
- package/src/protocolNegative.test.ts +0 -97
- package/src/protocolReplay.test.ts +0 -71
- package/src/protocolReplay.ts +0 -194
- package/src/protocolTransport.test.ts +0 -556
- package/src/protocolTransport.ts +0 -483
- package/src/queueBinding.test.ts +0 -130
- package/src/queueBinding.ts +0 -102
- package/src/rateLimiter.test.ts +0 -96
- package/src/rateLimiter.ts +0 -46
- package/src/redisIntegration.test.ts +0 -134
- package/src/replayStores.test.ts +0 -273
- package/src/replayStores.ts +0 -215
- package/src/revocation.test.ts +0 -98
- package/src/revocation.ts +0 -0
- package/src/routePolicy.test.ts +0 -87
- package/src/routePolicy.ts +0 -72
- package/src/runtimePolicy.test.ts +0 -49
- package/src/runtimePolicy.ts +0 -81
- package/src/runtimePolicyManager.test.ts +0 -29
- package/src/runtimePolicyManager.ts +0 -50
- package/src/runtimePolicyPresets.ts +0 -43
- package/src/signedResponse.test.ts +0 -111
- package/src/signedResponse.ts +0 -83
- package/src/stream.test.ts +0 -254
- package/src/stream.ts +0 -417
- package/src/telemetry.test.ts +0 -251
- package/src/telemetry.ts +0 -299
- package/src/webhookBinding.test.ts +0 -144
- package/src/webhookBinding.ts +0 -115
- package/src/wsBinding.test.ts +0 -221
- package/src/wsBinding.ts +0 -200
- package/tsconfig.json +0 -15
- package/tsconfig.lib.json +0 -23
- package/vite.lib.config.ts +0 -16
- package/vitest.config.ts +0 -11
package/sdk/pq/package.json
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@7h3/protocol-pq",
|
|
3
|
-
"version": "0.5.0",
|
|
4
|
-
"description": "7h3 Protocol — Post-quantum signatures (ML-DSA-65, ML-DSA-87) via @noble/post-quantum",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.js",
|
|
7
|
-
"types": "./index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./index.d.ts",
|
|
11
|
-
"import": "./index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"@noble/post-quantum": "^0.2.0"
|
|
16
|
-
},
|
|
17
|
-
"peerDependencies": {
|
|
18
|
-
"@7h3/protocol": "^0.5.0"
|
|
19
|
-
},
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"keywords": [
|
|
22
|
-
"7h3",
|
|
23
|
-
"protocol",
|
|
24
|
-
"post-quantum",
|
|
25
|
-
"ml-dsa",
|
|
26
|
-
"dilithium",
|
|
27
|
-
"signing"
|
|
28
|
-
],
|
|
29
|
-
"repository": {
|
|
30
|
-
"type": "git",
|
|
31
|
-
"url": "https://github.com/IceMasterT/7h3-protocol.git"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "tsc",
|
|
35
|
-
"test": "vitest run src/index.test.ts"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/node": "^26.1.0",
|
|
39
|
-
"typescript": "~6.0.3",
|
|
40
|
-
"vitest": "^4.1.8"
|
|
41
|
-
}
|
|
42
|
-
}
|
package/sdk/pq/src/index.test.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
generatePqKeyPair,
|
|
4
|
-
signEnvelopePq,
|
|
5
|
-
verifyEnvelopePq,
|
|
6
|
-
signEnvelopeMlDsa65,
|
|
7
|
-
signEnvelopeMlDsa87,
|
|
8
|
-
verifyEnvelopeMlDsa65,
|
|
9
|
-
verifyEnvelopeMlDsa87,
|
|
10
|
-
createEnvelope,
|
|
11
|
-
} from './index.js'
|
|
12
|
-
|
|
13
|
-
// Conformance vector envelope (deterministic for tests)
|
|
14
|
-
const baseEnvelope = {
|
|
15
|
-
header: {
|
|
16
|
-
version: '7h3/0.1' as const,
|
|
17
|
-
messageId: 'test-msg-001',
|
|
18
|
-
timestampMs: 1_700_000_000_000,
|
|
19
|
-
ttlMs: 60_000,
|
|
20
|
-
sender: 'agent-alpha',
|
|
21
|
-
recipient: 'agent-beta',
|
|
22
|
-
nonce: 'abc123xyz',
|
|
23
|
-
},
|
|
24
|
-
body: {
|
|
25
|
-
intent: 'TASK' as const,
|
|
26
|
-
content: 'Hello, post-quantum world!',
|
|
27
|
-
},
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('generatePqKeyPair', () => {
|
|
31
|
-
it('1. ML-DSA-65 returns keypair with correct algorithm field', () => {
|
|
32
|
-
const kp = generatePqKeyPair('ML-DSA-65')
|
|
33
|
-
expect(kp.algorithm).toBe('ML-DSA-65')
|
|
34
|
-
expect(typeof kp.publicKey).toBe('string')
|
|
35
|
-
expect(typeof kp.privateKey).toBe('string')
|
|
36
|
-
expect(typeof kp.createdAt).toBe('number')
|
|
37
|
-
expect(kp.publicKey.length).toBeGreaterThan(0)
|
|
38
|
-
expect(kp.privateKey.length).toBeGreaterThan(0)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('2. ML-DSA-87 returns keypair', () => {
|
|
42
|
-
const kp = generatePqKeyPair('ML-DSA-87')
|
|
43
|
-
expect(kp.algorithm).toBe('ML-DSA-87')
|
|
44
|
-
expect(typeof kp.publicKey).toBe('string')
|
|
45
|
-
expect(typeof kp.privateKey).toBe('string')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('7. ML-DSA-65 public key is correct size (1952 bytes → ~2603 base64url chars)', () => {
|
|
49
|
-
const kp = generatePqKeyPair('ML-DSA-65')
|
|
50
|
-
// base64url: ceil(1952 / 3) * 4 = 2604 chars, minus up to 2 padding = 2603 or 2604
|
|
51
|
-
const decoded = Buffer.from(
|
|
52
|
-
kp.publicKey.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(kp.publicKey.length / 4) * 4, '='),
|
|
53
|
-
'base64'
|
|
54
|
-
)
|
|
55
|
-
expect(decoded.length).toBe(1952)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('8. ML-DSA-65 private key is correct size (4032 bytes secretKey)', () => {
|
|
59
|
-
const kp = generatePqKeyPair('ML-DSA-65')
|
|
60
|
-
const decoded = Buffer.from(
|
|
61
|
-
kp.privateKey.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(kp.privateKey.length / 4) * 4, '='),
|
|
62
|
-
'base64'
|
|
63
|
-
)
|
|
64
|
-
expect(decoded.length).toBe(4032)
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('ML-DSA-65 sign/verify', () => {
|
|
69
|
-
it('3. ML-DSA-65 sign + verify round trip', async () => {
|
|
70
|
-
const kp = generatePqKeyPair('ML-DSA-65')
|
|
71
|
-
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-65')
|
|
72
|
-
expect(signed.signature?.alg).toBe('ML-DSA-65')
|
|
73
|
-
expect(typeof signed.signature?.value).toBe('string')
|
|
74
|
-
expect(signed.signature!.value.length).toBeGreaterThan(0)
|
|
75
|
-
const valid = await verifyEnvelopePq(signed, kp.publicKey)
|
|
76
|
-
expect(valid).toBe(true)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('4. ML-DSA-65 verify fails on tampered envelope', async () => {
|
|
80
|
-
const kp = generatePqKeyPair('ML-DSA-65')
|
|
81
|
-
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-65')
|
|
82
|
-
const tampered = {
|
|
83
|
-
...signed,
|
|
84
|
-
body: { ...signed.body, content: 'TAMPERED content' },
|
|
85
|
-
}
|
|
86
|
-
const valid = await verifyEnvelopePq(tampered, kp.publicKey)
|
|
87
|
-
expect(valid).toBe(false)
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
describe('ML-DSA-87 sign/verify', () => {
|
|
92
|
-
it('5. ML-DSA-87 sign + verify round trip', async () => {
|
|
93
|
-
const kp = generatePqKeyPair('ML-DSA-87')
|
|
94
|
-
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-87')
|
|
95
|
-
expect(signed.signature?.alg).toBe('ML-DSA-87')
|
|
96
|
-
const valid = await verifyEnvelopePq(signed, kp.publicKey)
|
|
97
|
-
expect(valid).toBe(true)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('6. ML-DSA-87 verify fails with wrong public key', async () => {
|
|
101
|
-
const kp = generatePqKeyPair('ML-DSA-87')
|
|
102
|
-
const wrongKp = generatePqKeyPair('ML-DSA-87')
|
|
103
|
-
const signed = await signEnvelopePq(baseEnvelope, kp.privateKey, 'ML-DSA-87')
|
|
104
|
-
const valid = await verifyEnvelopePq(signed, wrongKp.publicKey)
|
|
105
|
-
expect(valid).toBe(false)
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
describe('algorithm-specific aliases', () => {
|
|
110
|
-
it('signEnvelopeMlDsa65 / verifyEnvelopeMlDsa65 round trip', async () => {
|
|
111
|
-
const kp = generatePqKeyPair('ML-DSA-65')
|
|
112
|
-
const envelope = createEnvelope({
|
|
113
|
-
sender: 'agent-a',
|
|
114
|
-
recipient: 'agent-b',
|
|
115
|
-
intent: 'PING',
|
|
116
|
-
content: 'alias test',
|
|
117
|
-
})
|
|
118
|
-
const signed = await signEnvelopeMlDsa65(envelope, kp.privateKey)
|
|
119
|
-
expect(signed.signature?.alg).toBe('ML-DSA-65')
|
|
120
|
-
const valid = await verifyEnvelopeMlDsa65(signed, kp.publicKey)
|
|
121
|
-
expect(valid).toBe(true)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('signEnvelopeMlDsa87 / verifyEnvelopeMlDsa87 round trip', async () => {
|
|
125
|
-
const kp = generatePqKeyPair('ML-DSA-87')
|
|
126
|
-
const envelope = createEnvelope({
|
|
127
|
-
sender: 'agent-a',
|
|
128
|
-
recipient: 'agent-b',
|
|
129
|
-
intent: 'PONG',
|
|
130
|
-
content: 'ml-dsa-87 alias test',
|
|
131
|
-
})
|
|
132
|
-
const signed = await signEnvelopeMlDsa87(envelope, kp.privateKey)
|
|
133
|
-
expect(signed.signature?.alg).toBe('ML-DSA-87')
|
|
134
|
-
const valid = await verifyEnvelopeMlDsa87(signed, kp.publicKey)
|
|
135
|
-
expect(valid).toBe(true)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('verifyEnvelopePq returns false when no signature', async () => {
|
|
139
|
-
const unsigned = { ...baseEnvelope }
|
|
140
|
-
const valid = await verifyEnvelopePq(unsigned, 'fakepublickey')
|
|
141
|
-
expect(valid).toBe(false)
|
|
142
|
-
})
|
|
143
|
-
})
|
package/sdk/pq/src/index.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa'
|
|
2
|
-
|
|
3
|
-
// Re-export from @7h3/protocol
|
|
4
|
-
export type { ProtocolEnvelope, ProtocolHeader, ProtocolBody } from '../../../src/protocol.js'
|
|
5
|
-
export { canonicalizeEnvelope, createEnvelope } from '../../../src/protocol.js'
|
|
6
|
-
|
|
7
|
-
import type { ProtocolEnvelope, ProtocolHeader, ProtocolBody } from '../../../src/protocol.js'
|
|
8
|
-
import { canonicalizeEnvelope } from '../../../src/protocol.js'
|
|
9
|
-
|
|
10
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
export type PqAlgorithm = 'ML-DSA-65' | 'ML-DSA-87'
|
|
13
|
-
|
|
14
|
-
export interface PqKeyPair {
|
|
15
|
-
algorithm: PqAlgorithm
|
|
16
|
-
publicKey: string // base64url, no padding
|
|
17
|
-
privateKey: string // base64url, no padding (secretKey bytes from noble)
|
|
18
|
-
createdAt: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Extended envelope type that carries a PQ signature.
|
|
23
|
-
* Structurally identical to ProtocolEnvelope but alg is widened.
|
|
24
|
-
*/
|
|
25
|
-
export interface PqProtocolEnvelope {
|
|
26
|
-
header: ProtocolHeader
|
|
27
|
-
body: ProtocolBody
|
|
28
|
-
signature?: {
|
|
29
|
-
alg: PqAlgorithm
|
|
30
|
-
keyId: string
|
|
31
|
-
value: string
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─── Base64url helpers ───────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
function toBase64Url(bytes: Uint8Array): string {
|
|
38
|
-
const g = globalThis as unknown as {
|
|
39
|
-
Buffer?: { from: (b: Uint8Array) => { toString: (enc: string) => string } }
|
|
40
|
-
}
|
|
41
|
-
if (g.Buffer) {
|
|
42
|
-
return g.Buffer.from(bytes)
|
|
43
|
-
.toString('base64')
|
|
44
|
-
.replace(/\+/g, '-')
|
|
45
|
-
.replace(/\//g, '_')
|
|
46
|
-
.replace(/=+$/g, '')
|
|
47
|
-
}
|
|
48
|
-
let binary = ''
|
|
49
|
-
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])
|
|
50
|
-
return btoa(binary)
|
|
51
|
-
.replace(/\+/g, '-')
|
|
52
|
-
.replace(/\//g, '_')
|
|
53
|
-
.replace(/=+$/g, '')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function fromBase64Url(value: string): Uint8Array {
|
|
57
|
-
const padded = value
|
|
58
|
-
.replace(/-/g, '+')
|
|
59
|
-
.replace(/_/g, '/')
|
|
60
|
-
.padEnd(Math.ceil(value.length / 4) * 4, '=')
|
|
61
|
-
|
|
62
|
-
const g = globalThis as unknown as {
|
|
63
|
-
Buffer?: { from: (s: string, enc: string) => Uint8Array }
|
|
64
|
-
}
|
|
65
|
-
if (g.Buffer) {
|
|
66
|
-
return new Uint8Array(g.Buffer.from(padded, 'base64'))
|
|
67
|
-
}
|
|
68
|
-
const binary = atob(padded)
|
|
69
|
-
const bytes = new Uint8Array(binary.length)
|
|
70
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
|
71
|
-
return bytes
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── Key generation ──────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
export function generatePqKeyPair(algorithm: PqAlgorithm = 'ML-DSA-65'): PqKeyPair {
|
|
77
|
-
const impl = algorithm === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87
|
|
78
|
-
const seed = crypto.getRandomValues(new Uint8Array(32))
|
|
79
|
-
const { publicKey, secretKey } = impl.keygen(seed)
|
|
80
|
-
return {
|
|
81
|
-
algorithm,
|
|
82
|
-
publicKey: toBase64Url(publicKey),
|
|
83
|
-
privateKey: toBase64Url(secretKey),
|
|
84
|
-
createdAt: Date.now(),
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ─── Sign ────────────────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
export async function signEnvelopePq(
|
|
91
|
-
envelope: Omit<ProtocolEnvelope, 'signature'>,
|
|
92
|
-
privateKeyBase64Url: string,
|
|
93
|
-
algorithm: PqAlgorithm = 'ML-DSA-65',
|
|
94
|
-
): Promise<PqProtocolEnvelope> {
|
|
95
|
-
const impl = algorithm === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87
|
|
96
|
-
const canonical = canonicalizeEnvelope(envelope)
|
|
97
|
-
const message = new TextEncoder().encode(canonical)
|
|
98
|
-
const secretKey = fromBase64Url(privateKeyBase64Url)
|
|
99
|
-
const sigBytes = impl.sign(secretKey, message)
|
|
100
|
-
// keyId derived from first 16 chars of the secretKey base64url
|
|
101
|
-
const keyId = privateKeyBase64Url.slice(0, 16)
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
header: envelope.header,
|
|
105
|
-
body: envelope.body,
|
|
106
|
-
signature: {
|
|
107
|
-
alg: algorithm,
|
|
108
|
-
keyId,
|
|
109
|
-
value: toBase64Url(sigBytes),
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ─── Verify ──────────────────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
export async function verifyEnvelopePq(
|
|
117
|
-
envelope: PqProtocolEnvelope,
|
|
118
|
-
publicKeyBase64Url: string,
|
|
119
|
-
): Promise<boolean> {
|
|
120
|
-
if (!envelope.signature) return false
|
|
121
|
-
const alg = envelope.signature.alg
|
|
122
|
-
if (alg !== 'ML-DSA-65' && alg !== 'ML-DSA-87') return false
|
|
123
|
-
|
|
124
|
-
const impl = alg === 'ML-DSA-65' ? ml_dsa65 : ml_dsa87
|
|
125
|
-
const unsigned = { header: envelope.header, body: envelope.body }
|
|
126
|
-
const canonical = canonicalizeEnvelope(unsigned)
|
|
127
|
-
const message = new TextEncoder().encode(canonical)
|
|
128
|
-
const publicKey = fromBase64Url(publicKeyBase64Url)
|
|
129
|
-
const sigBytes = fromBase64Url(envelope.signature.value)
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
return impl.verify(publicKey, message, sigBytes)
|
|
133
|
-
} catch {
|
|
134
|
-
return false
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ─── Algorithm-specific aliases ──────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
export async function signEnvelopeMlDsa65(
|
|
141
|
-
envelope: Omit<ProtocolEnvelope, 'signature'>,
|
|
142
|
-
privateKeyBase64Url: string,
|
|
143
|
-
): Promise<PqProtocolEnvelope> {
|
|
144
|
-
return signEnvelopePq(envelope, privateKeyBase64Url, 'ML-DSA-65')
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function signEnvelopeMlDsa87(
|
|
148
|
-
envelope: Omit<ProtocolEnvelope, 'signature'>,
|
|
149
|
-
privateKeyBase64Url: string,
|
|
150
|
-
): Promise<PqProtocolEnvelope> {
|
|
151
|
-
return signEnvelopePq(envelope, privateKeyBase64Url, 'ML-DSA-87')
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function verifyEnvelopeMlDsa65(
|
|
155
|
-
envelope: PqProtocolEnvelope,
|
|
156
|
-
publicKeyBase64Url: string,
|
|
157
|
-
): Promise<boolean> {
|
|
158
|
-
return verifyEnvelopePq(envelope, publicKeyBase64Url)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function verifyEnvelopeMlDsa87(
|
|
162
|
-
envelope: PqProtocolEnvelope,
|
|
163
|
-
publicKeyBase64Url: string,
|
|
164
|
-
): Promise<boolean> {
|
|
165
|
-
return verifyEnvelopePq(envelope, publicKeyBase64Url)
|
|
166
|
-
}
|
package/sdk/pq/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "../../../",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"types": ["node"]
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/index.ts", "../../../src/protocol.ts"]
|
|
14
|
-
}
|
package/sdk/pq/vitest.config.ts
DELETED
package/sdk/python/README.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# 7h3 Protocol AIP Python SDK (Skeleton)
|
|
2
|
-
|
|
3
|
-
Minimal reference SDK for `aip/0.1` parity with the TypeScript implementation.
|
|
4
|
-
|
|
5
|
-
## Included
|
|
6
|
-
|
|
7
|
-
- deterministic canonicalization
|
|
8
|
-
- HS256 HMAC signing and verification
|
|
9
|
-
- ED25519 signing and verification (requires `cryptography` package)
|
|
10
|
-
- compact wire encode/decode
|
|
11
|
-
- envelope validation helpers (version, required fields, TTL)
|
|
12
|
-
- conformance tests using shared vectors from `conformance/aip_v0_1.json`
|
|
13
|
-
|
|
14
|
-
## Run conformance tests
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
PYTHONPATH=sdk/python python3 -m unittest discover -s sdk/python/tests -v
|
|
18
|
-
```
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
from .protocol import ( # noqa: F401
|
|
2
|
-
canonicalize_envelope,
|
|
3
|
-
decode_envelope,
|
|
4
|
-
encode_envelope_compact,
|
|
5
|
-
sign_canonical_payload_ed25519,
|
|
6
|
-
sign_canonical_payload_hmac,
|
|
7
|
-
sign_envelope_ed25519,
|
|
8
|
-
sign_envelope_hmac,
|
|
9
|
-
validate_envelope,
|
|
10
|
-
verify_canonical_payload_ed25519,
|
|
11
|
-
verify_canonical_payload_hmac,
|
|
12
|
-
verify_envelope_ed25519,
|
|
13
|
-
verify_envelope_hmac,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
from .http import ( # noqa: F401
|
|
18
|
-
DEFAULT_HEADER, KeyRegistry, StaticKeyRegistry,
|
|
19
|
-
verify_http_envelope, sign_http_request, build_signed_request_headers,
|
|
20
|
-
)
|
|
21
|
-
except ImportError:
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
try:
|
|
25
|
-
from .webhook import ( # noqa: F401
|
|
26
|
-
WEBHOOK_SIG_HEADER, WEBHOOK_TS_HEADER,
|
|
27
|
-
sign_webhook, sign_webhook_hmac, verify_webhook, verify_webhook_hmac, consume_webhook,
|
|
28
|
-
)
|
|
29
|
-
except ImportError:
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
from .queue import ( # noqa: F401
|
|
34
|
-
sign_queue_message, verify_queue_message, verify_queue_batch,
|
|
35
|
-
)
|
|
36
|
-
except ImportError:
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
try:
|
|
40
|
-
from .keys import ( # noqa: F401
|
|
41
|
-
KeyEntry, WellKnownKeysDocument, ManagedKeyPair,
|
|
42
|
-
KeyRotationManager, RevocationRegistry,
|
|
43
|
-
fetch_well_known_keys,
|
|
44
|
-
)
|
|
45
|
-
except ImportError:
|
|
46
|
-
pass
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
E2E Encryption for 7h3 Protocol — Python SDK
|
|
3
|
-
|
|
4
|
-
Uses X25519 Diffie-Hellman key exchange + ChaCha20-Poly1305 AEAD.
|
|
5
|
-
Requires: pip install cryptography
|
|
6
|
-
|
|
7
|
-
EncryptedEnvelope = SignedEnvelope where body['content'] is a base64url-encoded
|
|
8
|
-
EncryptedPayload JSON, body['intent'] = 'ENCRYPTED',
|
|
9
|
-
body['capability'] = 'x25519-chacha20poly1305'
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import base64
|
|
15
|
-
import json
|
|
16
|
-
import os
|
|
17
|
-
from typing import Any, Dict, Tuple
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
from cryptography.hazmat.primitives.asymmetric.x25519 import (
|
|
21
|
-
X25519PrivateKey,
|
|
22
|
-
X25519PublicKey,
|
|
23
|
-
)
|
|
24
|
-
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
25
|
-
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
26
|
-
from cryptography.hazmat.primitives import hashes
|
|
27
|
-
from cryptography.hazmat.primitives import serialization
|
|
28
|
-
except ImportError as exc:
|
|
29
|
-
raise ImportError(
|
|
30
|
-
"7h3/encryption requires the 'cryptography' package. "
|
|
31
|
-
"Install it with: pip install cryptography"
|
|
32
|
-
) from exc
|
|
33
|
-
|
|
34
|
-
from .protocol import (
|
|
35
|
-
sign_envelope_ed25519,
|
|
36
|
-
verify_envelope_ed25519,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# ---------------------------------------------------------------------------
|
|
41
|
-
# Utility
|
|
42
|
-
# ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _b64url_encode(data: bytes) -> str:
|
|
46
|
-
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _b64url_decode(value: str) -> bytes:
|
|
50
|
-
padding = "=" * ((4 - (len(value) % 4)) % 4)
|
|
51
|
-
return base64.urlsafe_b64decode(value + padding)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# ---------------------------------------------------------------------------
|
|
55
|
-
# Types (documented shapes, not enforced at runtime)
|
|
56
|
-
# ---------------------------------------------------------------------------
|
|
57
|
-
# X25519KeyPair: tuple[str, str] = (publicKey_base64url, privateKey_base64url)
|
|
58
|
-
# EncryptedPayload dict keys: ephemeralPublic, nonce, ciphertext, tag
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# ---------------------------------------------------------------------------
|
|
62
|
-
# Key generation
|
|
63
|
-
# ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def generate_x25519_keypair() -> Tuple[str, str]:
|
|
67
|
-
"""
|
|
68
|
-
Generate a fresh X25519 keypair.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
(public_base64url, private_base64url) — both are raw 32 bytes, base64url-encoded
|
|
72
|
-
"""
|
|
73
|
-
private_key = X25519PrivateKey.generate()
|
|
74
|
-
public_key = private_key.public_key()
|
|
75
|
-
|
|
76
|
-
priv_raw = private_key.private_bytes(
|
|
77
|
-
serialization.Encoding.Raw,
|
|
78
|
-
serialization.PrivateFormat.Raw,
|
|
79
|
-
serialization.NoEncryption(),
|
|
80
|
-
)
|
|
81
|
-
pub_raw = public_key.public_bytes(
|
|
82
|
-
serialization.Encoding.Raw,
|
|
83
|
-
serialization.PublicFormat.Raw,
|
|
84
|
-
)
|
|
85
|
-
return _b64url_encode(pub_raw), _b64url_encode(priv_raw)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# ---------------------------------------------------------------------------
|
|
89
|
-
# HKDF key derivation
|
|
90
|
-
# ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _derive_encryption_key(
|
|
94
|
-
private_key_b64: str,
|
|
95
|
-
peer_public_key_b64: str,
|
|
96
|
-
nonce_b64: str,
|
|
97
|
-
) -> bytes:
|
|
98
|
-
"""
|
|
99
|
-
X25519 DH + HKDF-SHA256 → 32 bytes for ChaCha20-Poly1305.
|
|
100
|
-
|
|
101
|
-
nonce_b64: base64url-encoded 12-byte ChaCha nonce; used as HKDF salt.
|
|
102
|
-
"""
|
|
103
|
-
priv_bytes = _b64url_decode(private_key_b64)
|
|
104
|
-
pub_bytes = _b64url_decode(peer_public_key_b64)
|
|
105
|
-
nonce_bytes = _b64url_decode(nonce_b64)
|
|
106
|
-
|
|
107
|
-
private_key = X25519PrivateKey.from_private_bytes(priv_bytes)
|
|
108
|
-
public_key = X25519PublicKey.from_public_bytes(pub_bytes)
|
|
109
|
-
|
|
110
|
-
shared_secret = private_key.exchange(public_key)
|
|
111
|
-
|
|
112
|
-
hkdf = HKDF(
|
|
113
|
-
algorithm=hashes.SHA256(),
|
|
114
|
-
length=32,
|
|
115
|
-
salt=nonce_bytes,
|
|
116
|
-
info=b"7h3-enc/1",
|
|
117
|
-
)
|
|
118
|
-
return hkdf.derive(shared_secret)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ---------------------------------------------------------------------------
|
|
122
|
-
# Body encryption / decryption
|
|
123
|
-
# ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _encrypt_body(
|
|
127
|
-
body: Dict[str, Any],
|
|
128
|
-
recipient_x25519_public_b64: str,
|
|
129
|
-
) -> Dict[str, str]:
|
|
130
|
-
"""
|
|
131
|
-
Encrypt a protocol body dict.
|
|
132
|
-
|
|
133
|
-
Returns EncryptedPayload dict with: ephemeralPublic, nonce, ciphertext, tag
|
|
134
|
-
"""
|
|
135
|
-
# Generate ephemeral keypair for forward secrecy
|
|
136
|
-
ephemeral_pub_b64, ephemeral_priv_b64 = generate_x25519_keypair()
|
|
137
|
-
|
|
138
|
-
# Random 12-byte ChaCha nonce (also used as HKDF salt)
|
|
139
|
-
nonce12 = os.urandom(12)
|
|
140
|
-
nonce_b64 = _b64url_encode(nonce12)
|
|
141
|
-
|
|
142
|
-
# Derive encryption key
|
|
143
|
-
key = _derive_encryption_key(ephemeral_priv_b64, recipient_x25519_public_b64, nonce_b64)
|
|
144
|
-
|
|
145
|
-
# Encrypt body as JSON
|
|
146
|
-
plaintext = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
147
|
-
aead = ChaCha20Poly1305(key)
|
|
148
|
-
# ChaCha20Poly1305.encrypt returns ciphertext‖tag (tag is last 16 bytes)
|
|
149
|
-
ct_with_tag = aead.encrypt(nonce12, plaintext, None)
|
|
150
|
-
ciphertext = ct_with_tag[:-16]
|
|
151
|
-
tag = ct_with_tag[-16:]
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
"ephemeralPublic": ephemeral_pub_b64,
|
|
155
|
-
"nonce": nonce_b64,
|
|
156
|
-
"ciphertext": _b64url_encode(ciphertext),
|
|
157
|
-
"tag": _b64url_encode(tag),
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _decrypt_body(
|
|
162
|
-
encrypted_content_b64: str,
|
|
163
|
-
recipient_x25519_private_b64: str,
|
|
164
|
-
) -> Dict[str, Any]:
|
|
165
|
-
"""
|
|
166
|
-
Decrypt an EncryptedPayload back to a protocol body dict.
|
|
167
|
-
|
|
168
|
-
Raises ValueError if AEAD tag verification fails.
|
|
169
|
-
"""
|
|
170
|
-
payload_json = _b64url_decode(encrypted_content_b64).decode("utf-8")
|
|
171
|
-
payload = json.loads(payload_json)
|
|
172
|
-
|
|
173
|
-
ephemeral_pub_b64 = payload["ephemeralPublic"]
|
|
174
|
-
nonce_b64 = payload["nonce"]
|
|
175
|
-
ciphertext_b64 = payload["ciphertext"]
|
|
176
|
-
tag_b64 = payload["tag"]
|
|
177
|
-
|
|
178
|
-
key = _derive_encryption_key(recipient_x25519_private_b64, ephemeral_pub_b64, nonce_b64)
|
|
179
|
-
|
|
180
|
-
nonce12 = _b64url_decode(nonce_b64)
|
|
181
|
-
ciphertext = _b64url_decode(ciphertext_b64)
|
|
182
|
-
tag = _b64url_decode(tag_b64)
|
|
183
|
-
|
|
184
|
-
aead = ChaCha20Poly1305(key)
|
|
185
|
-
# Re-concatenate ciphertext‖tag for decryption
|
|
186
|
-
ct_with_tag = ciphertext + tag
|
|
187
|
-
plaintext = aead.decrypt(nonce12, ct_with_tag, None)
|
|
188
|
-
return json.loads(plaintext.decode("utf-8"))
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# ---------------------------------------------------------------------------
|
|
192
|
-
# Envelope-level seal / open
|
|
193
|
-
# ---------------------------------------------------------------------------
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def seal_envelope(
|
|
197
|
-
envelope: Dict[str, Any],
|
|
198
|
-
recipient_x25519_public_b64: str,
|
|
199
|
-
sender_ed25519_private_b64: str,
|
|
200
|
-
) -> Dict[str, Any]:
|
|
201
|
-
"""
|
|
202
|
-
Encrypt the envelope body and sign the result with Ed25519.
|
|
203
|
-
|
|
204
|
-
The original body is encrypted; the envelope body is replaced with:
|
|
205
|
-
{ intent: 'ENCRYPTED', content: <encrypted-payload>, capability: 'x25519-chacha20poly1305' }
|
|
206
|
-
The modified envelope is signed with Ed25519.
|
|
207
|
-
"""
|
|
208
|
-
encrypted_payload = _encrypt_body(envelope["body"], recipient_x25519_public_b64)
|
|
209
|
-
encrypted_content = _b64url_encode(
|
|
210
|
-
json.dumps(encrypted_payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
encrypted_body: Dict[str, Any] = {
|
|
214
|
-
"intent": "ENCRYPTED",
|
|
215
|
-
"content": encrypted_content,
|
|
216
|
-
"capability": "x25519-chacha20poly1305",
|
|
217
|
-
}
|
|
218
|
-
# Preserve correlationId if present
|
|
219
|
-
if "correlationId" in envelope["body"] and envelope["body"]["correlationId"] is not None:
|
|
220
|
-
encrypted_body["correlationId"] = envelope["body"]["correlationId"]
|
|
221
|
-
|
|
222
|
-
unsigned = {
|
|
223
|
-
"header": envelope["header"],
|
|
224
|
-
"body": encrypted_body,
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return sign_envelope_ed25519(unsigned, sender_ed25519_private_b64)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def open_envelope(
|
|
231
|
-
envelope: Dict[str, Any],
|
|
232
|
-
recipient_x25519_private_b64: str,
|
|
233
|
-
sender_ed25519_public_b64: str,
|
|
234
|
-
) -> Dict[str, Any]:
|
|
235
|
-
"""
|
|
236
|
-
Verify Ed25519 signature, then decrypt the body.
|
|
237
|
-
|
|
238
|
-
Signature is verified FIRST — decryption only proceeds if valid.
|
|
239
|
-
|
|
240
|
-
Returns a dict with:
|
|
241
|
-
'envelope': the signed envelope (with encrypted body)
|
|
242
|
-
'body': the decrypted original ProtocolBody dict
|
|
243
|
-
|
|
244
|
-
Raises ValueError if signature is invalid or AEAD tag fails.
|
|
245
|
-
"""
|
|
246
|
-
# 1. Verify Ed25519 signature FIRST
|
|
247
|
-
if not verify_envelope_ed25519(envelope, sender_ed25519_public_b64):
|
|
248
|
-
raise ValueError("7h3/encryption: Ed25519 signature verification failed")
|
|
249
|
-
|
|
250
|
-
# 2. Decrypt
|
|
251
|
-
body = _decrypt_body(envelope["body"]["content"], recipient_x25519_private_b64)
|
|
252
|
-
return {"envelope": envelope, "body": body}
|