atp-did 0.1.0__tar.gz

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.
@@ -0,0 +1,253 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+ .yarn/cache/
5
+ .yarn/unplugged/
6
+
7
+ # Next.js
8
+ .next/
9
+ out/
10
+ build/
11
+
12
+ # Environment variables
13
+ .env*
14
+ !.env.example
15
+ !.env.docker.example
16
+ # Explicitly ignore all env variants (in case previously committed)
17
+ .env.production
18
+ .env.staging
19
+ .env.local
20
+ .env.production.example
21
+ .env.production.template
22
+
23
+ # Debug
24
+ npm-debug.log*
25
+ yarn-debug.log*
26
+ yarn-error.log*
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+
34
+ # IDE/Tool configurations (local development)
35
+ .claude/
36
+ .zenflow/
37
+ .zencoder/
38
+
39
+ # Local W3C CG staging bundle and generated download artifacts (kept local,
40
+ # not distributed via the repo; see GitHub Pages if/when published)
41
+ /w3c-cg-upload/
42
+ /atp-w3c-cg-drafts-*.zip
43
+ /atp-w3c-cg-drafts-*.tar.gz
44
+
45
+ # OS generated files
46
+ .DS_Store
47
+ .DS_Store?
48
+ ._*
49
+ .Spotlight-V100
50
+ .Trashes
51
+ ehthumbs.db
52
+ Thumbs.db
53
+
54
+ # Logs
55
+ *.log
56
+ logs/
57
+ server.log
58
+ .vercel
59
+
60
+ # Backup files and archives
61
+ .backup/
62
+ *.bak
63
+ *.old/
64
+ **/*.old
65
+ **/*.old/**
66
+ *.tar.gz
67
+ *.tgz
68
+
69
+ # Internal scripts and tooling
70
+ activate-*.sh
71
+ deactivate-*.sh
72
+ check-*.sh
73
+ deploy-*.sh
74
+ fix-*.sh
75
+ test-*.sh
76
+ push-to-github*.sh
77
+ migrate-to-*.sh
78
+ public-launch.sh
79
+ run-with-real-data.sh
80
+ e2e-*.sh
81
+ CORRECTED-*.sh
82
+ diagnose-*.sh
83
+ quick-*.sh
84
+ force-*.sh
85
+ godaddy-*.sh
86
+ health-*.sh
87
+ !scripts/dev/health-check.sh
88
+ manual-*.sh
89
+ mock-*.sh
90
+ monitor-*.sh
91
+ move-*.sh
92
+ reset-*.sh
93
+ restart-*.sh
94
+ setup-*.sh
95
+ start-*.sh
96
+ stop-*.sh
97
+ update-*.sh
98
+ verify-*.sh
99
+ wait-*.sh
100
+
101
+ # Internal documentation
102
+ *_GUIDE.md
103
+ *_INSTRUCTIONS.md
104
+ *_SUMMARY.md
105
+ *_COMPLETED.md
106
+ *_MIGRATION.md
107
+ *_IMPLEMENTATION.md
108
+ *_MODE.md
109
+ CLERK_*.md
110
+ ENTERPRISE_*.md
111
+ MAINTENANCE*.md
112
+ POSTGRESQL_*.md
113
+ PRIORITY_*.md
114
+ COMPLETE_*.md
115
+ ENV_*.md
116
+ README-*.md
117
+ !README.md
118
+
119
+ # SSL certificates and keys
120
+ ssl/
121
+ certs/
122
+
123
+ # Coverage reports
124
+ coverage/
125
+
126
+ # Secrets and sensitive files
127
+ secrets/
128
+ *.key
129
+ *.pem
130
+ *.crt
131
+
132
+ # Build artifacts
133
+ dist/
134
+ **/dist/
135
+ *.tsbuildinfo
136
+ .turbo/
137
+ .turbopack/
138
+
139
+ # Database files
140
+ *.db
141
+ *.sqlite
142
+ *.sqlite3
143
+
144
+ # Internal monitoring and configs
145
+ /monitoring/
146
+ ecosystem.config.js
147
+
148
+ # AI/ML model files and data
149
+ *.pkl
150
+ *.h5
151
+ *.pb
152
+ data/
153
+ collections/
154
+
155
+ # Temporary files
156
+ tmp/
157
+ temp/
158
+ .cache/
159
+ .eslintcache
160
+ .rts2_cache_*/
161
+ .nyc_output/
162
+ lerna-debug.log
163
+
164
+ # MCP config (may contain tokens)
165
+ .mcp.json
166
+
167
+ # AI assistant configurations
168
+ .clinerules/
169
+ .kilocode/
170
+ .roo/
171
+ .windsurf/
172
+ .cursor/
173
+ .giga/
174
+ .roomodes
175
+ .kiro/
176
+ .qoder/
177
+ .augment/
178
+ .agent-os/
179
+ .bmad-*/
180
+ .copilot/
181
+ .gemini/
182
+ .trae/
183
+
184
+ # Docker internal files
185
+ docker-build-test.log
186
+ build.log
187
+ build-*.log
188
+
189
+ # Test artifacts
190
+ test-results/
191
+ benchmark.ts
192
+ test-*.js
193
+ test-*.cjs
194
+ test-scenarios/
195
+
196
+ # Root level scripts (internal)
197
+ add-ssh-key.sh
198
+ build-packages-locally.sh
199
+ configure-ssl.sh
200
+ generate-dev-certs.sh
201
+ root@165.227.13.206
202
+
203
+ # Accidental local paths
204
+ Users/
205
+
206
+ # Runtime files
207
+ .atp-pids
208
+ *.pids
209
+
210
+ # Deployment archives and backups (belong on server, not in repo)
211
+ deployment-backups/
212
+ website-repo-old/
213
+ atp-ip-protection-deploy.tar.gz
214
+ *.tar.gz
215
+ .npmrc
216
+
217
+ # Local dev artifacts (user-specific, not part of the project)
218
+ quicksort.js
219
+ npm-publish.sh
220
+ release-v*.sh
221
+
222
+ # User-created agent profiles (local only)
223
+ *-agent-bot/
224
+ *-agent/
225
+ # First-party CLI package name ends with -agent; keep it tracked
226
+ !packages/create-atp-agent/
227
+ !packages/create-atp-agent/**
228
+ packages/create-atp-agent/node_modules/
229
+ packages/create-atp-agent/dist/
230
+ packages/create-atp-agent/.create-atp-agent/
231
+ *-security/
232
+ security-*/
233
+ agent-*/
234
+ # First-party Python umbrella package; keep it tracked despite agent-*/
235
+ !packages/agent-trust-protocol-python/
236
+ !packages/agent-trust-protocol-python/**
237
+ .*.agent.md
238
+ .env*.local
239
+
240
+ # Generated SBOM (produced on release in CI, attached to the release)
241
+ sbom.json
242
+
243
+ # W3C CG upload staging area (manual copy-out; not tracked source)
244
+ w3c-cg-upload/
245
+
246
+ # Monitoring native storage test build
247
+ packages/monitoring-service/.test-dist/
248
+
249
+ # Python build and cache artifacts
250
+ __pycache__/
251
+ *.py[cod]
252
+ *.egg-info/
253
+ .venv/
atp_did-0.1.0/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2025 The Agent Trust Protocol Contributors
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
atp_did-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.5
2
+ Name: atp-did
3
+ Version: 0.1.0
4
+ Summary: Experimental offline Agent Trust Protocol DID identifier and key-binding primitives
5
+ Project-URL: Homepage, https://agenttrustprotocol.com
6
+ Project-URL: Source, https://github.com/agent-trust-protocol/atp-core
7
+ Project-URL: Issues, https://github.com/agent-trust-protocol/atp-core/issues
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: agent-identity,agent-trust-protocol,did,hybrid-signatures
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security :: Cryptography
15
+ Requires-Python: >=3.9
16
+ Provides-Extra: crypto
17
+ Requires-Dist: cryptography<51,>=44; extra == 'crypto'
18
+ Requires-Dist: pqcrypto==0.3.4; extra == 'crypto'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ATP DID primitives for Python (experimental)
22
+
23
+ Offline Python counterpart of `@atpdeveloper/did-atp`. The distribution name
24
+ is `atp-did`; check PyPI for release availability. This module is experimental.
25
+ Python 3.9+ for base primitives; no base runtime dependencies. Optional signing
26
+ is experimental and currently exercised on Linux/Python 3.12. This is not the
27
+ full Python identity SDK.
28
+
29
+ From the repository root:
30
+
31
+ ```sh
32
+ python -m pip install ./packages/did-atp-python
33
+ ```
34
+
35
+ ```python
36
+ from atp_did import build_atp_v2_did, parse_atp_v2_path_did, verify_atp_v2_key_bindings
37
+
38
+ # Format demonstration only. Supply real public keys in an application.
39
+ ed, pq = bytes(32), bytes(1952)
40
+ did = build_atp_v2_did('example.com', ['agents', 'demo'], ed, pq)
41
+ assert parse_atp_v2_path_did(did).path == ('agents', 'demo')
42
+ assert verify_atp_v2_key_bindings(did, ed, pq)
43
+ ```
44
+
45
+ The public functions use snake_case equivalents of the TypeScript primitives:
46
+ `atp_base64_url`, `atp_jwk_thumbprint`, `atp_e1_fingerprint`,
47
+ `atp_pq1_fingerprint`, `build_atp_v2_did`, `parse_atp_v2_path_did`,
48
+ `is_atp_v2_path_did`, and `verify_atp_v2_key_bindings`.
49
+ Parsed results are immutable dataclasses, with a tuple path. Parse errors carry
50
+ `invalid_format`, `invalid_domain`, `invalid_path` or `invalid_bindings` in `code`.
51
+ Builders reject malformed inputs; binding checks throw on malformed DIDs/keys
52
+ and return false on a valid identifier with mismatching keys. JWK thumbprints
53
+ select required public members and ignore optional/private members.
54
+
55
+ A matching fingerprint does not prove key possession, cryptographic signature
56
+ validity, trust, current status or authorization. The base module has no key generation or
57
+ signing. There is no network resolver, keystore, mandate verifier, sandbox or
58
+ host adapter. The optional crypto module below adds key generation and signing.
59
+ No W3C certification or full SDK parity is claimed. Community Group drafts and
60
+ implementation releases have separate lifecycles.
61
+
62
+ Tests reuse the existing canonical vectors and shared malformed-identifier cases:
63
+
64
+ ```sh
65
+ PYTHONPATH=packages/did-atp-python/src python -m unittest discover -s packages/did-atp-python/tests
66
+ npm ci --prefix packages/did-atp
67
+ npm test --prefix packages/did-atp
68
+ ```
69
+
70
+ ## Optional signing (experimental)
71
+
72
+ ```sh
73
+ python -m pip install './packages/did-atp-python[crypto]'
74
+ ```
75
+
76
+ ```python
77
+ from atp_did import build_atp_v2_did
78
+ from atp_did.crypto import generate_hybrid_key_pair, sign_hybrid, verify_hybrid
79
+
80
+ keys = generate_hybrid_key_pair()
81
+ did = build_atp_v2_did('example.com', ['agents', 'demo'],
82
+ keys.public_keys.ed25519, keys.public_keys.ml_dsa65)
83
+ message = b'example challenge bytes'
84
+ signature = sign_hybrid(message, keys)
85
+ assert verify_hybrid(message, signature, keys.public_keys)
86
+ assert not verify_hybrid(message + b'!', signature, keys.public_keys)
87
+ ```
88
+
89
+ `sign_ed25519` / `verify_ed25519` and `sign_ml_dsa65` / `verify_ml_dsa65`
90
+ use raw key/signature bytes. Strings are encoded as UTF-8 without normalization;
91
+ objects are not implicitly JSON-serialized. Signing rejects bad key sizes;
92
+ verification returns false for malformed inputs or invalid signatures.
93
+ `verify_hybrid` requires BOTH signatures over identical bytes. Its dataclass
94
+ is a local helper, not a new interoperable ATP proof-envelope specification.
95
+ The SDK's individual algorithm methods are the interoperability reference.
96
+
97
+ Backends: cryptography (Ed25519) and pinned pqcrypto 0.3.4 (PQClean ML-DSA-65).
98
+ The latter is pinned because its tested API differs from pqcrypto 1.x. This is
99
+ pure ML-DSA with an empty FIPS 204 context, not a prehashed variant. Public keys
100
+ are 32/1952 bytes; signatures are 64/3309 bytes. No custom signature algorithm
101
+ is implemented. Backend error conditions are not a reason to downgrade.
102
+
103
+ Key generation uses backend cryptographic randomness. Secret fields are omitted
104
+ from repr, but raw keys remain in process memory: there is no secure keystore,
105
+ encrypted export, guaranteed zeroization or key rotation in this milestone.
106
+ Never log or persist the KeyPair. Callers must bind messages to their own
107
+ challenge, audience, action, nonce and expiry and enforce replay protection;
108
+ a valid signature alone grants no authority. No blanket adversarial equivalence
109
+ between Ed25519 backends or security certification is claimed.
110
+
111
+ The optional suite checks both directions against published `atp-sdk@6.0.2`,
112
+ using ephemeral random keys and public signatures only (no secret fixture files).
113
+ CI installs that version in an isolated temporary directory and sets
114
+ `ATP_SDK_MODULE` to its module URL, then runs with `ATP_TEST_CRYPTO=1`.
115
+ The base matrix still runs without optional dependencies. Broader crypto
116
+ platform/Python support must be verified before promotion or publication.
117
+
118
+ ## Local encrypted key file and onboarding example
119
+
120
+ From the repository root, install the optional extra and run:
121
+
122
+ ```sh
123
+ python -m pip install './packages/did-atp-python[crypto]'
124
+ python packages/did-atp-python/examples/local_identity.py ./my-agent.keys.json --domain example.com --agent demo
125
+ ```
126
+
127
+ The example prompts for a passphrase (at least 16 characters), generates fresh
128
+ Ed25519 and ML-DSA-65 keys, creates an encrypted file with exclusive creation
129
+ and mode 0600, reloads it, builds the DID from its public keys and checks a
130
+ local signature round trip. It refuses to overwrite an existing file. Keep the
131
+ passphrase in a separate password manager; losing it loses access to the keys.
132
+ The example uses `example.com` as a format placeholder and does not register
133
+ or publish a DID, establish domain control, prove third-party trust, or grant
134
+ permission. Never commit the key file or use the demo challenge for a real request.
135
+
136
+ `atp_did.keyfile.save_key_file(path, key_pair, passphrase)` and
137
+ `load_key_file(path, passphrase)` use scrypt (N=131072, r=8, p=1) and
138
+ AES-256-GCM with random salt and nonce and a versioned authenticated envelope.
139
+ Reading rejects symlinks on supported Unix systems, group/world-accessible
140
+ files, oversized files, bad passphrases, tampering and mismatched key pairs.
141
+ This is application-level encryption, not OS keychain or HSM protection:
142
+ passphrases and raw secrets are present in process memory without guaranteed
143
+ zeroization, and host compromise can expose them. There is no key rotation,
144
+ recovery, migration, or cross-device synchronization. Use a managed keystore
145
+ for production deployments and design scoped challenge and replay checks
146
+ before using signatures for actions.
@@ -0,0 +1,126 @@
1
+ # ATP DID primitives for Python (experimental)
2
+
3
+ Offline Python counterpart of `@atpdeveloper/did-atp`. The distribution name
4
+ is `atp-did`; check PyPI for release availability. This module is experimental.
5
+ Python 3.9+ for base primitives; no base runtime dependencies. Optional signing
6
+ is experimental and currently exercised on Linux/Python 3.12. This is not the
7
+ full Python identity SDK.
8
+
9
+ From the repository root:
10
+
11
+ ```sh
12
+ python -m pip install ./packages/did-atp-python
13
+ ```
14
+
15
+ ```python
16
+ from atp_did import build_atp_v2_did, parse_atp_v2_path_did, verify_atp_v2_key_bindings
17
+
18
+ # Format demonstration only. Supply real public keys in an application.
19
+ ed, pq = bytes(32), bytes(1952)
20
+ did = build_atp_v2_did('example.com', ['agents', 'demo'], ed, pq)
21
+ assert parse_atp_v2_path_did(did).path == ('agents', 'demo')
22
+ assert verify_atp_v2_key_bindings(did, ed, pq)
23
+ ```
24
+
25
+ The public functions use snake_case equivalents of the TypeScript primitives:
26
+ `atp_base64_url`, `atp_jwk_thumbprint`, `atp_e1_fingerprint`,
27
+ `atp_pq1_fingerprint`, `build_atp_v2_did`, `parse_atp_v2_path_did`,
28
+ `is_atp_v2_path_did`, and `verify_atp_v2_key_bindings`.
29
+ Parsed results are immutable dataclasses, with a tuple path. Parse errors carry
30
+ `invalid_format`, `invalid_domain`, `invalid_path` or `invalid_bindings` in `code`.
31
+ Builders reject malformed inputs; binding checks throw on malformed DIDs/keys
32
+ and return false on a valid identifier with mismatching keys. JWK thumbprints
33
+ select required public members and ignore optional/private members.
34
+
35
+ A matching fingerprint does not prove key possession, cryptographic signature
36
+ validity, trust, current status or authorization. The base module has no key generation or
37
+ signing. There is no network resolver, keystore, mandate verifier, sandbox or
38
+ host adapter. The optional crypto module below adds key generation and signing.
39
+ No W3C certification or full SDK parity is claimed. Community Group drafts and
40
+ implementation releases have separate lifecycles.
41
+
42
+ Tests reuse the existing canonical vectors and shared malformed-identifier cases:
43
+
44
+ ```sh
45
+ PYTHONPATH=packages/did-atp-python/src python -m unittest discover -s packages/did-atp-python/tests
46
+ npm ci --prefix packages/did-atp
47
+ npm test --prefix packages/did-atp
48
+ ```
49
+
50
+ ## Optional signing (experimental)
51
+
52
+ ```sh
53
+ python -m pip install './packages/did-atp-python[crypto]'
54
+ ```
55
+
56
+ ```python
57
+ from atp_did import build_atp_v2_did
58
+ from atp_did.crypto import generate_hybrid_key_pair, sign_hybrid, verify_hybrid
59
+
60
+ keys = generate_hybrid_key_pair()
61
+ did = build_atp_v2_did('example.com', ['agents', 'demo'],
62
+ keys.public_keys.ed25519, keys.public_keys.ml_dsa65)
63
+ message = b'example challenge bytes'
64
+ signature = sign_hybrid(message, keys)
65
+ assert verify_hybrid(message, signature, keys.public_keys)
66
+ assert not verify_hybrid(message + b'!', signature, keys.public_keys)
67
+ ```
68
+
69
+ `sign_ed25519` / `verify_ed25519` and `sign_ml_dsa65` / `verify_ml_dsa65`
70
+ use raw key/signature bytes. Strings are encoded as UTF-8 without normalization;
71
+ objects are not implicitly JSON-serialized. Signing rejects bad key sizes;
72
+ verification returns false for malformed inputs or invalid signatures.
73
+ `verify_hybrid` requires BOTH signatures over identical bytes. Its dataclass
74
+ is a local helper, not a new interoperable ATP proof-envelope specification.
75
+ The SDK's individual algorithm methods are the interoperability reference.
76
+
77
+ Backends: cryptography (Ed25519) and pinned pqcrypto 0.3.4 (PQClean ML-DSA-65).
78
+ The latter is pinned because its tested API differs from pqcrypto 1.x. This is
79
+ pure ML-DSA with an empty FIPS 204 context, not a prehashed variant. Public keys
80
+ are 32/1952 bytes; signatures are 64/3309 bytes. No custom signature algorithm
81
+ is implemented. Backend error conditions are not a reason to downgrade.
82
+
83
+ Key generation uses backend cryptographic randomness. Secret fields are omitted
84
+ from repr, but raw keys remain in process memory: there is no secure keystore,
85
+ encrypted export, guaranteed zeroization or key rotation in this milestone.
86
+ Never log or persist the KeyPair. Callers must bind messages to their own
87
+ challenge, audience, action, nonce and expiry and enforce replay protection;
88
+ a valid signature alone grants no authority. No blanket adversarial equivalence
89
+ between Ed25519 backends or security certification is claimed.
90
+
91
+ The optional suite checks both directions against published `atp-sdk@6.0.2`,
92
+ using ephemeral random keys and public signatures only (no secret fixture files).
93
+ CI installs that version in an isolated temporary directory and sets
94
+ `ATP_SDK_MODULE` to its module URL, then runs with `ATP_TEST_CRYPTO=1`.
95
+ The base matrix still runs without optional dependencies. Broader crypto
96
+ platform/Python support must be verified before promotion or publication.
97
+
98
+ ## Local encrypted key file and onboarding example
99
+
100
+ From the repository root, install the optional extra and run:
101
+
102
+ ```sh
103
+ python -m pip install './packages/did-atp-python[crypto]'
104
+ python packages/did-atp-python/examples/local_identity.py ./my-agent.keys.json --domain example.com --agent demo
105
+ ```
106
+
107
+ The example prompts for a passphrase (at least 16 characters), generates fresh
108
+ Ed25519 and ML-DSA-65 keys, creates an encrypted file with exclusive creation
109
+ and mode 0600, reloads it, builds the DID from its public keys and checks a
110
+ local signature round trip. It refuses to overwrite an existing file. Keep the
111
+ passphrase in a separate password manager; losing it loses access to the keys.
112
+ The example uses `example.com` as a format placeholder and does not register
113
+ or publish a DID, establish domain control, prove third-party trust, or grant
114
+ permission. Never commit the key file or use the demo challenge for a real request.
115
+
116
+ `atp_did.keyfile.save_key_file(path, key_pair, passphrase)` and
117
+ `load_key_file(path, passphrase)` use scrypt (N=131072, r=8, p=1) and
118
+ AES-256-GCM with random salt and nonce and a versioned authenticated envelope.
119
+ Reading rejects symlinks on supported Unix systems, group/world-accessible
120
+ files, oversized files, bad passphrases, tampering and mismatched key pairs.
121
+ This is application-level encryption, not OS keychain or HSM protection:
122
+ passphrases and raw secrets are present in process memory without guaranteed
123
+ zeroization, and host compromise can expose them. There is no key rotation,
124
+ recovery, migration, or cross-device synchronization. Use a managed keystore
125
+ for production deployments and design scoped challenge and replay checks
126
+ before using signatures for actions.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "atp-did"
7
+ version = "0.1.0"
8
+ description = "Experimental offline Agent Trust Protocol DID identifier and key-binding primitives"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0"
12
+ dependencies = []
13
+ keywords = ["agent-trust-protocol", "did", "agent-identity", "hybrid-signatures"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Security :: Cryptography",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://agenttrustprotocol.com"
23
+ Source = "https://github.com/agent-trust-protocol/atp-core"
24
+ Issues = "https://github.com/agent-trust-protocol/atp-core/issues"
25
+
26
+ [project.optional-dependencies]
27
+ crypto = ["cryptography>=44,<51", "pqcrypto==0.3.4"]
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/atp_did"]
31
+
32
+ [tool.hatch.build.targets.sdist]
33
+ include = ["src/atp_did", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,118 @@
1
+ """Offline did:atp v2 primitives. A key binding is not proof of authority."""
2
+ import base64
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+ ATP_ED25519_PUBLIC_KEY_BYTES = 32
9
+ ATP_ML_DSA_65_PUBLIC_KEY_BYTES = 1952
10
+ ATP_THUMBPRINT_LENGTH = 43
11
+ _E1 = re.compile(r'e1_[A-Za-z0-9_-]{43}', re.ASCII)
12
+ _PQ1 = re.compile(r'pq1_[A-Za-z0-9_-]{43}', re.ASCII)
13
+ _PATH = re.compile(r'[A-Za-z0-9._-]+', re.ASCII)
14
+ _DOMAIN = re.compile(r'(?=.{1,253}(%3[aA]|$))[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*(%3[aA][0-9]{1,5})?', re.ASCII)
15
+
16
+
17
+ def _matches(pattern, value):
18
+ return isinstance(value, str) and pattern.fullmatch(value) is not None
19
+
20
+
21
+ def _bytes(value):
22
+ if not isinstance(value, (bytes, bytearray, memoryview)):
23
+ raise TypeError('Expected public-key bytes')
24
+ return bytes(value)
25
+
26
+
27
+ def atp_base64_url(value):
28
+ return base64.urlsafe_b64encode(_bytes(value)).rstrip(b'=').decode('ascii')
29
+
30
+
31
+ def atp_jwk_thumbprint(jwk):
32
+ members = {'OKP': ('crv', 'kty', 'x'), 'AKP': ('alg', 'kty', 'pub'),
33
+ 'EC': ('crv', 'kty', 'x', 'y'), 'RSA': ('e', 'kty', 'n')}
34
+ if not isinstance(jwk, dict) or not isinstance(jwk.get('kty'), str):
35
+ raise ValueError('Invalid JWK: expected a supported public key type')
36
+ required = members.get(jwk['kty'])
37
+ if required is None:
38
+ raise ValueError('Unsupported JWK key type')
39
+ if any(not isinstance(jwk.get(k), str) or not jwk[k] for k in required):
40
+ raise ValueError('Invalid JWK: missing or invalid public member')
41
+ public = {k: jwk[k] for k in required}
42
+ canonical = json.dumps(public, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
43
+ return atp_base64_url(hashlib.sha256(canonical).digest())
44
+
45
+
46
+ def atp_e1_fingerprint(public_key):
47
+ key = _bytes(public_key)
48
+ if len(key) != ATP_ED25519_PUBLIC_KEY_BYTES:
49
+ raise ValueError('Expected 32 Ed25519 public-key bytes')
50
+ return 'e1_' + atp_jwk_thumbprint({'crv': 'Ed25519', 'kty': 'OKP', 'x': atp_base64_url(key)})
51
+
52
+
53
+ def atp_pq1_fingerprint(public_key):
54
+ key = _bytes(public_key)
55
+ if len(key) != ATP_ML_DSA_65_PUBLIC_KEY_BYTES:
56
+ raise ValueError('Expected 1952 ML-DSA-65 public-key bytes')
57
+ return 'pq1_' + atp_jwk_thumbprint({'alg': 'ML-DSA-65', 'kty': 'AKP', 'pub': atp_base64_url(key)})
58
+
59
+
60
+ def _valid_segment(segment):
61
+ return _matches(_PATH, segment) and not _matches(_E1, segment) and not _matches(_PQ1, segment)
62
+
63
+
64
+ def build_atp_v2_did(domain, path, ed25519_public_key, ml_dsa65_public_key):
65
+ if not _matches(_DOMAIN, domain):
66
+ raise ValueError('Invalid did:atp domain')
67
+ if not isinstance(path, (list, tuple)) or not path or not all(_valid_segment(s) for s in path):
68
+ raise ValueError('Expected at least one valid, unambiguous path segment')
69
+ return ':'.join(('did', 'atp', domain, *path, atp_e1_fingerprint(ed25519_public_key),
70
+ atp_pq1_fingerprint(ml_dsa65_public_key)))
71
+
72
+
73
+ class AtpDidParseError(ValueError):
74
+ """Stable syntax error code matching the TypeScript primitives."""
75
+ def __init__(self, code, message):
76
+ super().__init__(message)
77
+ self.code = code
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class AtpV2PathDid:
82
+ domain: str
83
+ path: tuple
84
+ e1: str
85
+ pq1: str
86
+
87
+
88
+ def parse_atp_v2_path_did(did):
89
+ if not isinstance(did, str) or not did.startswith('did:atp:'):
90
+ raise AtpDidParseError('invalid_format', 'Expected a bare did:atp path identifier')
91
+ parts = did[8:].split(':')
92
+ if len(parts) < 4:
93
+ raise AtpDidParseError('invalid_format', 'Expected domain, path and both bindings')
94
+ domain, e1, pq1 = parts[0], parts[-2], parts[-1]
95
+ path = parts[1:-2]
96
+ if not _matches(_DOMAIN, domain):
97
+ raise AtpDidParseError('invalid_domain', 'Invalid did:atp domain')
98
+ if not _matches(_E1, e1) or not _matches(_PQ1, pq1):
99
+ raise AtpDidParseError('invalid_bindings', 'Expected trailing e1_ and pq1_ fingerprints')
100
+ if not all(_valid_segment(segment) for segment in path):
101
+ raise AtpDidParseError('invalid_path', 'Invalid or ambiguous did:atp path')
102
+ return AtpV2PathDid(domain, tuple(path), e1, pq1)
103
+
104
+
105
+ def is_atp_v2_path_did(value):
106
+ try:
107
+ parse_atp_v2_path_did(value)
108
+ return True
109
+ except AtpDidParseError:
110
+ return False
111
+
112
+
113
+ def verify_atp_v2_key_bindings(did, ed25519_public_key, ml_dsa65_public_key):
114
+ """Check fingerprints only; does not verify possession, signatures or permission."""
115
+ parsed = parse_atp_v2_path_did(did)
116
+ e1 = atp_e1_fingerprint(ed25519_public_key)
117
+ pq1 = atp_pq1_fingerprint(ml_dsa65_public_key)
118
+ return parsed.e1 == e1 and parsed.pq1 == pq1
@@ -0,0 +1,90 @@
1
+ """Optional signing primitives; raw signatures do not establish authorization.
2
+
3
+ Pure ML-DSA-65 uses the empty FIPS 204 context, matching atp-sdk 6.0.2.
4
+ No key serialization, persistence, algorithm guessing or single-key fallback.
5
+ """
6
+ from dataclasses import dataclass, field
7
+ from cryptography.exceptions import InvalidSignature
8
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
9
+ from cryptography.hazmat.primitives import serialization
10
+ from pqcrypto.sign import ml_dsa_65
11
+ from . import _bytes
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PublicKeys:
16
+ ed25519: bytes
17
+ ml_dsa65: bytes
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class KeyPair:
22
+ public_keys: PublicKeys
23
+ ed25519_secret: bytes = field(repr=False)
24
+ ml_dsa65_secret: bytes = field(repr=False)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class HybridSignature:
29
+ ed25519: bytes
30
+ ml_dsa65: bytes
31
+
32
+
33
+ def _sized(value, length):
34
+ raw = _bytes(value)
35
+ if len(raw) != length:
36
+ raise ValueError('Invalid key or signature length: expected %d bytes' % length)
37
+ return raw
38
+
39
+
40
+ def _message(data):
41
+ return data.encode('utf-8') if isinstance(data, str) else _bytes(data)
42
+
43
+
44
+ def generate_hybrid_key_pair():
45
+ """Generate independent keys with the cryptographic backends' OS randomness."""
46
+ ed = Ed25519PrivateKey.generate()
47
+ pq_public, pq_secret = ml_dsa_65.generate_keypair()
48
+ return KeyPair(PublicKeys(ed.public_key().public_bytes(
49
+ serialization.Encoding.Raw, serialization.PublicFormat.Raw), _sized(pq_public, 1952)),
50
+ ed.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw,
51
+ serialization.NoEncryption()), _sized(pq_secret, 4032))
52
+
53
+
54
+ def sign_ed25519(data, secret_key):
55
+ return Ed25519PrivateKey.from_private_bytes(_sized(secret_key, 32)).sign(_message(data))
56
+
57
+
58
+ def verify_ed25519(data, signature, public_key):
59
+ try:
60
+ Ed25519PublicKey.from_public_bytes(_sized(public_key, 32)).verify(
61
+ _sized(signature, 64), _message(data))
62
+ return True
63
+ except (ValueError, TypeError, InvalidSignature):
64
+ return False
65
+
66
+
67
+ def sign_ml_dsa65(data, secret_key):
68
+ return _sized(ml_dsa_65.sign(_sized(secret_key, 4032), _message(data)), 3309)
69
+
70
+
71
+ def verify_ml_dsa65(data, signature, public_key):
72
+ try:
73
+ return ml_dsa_65.verify(_sized(public_key, 1952), _message(data), _sized(signature, 3309))
74
+ except (ValueError, TypeError):
75
+ return False
76
+
77
+
78
+ def sign_hybrid(data, key_pair):
79
+ if not isinstance(key_pair, KeyPair):
80
+ raise TypeError('Expected KeyPair')
81
+ return HybridSignature(sign_ed25519(data, key_pair.ed25519_secret),
82
+ sign_ml_dsa65(data, key_pair.ml_dsa65_secret))
83
+
84
+
85
+ def verify_hybrid(data, signature, public_keys):
86
+ """Both independent signatures must verify. This is not a protocol envelope."""
87
+ if not isinstance(signature, HybridSignature) or not isinstance(public_keys, PublicKeys):
88
+ return False
89
+ return (verify_ed25519(data, signature.ed25519, public_keys.ed25519)
90
+ and verify_ml_dsa65(data, signature.ml_dsa65, public_keys.ml_dsa65))
@@ -0,0 +1,88 @@
1
+ """Encrypted local development key file. Not a hardware-backed keystore."""
2
+ import base64
3
+ import json
4
+ import os
5
+ import stat
6
+ from pathlib import Path
7
+
8
+ from cryptography.exceptions import InvalidTag
9
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
10
+ from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
11
+
12
+ from .crypto import KeyPair, PublicKeys, sign_hybrid, verify_hybrid
13
+
14
+ _N = 1 << 17
15
+ _MAX_FILE = 16384
16
+
17
+
18
+ def _key(passphrase, salt):
19
+ if not isinstance(passphrase, str) or not passphrase:
20
+ raise ValueError('A nonempty passphrase is required')
21
+ return Scrypt(salt=salt, length=32, n=_N, r=8, p=1).derive(passphrase.encode('utf-8'))
22
+
23
+
24
+ def _encode(data):
25
+ return base64.urlsafe_b64encode(data).decode('ascii')
26
+
27
+
28
+ def _decode(data):
29
+ if not isinstance(data, str):
30
+ raise ValueError('Invalid key file')
31
+ return base64.b64decode(data, altchars=b'-_', validate=True)
32
+
33
+
34
+ def save_key_file(path, key_pair, passphrase):
35
+ """Create a new 0600 file exclusively; never replace an existing key file."""
36
+ if not isinstance(key_pair, KeyPair):
37
+ raise TypeError('Expected KeyPair')
38
+ destination = Path(path)
39
+ salt, nonce = os.urandom(16), os.urandom(12)
40
+ payload = json.dumps({
41
+ 'ed_public': _encode(key_pair.public_keys.ed25519),
42
+ 'pq_public': _encode(key_pair.public_keys.ml_dsa65),
43
+ 'ed_secret': _encode(key_pair.ed25519_secret),
44
+ 'pq_secret': _encode(key_pair.ml_dsa65_secret),
45
+ }, separators=(',', ':')).encode('utf-8')
46
+ ciphertext = AESGCM(_key(passphrase, salt)).encrypt(nonce, payload, b'atp-did-keyfile-v1')
47
+ envelope = json.dumps({'version': 1, 'kdf': 'scrypt', 'salt': _encode(salt),
48
+ 'nonce': _encode(nonce), 'ciphertext': _encode(ciphertext)},
49
+ separators=(',', ':')).encode('utf-8')
50
+ fd = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_NOFOLLOW', 0), 0o600)
51
+ try:
52
+ with os.fdopen(fd, 'wb') as stream:
53
+ stream.write(envelope)
54
+ except BaseException:
55
+ destination.unlink(missing_ok=True)
56
+ raise
57
+
58
+
59
+ def load_key_file(path, passphrase):
60
+ """Reject symlinks, permissive Unix modes, oversized and invalid files."""
61
+ p = Path(path)
62
+ fd = os.open(p, os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0))
63
+ with os.fdopen(fd, 'rb') as stream:
64
+ metadata = os.fstat(stream.fileno())
65
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > _MAX_FILE or (os.name == 'posix' and metadata.st_mode & 0o077):
66
+ raise ValueError('Key file must be a private regular file under 16 KiB')
67
+ raw = stream.read(_MAX_FILE + 1)
68
+ try:
69
+ document = json.loads(raw)
70
+ if set(document) != {'version', 'kdf', 'salt', 'nonce', 'ciphertext'} or document['version'] != 1 or document['kdf'] != 'scrypt':
71
+ raise ValueError('Unsupported key file')
72
+ salt, nonce, ciphertext = (_decode(document[name]) for name in ('salt', 'nonce', 'ciphertext'))
73
+ if len(salt) != 16 or len(nonce) != 12:
74
+ raise ValueError('Invalid key file')
75
+ data = json.loads(AESGCM(_key(passphrase, salt)).decrypt(nonce, ciphertext, b'atp-did-keyfile-v1'))
76
+ if set(data) != {'ed_public', 'pq_public', 'ed_secret', 'pq_secret'}:
77
+ raise ValueError('Invalid key file')
78
+ keys = KeyPair(PublicKeys(_decode(data['ed_public']), _decode(data['pq_public'])),
79
+ _decode(data['ed_secret']), _decode(data['pq_secret']))
80
+ if tuple(map(len, (keys.public_keys.ed25519, keys.public_keys.ml_dsa65,
81
+ keys.ed25519_secret, keys.ml_dsa65_secret))) != (32, 1952, 32, 4032):
82
+ raise ValueError('Invalid key lengths')
83
+ probe = b'atp-did-keyfile-v1 key consistency'
84
+ if not verify_hybrid(probe, sign_hybrid(probe, keys), keys.public_keys):
85
+ raise ValueError('Key file contains mismatched public and secret keys')
86
+ return keys
87
+ except (InvalidTag, KeyError, TypeError, UnicodeError, json.JSONDecodeError) as exc:
88
+ raise ValueError('Invalid key file or passphrase') from exc
File without changes