dnsid 0.26.0__py3-none-any.whl

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 (53) hide show
  1. dnsid/__init__.py +387 -0
  2. dnsid/_crypto.py +249 -0
  3. dnsid/_default_resolver.py +251 -0
  4. dnsid/_https_client.py +363 -0
  5. dnsid/_utils.py +283 -0
  6. dnsid/_verification_budget.py +170 -0
  7. dnsid/agent_status.py +25 -0
  8. dnsid/aws_kms_key_provider.py +611 -0
  9. dnsid/c2sp_tlog/__init__.py +297 -0
  10. dnsid/c2sp_tlog/canonical.py +152 -0
  11. dnsid/c2sp_tlog/checkpoint.py +92 -0
  12. dnsid/c2sp_tlog/checkpoint_store.py +127 -0
  13. dnsid/c2sp_tlog/errors.py +137 -0
  14. dnsid/c2sp_tlog/event_codec.py +472 -0
  15. dnsid/c2sp_tlog/lr.py +185 -0
  16. dnsid/c2sp_tlog/managed_verification_registry.py +176 -0
  17. dnsid/c2sp_tlog/merkle.py +111 -0
  18. dnsid/c2sp_tlog/policy.py +408 -0
  19. dnsid/c2sp_tlog/prepared.py +589 -0
  20. dnsid/c2sp_tlog/proof.py +95 -0
  21. dnsid/c2sp_tlog/reader.py +1486 -0
  22. dnsid/c2sp_tlog/resource_fetcher.py +299 -0
  23. dnsid/c2sp_tlog/signed_note.py +159 -0
  24. dnsid/c2sp_tlog/sqlite_checkpoint_store.py +220 -0
  25. dnsid/c2sp_tlog/stream_bundle.py +759 -0
  26. dnsid/c2sp_tlog/stream_source.py +336 -0
  27. dnsid/c2sp_tlog/stream_verifier.py +674 -0
  28. dnsid/c2sp_tlog/tiles.py +83 -0
  29. dnsid/c2sp_tlog/trust_profile.py +154 -0
  30. dnsid/c2sp_tlog/verification_registry.py +277 -0
  31. dnsid/config_loading.py +742 -0
  32. dnsid/conformance.py +51 -0
  33. dnsid/enums.py +149 -0
  34. dnsid/exceptions.py +190 -0
  35. dnsid/http_signatures.py +889 -0
  36. dnsid/interfaces.py +585 -0
  37. dnsid/jose.py +509 -0
  38. dnsid/local_key_provider.py +616 -0
  39. dnsid/manager.py +2422 -0
  40. dnsid/models.py +2315 -0
  41. dnsid/oidc.py +1029 -0
  42. dnsid/py.typed +0 -0
  43. dnsid/registry.py +69 -0
  44. dnsid/registry_client.py +1759 -0
  45. dnsid/retry.py +86 -0
  46. dnsid/safe_transport.py +385 -0
  47. dnsid/wba_signer.py +435 -0
  48. dnsid/wba_verifier.py +447 -0
  49. dnsid/web_bot_auth.py +625 -0
  50. dnsid-0.26.0.dist-info/METADATA +827 -0
  51. dnsid-0.26.0.dist-info/RECORD +53 -0
  52. dnsid-0.26.0.dist-info/WHEEL +4 -0
  53. dnsid-0.26.0.dist-info/licenses/LICENSE.txt +202 -0
dnsid/__init__.py ADDED
@@ -0,0 +1,387 @@
1
+ """DNSid Python SDK.
2
+
3
+ Implements the DNSid Protocol for identity management and verification.
4
+
5
+ Protocol layer (spec-defined):
6
+ IdentityManager — primary entry point; all protocol operations flow through it
7
+ IdentityManagerDependencies — optional dependency bundle for IdentityManager
8
+ DnsidConfig — core configuration: identity, verification, transport
9
+ IdentityConfig — local identity publication settings
10
+ VerificationConfig — verification policy and counterparty acceptance
11
+ TrustedEntity — one counterparty allowlist entry (gi + optional key pins)
12
+ TransportConfig — optional deployment/runtime DNS and TLS controls
13
+ RegistryConfig — optional registry control-plane configuration
14
+ DnsIdTxtRecord — _dnsid TXT record parsing, validation, serialization
15
+ JWKS / JWK — key set wrapper and individual key type
16
+ check_ek_ku_distinctness — RFC 7638 thumbprint distinctness check (ek vs ku key sets)
17
+ AgentStatus — status endpoint response
18
+ VerifiedDomain — result of a successful verify_domain call
19
+ DomainLog — verified event history for a domain
20
+ DomainSnapshot — materialized domain state at a point in time
21
+ LogRegistry — factory registry for log method implementations
22
+ RegistryClient — operator-side registry workflows
23
+
24
+ Application profiles (not part of the DNSid protocol):
25
+ JoseProfile / JoseConfig — JWT and JWS helpers (dnsid.jose)
26
+ HttpSignatureProfile / HttpMessageSignatureConfig
27
+ — RFC 9421 HTTP Message Signatures (dnsid.http_signatures)
28
+ OIDCProfile / OIDCConfig — OIDC token minting and verification (dnsid.oidc)
29
+
30
+ Key management:
31
+ LocalKeyProvider — file-backed EdDSA/ES256 KeyProvider with key rotation lifecycle
32
+ AwsKmsKeyProvider — AWS KMS-backed KeyProvider (optional dep: boto3)
33
+ AwsKmsConfig — configuration for AwsKmsKeyProvider
34
+ AwsKmsKeyState — mutable lifecycle state for AwsKmsKeyProvider
35
+ BotoKmsFacade — boto3 adapter for AwsKmsKeyProvider
36
+ AwsKmsFacade — abstract facade for testing without AWS
37
+
38
+ Configuration loading (loaders parse; constructors default):
39
+ load_environment / load_file / load_cli_directory — one loader per source → LoadedConfig
40
+ LoadedConfig, LogTrust, KeySource — partial configuration shape shared by every source
41
+ merge_loaded_config — field-wise merge of partial configurations (later wins)
42
+ construct_identity_manager — LoadedConfig + caller deps → IdentityManager
43
+ identity_manager_from_environment / identity_manager_from_dnsid / identity_manager_from_file
44
+ registry_client_from_environment — RegistryClient from DNSID_REGISTRY_URL / DNSID_API_KEY
45
+
46
+ Interfaces (implement to integrate your own backends):
47
+ IdentityResolver — minimal protocol satisfied by IdentityManager; accepted by profiles
48
+ KeyProvider — key management (local files, KMS, HSM)
49
+ Log — log write interface
50
+ LogReader — log read/verify interface
51
+ IdentityCache — verified domain cache
52
+ DNSResolver — DNS resolver
53
+
54
+ Errors:
55
+ ParseError — structurally malformed TXT record
56
+ ValidationError — semantic constraint violation
57
+ VerificationError — live verification failure (carries VerificationCode)
58
+ ArgumentError — invalid caller-supplied argument
59
+
60
+ Enumerations:
61
+ DNSSECState, DNSSECMode, VerificationCode, AgentState, RevocationReason,
62
+ RegistryRevocationReason, EventType
63
+ """
64
+
65
+ from ._verification_budget import remaining_seconds, verification_budget
66
+ from .agent_status import active_status_document
67
+ from .aws_kms_key_provider import (
68
+ AwsKmsConfig as AwsKmsConfig,
69
+ )
70
+ from .aws_kms_key_provider import (
71
+ AwsKmsFacade as AwsKmsFacade,
72
+ )
73
+ from .aws_kms_key_provider import (
74
+ AwsKmsKeyProvider as AwsKmsKeyProvider,
75
+ )
76
+ from .aws_kms_key_provider import (
77
+ AwsKmsKeyState as AwsKmsKeyState,
78
+ )
79
+ from .aws_kms_key_provider import (
80
+ BotoKmsFacade as BotoKmsFacade,
81
+ )
82
+ from .config_loading import (
83
+ KeySource,
84
+ LoadedConfig,
85
+ LogTrust,
86
+ construct_identity_manager,
87
+ identity_manager_from_dnsid,
88
+ identity_manager_from_environment,
89
+ identity_manager_from_file,
90
+ load_cli_directory,
91
+ load_environment,
92
+ load_file,
93
+ merge_loaded_config,
94
+ registry_client_from_environment,
95
+ )
96
+ from .conformance import SDK_CONFORMANCE, SDKConformance
97
+ from .enums import (
98
+ AgentState,
99
+ DNSSECMode,
100
+ DNSSECState,
101
+ EventType,
102
+ LifecycleErrorCategory,
103
+ LogSignerRole,
104
+ RegistryRevocationReason,
105
+ RevocationReason,
106
+ VerificationCode,
107
+ )
108
+ from .exceptions import (
109
+ ArgumentError,
110
+ DNSidError,
111
+ LifecycleVerificationError,
112
+ ManagedKeyRotationActivationError,
113
+ ManagedKeyRotationSubmissionError,
114
+ ParseError,
115
+ RegistryRequestError,
116
+ ValidationError,
117
+ VerificationError,
118
+ )
119
+ from .http_signatures import HttpMessageSignatureConfig, HttpSignatureProfile
120
+ from .interfaces import (
121
+ AbstractRegistryClient,
122
+ DNSResolver,
123
+ HTTPSFetcher,
124
+ IdentityCache,
125
+ IdentityResolver,
126
+ KeyProvider,
127
+ Log,
128
+ LogReader,
129
+ NoopLogReader,
130
+ )
131
+ from .jose import JoseConfig, JoseProfile
132
+ from .local_key_provider import LocalKeyProvider
133
+ from .manager import (
134
+ ApplicationSigningPauseHook,
135
+ IdentityManager,
136
+ IdentityManagerDependencies,
137
+ KeyRotationPersistenceHook,
138
+ sign_event_with_provider,
139
+ )
140
+ from .models import (
141
+ DEFAULT_REGISTRY_URL,
142
+ IDENTITY_RECORD_VERSION,
143
+ JWK,
144
+ JWKS,
145
+ AgentRegistration,
146
+ AgentRegistrationInput,
147
+ AgentStatus,
148
+ AnyLogEvent,
149
+ CanonicalRecordContentResponse,
150
+ DelegationEvent,
151
+ DnsidConfig,
152
+ DnsIdTxtRecord,
153
+ DomainLog,
154
+ DomainSnapshot,
155
+ HttpRequest,
156
+ HttpSigningOptions,
157
+ HttpVerificationOptions,
158
+ IdentityConfig,
159
+ IssuanceEvent,
160
+ JWTOptions,
161
+ KeyRotationEvent,
162
+ KeyRotationPreparationRequest,
163
+ KeyRotationResult,
164
+ LifecycleResult,
165
+ LiveAgentRegistrationInput,
166
+ LiveChallengeTranscript,
167
+ LiveProofReissueRequest,
168
+ LiveProofReissueResponse,
169
+ LiveProofRequest,
170
+ LiveProofResponse,
171
+ LiveProvisioningResponse,
172
+ LogEvent,
173
+ LoggedStateEvidence,
174
+ LogRef,
175
+ MigrationEvent,
176
+ PreparedRegistryEvent,
177
+ PublicationConfig,
178
+ PublishedRecord,
179
+ RegistryAgentStatus,
180
+ RegistryConfig,
181
+ RetirementEvent,
182
+ RevocationEvent,
183
+ SignatureParams,
184
+ SubmissionResult,
185
+ TLSCertificate,
186
+ TransportConfig,
187
+ TrustedEntity,
188
+ TXTRecord,
189
+ VerificationConfig,
190
+ VerifiedCutoffHistory,
191
+ VerifiedDomain,
192
+ check_ek_ku_distinctness,
193
+ publish_allowed_version,
194
+ )
195
+ from .oidc import (
196
+ NetworkError,
197
+ OAuthError,
198
+ OIDCAssertionOptions,
199
+ OIDCConfig,
200
+ OIDCDiscoveryDocument,
201
+ OIDCProfile,
202
+ OIDCTokenExchangeOptions,
203
+ OIDCTokenResponse,
204
+ VerifiedOIDCSubject,
205
+ VerifyOIDCTokenOptions,
206
+ )
207
+ from .registry import LogRegistry
208
+ from .registry_client import RegistryClient
209
+ from .retry import async_retry_transient, retry_transient
210
+ from .wba_signer import (
211
+ WBADirectoryResponse,
212
+ WBAHttpSigner,
213
+ WBAVariant,
214
+ WebBotAuthConfig,
215
+ WebBotAuthSigningOptions,
216
+ serve_http_message_signatures_directory,
217
+ )
218
+ from .wba_verifier import WBAHttpVerifier, WebBotAuthVerificationOptions
219
+ from .web_bot_auth import (
220
+ BotAuthConfig,
221
+ BotIdentity,
222
+ VerifiedBotRequest,
223
+ WebBotAuthProfile,
224
+ )
225
+
226
+ PROTOCOL_VERSION: str = IDENTITY_RECORD_VERSION
227
+ """Wire version string for the DNSid protocol implemented by this SDK.
228
+
229
+ Alias of IDENTITY_RECORD_VERSION — the current numbered publish profile.
230
+ """
231
+
232
+ __all__ = [
233
+ # Core
234
+ "IdentityManager",
235
+ "IdentityManagerDependencies",
236
+ # Config
237
+ "DnsidConfig",
238
+ "IdentityConfig",
239
+ "VerificationConfig",
240
+ "TrustedEntity",
241
+ "TransportConfig",
242
+ "RegistryConfig",
243
+ # DNS record
244
+ "DnsIdTxtRecord",
245
+ "TXTRecord",
246
+ # Keys
247
+ "JWK",
248
+ "JWKS",
249
+ "check_ek_ku_distinctness",
250
+ # Status / TLS
251
+ "AgentStatus",
252
+ "TLSCertificate",
253
+ # Verification result
254
+ "VerifiedDomain",
255
+ # Log
256
+ "DomainLog",
257
+ "DomainSnapshot",
258
+ "LoggedStateEvidence",
259
+ "VerifiedCutoffHistory",
260
+ "LogRef",
261
+ "LogEvent",
262
+ "AnyLogEvent",
263
+ "IssuanceEvent",
264
+ "KeyRotationEvent",
265
+ "RevocationEvent",
266
+ "RetirementEvent",
267
+ "MigrationEvent",
268
+ "DelegationEvent",
269
+ "LifecycleErrorCategory",
270
+ "LifecycleVerificationError",
271
+ "ManagedKeyRotationActivationError",
272
+ "ManagedKeyRotationSubmissionError",
273
+ "LogSignerRole",
274
+ "sign_event_with_provider",
275
+ # Registry
276
+ "LogRegistry",
277
+ "RegistryClient",
278
+ "AgentRegistrationInput",
279
+ "AgentRegistration",
280
+ "LifecycleResult",
281
+ "LiveAgentRegistrationInput",
282
+ "LiveChallengeTranscript",
283
+ "LiveProvisioningResponse",
284
+ "LiveProofRequest",
285
+ "LiveProofResponse",
286
+ "LiveProofReissueRequest",
287
+ "LiveProofReissueResponse",
288
+ "RegistryAgentStatus",
289
+ "CanonicalRecordContentResponse",
290
+ "PublishedRecord",
291
+ "PreparedRegistryEvent",
292
+ "KeyRotationPreparationRequest",
293
+ "KeyRotationResult",
294
+ "KeyRotationPersistenceHook",
295
+ "ApplicationSigningPauseHook",
296
+ "SubmissionResult",
297
+ # Application-layer option types
298
+ "JWTOptions",
299
+ "HttpRequest",
300
+ "HttpSigningOptions",
301
+ "HttpVerificationOptions",
302
+ "SignatureParams",
303
+ # JOSE / HTTP signatures profiles
304
+ "JoseProfile",
305
+ "JoseConfig",
306
+ "HttpSignatureProfile",
307
+ "HttpMessageSignatureConfig",
308
+ # OIDC profile
309
+ "OIDCProfile",
310
+ "OIDCConfig",
311
+ "OIDCAssertionOptions",
312
+ "OIDCTokenExchangeOptions",
313
+ "OIDCTokenResponse",
314
+ "OIDCDiscoveryDocument",
315
+ "VerifyOIDCTokenOptions",
316
+ "VerifiedOIDCSubject",
317
+ # Key management
318
+ "LocalKeyProvider",
319
+ # Configuration loading
320
+ "LoadedConfig",
321
+ "LogTrust",
322
+ "KeySource",
323
+ "load_environment",
324
+ "load_file",
325
+ "load_cli_directory",
326
+ "merge_loaded_config",
327
+ "construct_identity_manager",
328
+ "identity_manager_from_environment",
329
+ "identity_manager_from_dnsid",
330
+ "identity_manager_from_file",
331
+ "registry_client_from_environment",
332
+ # Agent status helpers
333
+ "active_status_document",
334
+ # Interfaces
335
+ "AbstractRegistryClient",
336
+ "IdentityResolver",
337
+ "KeyProvider",
338
+ "Log",
339
+ "LogReader",
340
+ "NoopLogReader",
341
+ "IdentityCache",
342
+ "DNSResolver",
343
+ "HTTPSFetcher",
344
+ # Retry helpers
345
+ "retry_transient",
346
+ "async_retry_transient",
347
+ # Conformance
348
+ "SDKConformance",
349
+ "SDK_CONFORMANCE",
350
+ "verification_budget",
351
+ "remaining_seconds",
352
+ "PROTOCOL_VERSION",
353
+ "PublicationConfig",
354
+ "DEFAULT_REGISTRY_URL",
355
+ "IDENTITY_RECORD_VERSION",
356
+ "publish_allowed_version",
357
+ # Web Bot Auth profile
358
+ "WBAHttpSigner",
359
+ "WBAHttpVerifier",
360
+ "WebBotAuthVerificationOptions",
361
+ "WBAVariant",
362
+ "WBADirectoryResponse",
363
+ "WebBotAuthConfig",
364
+ "WebBotAuthSigningOptions",
365
+ "serve_http_message_signatures_directory",
366
+ "WebBotAuthProfile",
367
+ "BotAuthConfig",
368
+ "BotIdentity",
369
+ "VerifiedBotRequest",
370
+ # Errors
371
+ "DNSidError",
372
+ "ParseError",
373
+ "ValidationError",
374
+ "RegistryRequestError",
375
+ "VerificationError",
376
+ "ArgumentError",
377
+ "NetworkError",
378
+ "OAuthError",
379
+ # Enumerations
380
+ "DNSSECState",
381
+ "DNSSECMode",
382
+ "VerificationCode",
383
+ "AgentState",
384
+ "RevocationReason",
385
+ "RegistryRevocationReason",
386
+ "EventType",
387
+ ]
dnsid/_crypto.py ADDED
@@ -0,0 +1,249 @@
1
+ """Cryptographic operations: JWK thumbprint, signature verification, and key parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING:
10
+ from .models import JWK
11
+
12
+ from cryptography.exceptions import InvalidSignature
13
+ from cryptography.hazmat.primitives import hashes
14
+ from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa
15
+ from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
16
+
17
+ from ._utils import b64url_decode, b64url_encode
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # RFC 7638 thumbprint
21
+ # ---------------------------------------------------------------------------
22
+
23
+ # Required public members per key type (alphabetical — the spec mandates this order).
24
+ _THUMBPRINT_MEMBERS: dict[str, list[str]] = {
25
+ "EC": ["crv", "kty", "x", "y"],
26
+ "RSA": ["e", "kty", "n"],
27
+ "OKP": ["crv", "kty", "x"],
28
+ }
29
+
30
+
31
+ def compute_thumbprint(raw: dict[str, Any]) -> str:
32
+ """Return the RFC 7638 JWK thumbprint (SHA-256, unpadded base64url).
33
+
34
+ Raises ValueError if the key type is unsupported or required members are missing.
35
+ """
36
+ kty = raw.get("kty", "")
37
+ members = _THUMBPRINT_MEMBERS.get(kty)
38
+ if members is None:
39
+ raise ValueError(f"unsupported kty for thumbprint: {kty!r}")
40
+
41
+ missing = [k for k in members if k not in raw]
42
+ if missing:
43
+ raise ValueError(f"JWK missing required thumbprint members: {missing}")
44
+
45
+ canonical = {k: raw[k] for k in members}
46
+ canonical_json = json.dumps(canonical, separators=(",", ":"), sort_keys=True).encode("ascii")
47
+ digest = hashlib.sha256(canonical_json).digest()
48
+ return b64url_encode(digest)
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Signature verification
53
+ # ---------------------------------------------------------------------------
54
+
55
+ _EC_CURVES: dict[str, ec.EllipticCurve] = {
56
+ "P-256": ec.SECP256R1(),
57
+ }
58
+
59
+ # Number of bytes per integer component (r or s) — ES256 / P-256 only.
60
+ _EC_COORD_BYTES: dict[str, int] = {
61
+ "ES256": 32,
62
+ }
63
+
64
+ _EC_HASH: dict[str, type[hashes.HashAlgorithm]] = {
65
+ "ES256": hashes.SHA256,
66
+ }
67
+
68
+ _RSA_HASH: dict[str, type[hashes.HashAlgorithm]] = {
69
+ "RS256": hashes.SHA256,
70
+ }
71
+
72
+ # Supported kty/crv → JOSE alg mappings (EdDSA and ES256 only per DNSid protocol).
73
+ _KTY_CRV_TO_ALG: dict[tuple[str, str], str] = {
74
+ ("OKP", "Ed25519"): "EdDSA",
75
+ ("EC", "P-256"): "ES256",
76
+ }
77
+
78
+
79
+ def signature_alg_from_raw(raw: dict[str, Any]) -> str:
80
+ """Derive JOSE signature algorithm from kty/crv.
81
+
82
+ Raises ValueError if the kty/crv combination is not supported by the DNSid protocol.
83
+ """
84
+ kty = raw.get("kty", "")
85
+ crv = raw.get("crv", "")
86
+ alg = _KTY_CRV_TO_ALG.get((kty, crv))
87
+ if alg is None:
88
+ raise ValueError(
89
+ f"unsupported kty/crv for DNSid signature algorithm: kty={kty!r}, crv={crv!r}"
90
+ )
91
+ return alg
92
+
93
+
94
+ _ALLOWED_ALGS: frozenset[str] = frozenset({"EdDSA", "ES256", "RS256"})
95
+
96
+
97
+ def verify_signature(raw: dict[str, Any], payload: bytes, signature: bytes) -> bool:
98
+ """Return True if *signature* over *payload* is valid for the public key in *raw*.
99
+
100
+ Returns False for an invalid signature; never raises for a bad sig value.
101
+ When the JWK has no alg field, the algorithm is derived from kty/crv.
102
+ Only EdDSA and ES256 are accepted; any other algorithm returns False.
103
+ """
104
+ kty = raw.get("kty", "")
105
+ alg = raw.get("alg", "")
106
+
107
+ # Derive alg from kty/crv when absent (alg is optional per DNSid design).
108
+ if not alg:
109
+ try:
110
+ alg = signature_alg_from_raw(raw)
111
+ except ValueError:
112
+ return False
113
+
114
+ if alg not in _ALLOWED_ALGS:
115
+ return False
116
+
117
+ try:
118
+ if kty == "EC":
119
+ return _verify_ec(raw, alg, payload, signature)
120
+ if kty == "OKP":
121
+ return _verify_okp(raw, payload, signature)
122
+ if kty == "RSA":
123
+ return _verify_rsa(raw, alg, payload, signature)
124
+ return False
125
+ except InvalidSignature:
126
+ return False
127
+ except Exception:
128
+ return False
129
+
130
+
131
+ def _verify_ec(raw: dict[str, Any], alg: str, payload: bytes, sig: bytes) -> bool:
132
+ crv = raw.get("crv", "")
133
+ curve = _EC_CURVES.get(crv)
134
+ if not curve:
135
+ raise ValueError(f"unsupported EC curve: {crv!r}")
136
+
137
+ hash_cls = _EC_HASH.get(alg)
138
+ if not hash_cls:
139
+ raise ValueError(f"unsupported EC alg: {alg!r}")
140
+
141
+ x = int.from_bytes(b64url_decode(raw["x"]), "big")
142
+ y = int.from_bytes(b64url_decode(raw["y"]), "big")
143
+ pub_key = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=curve).public_key()
144
+
145
+ # JWT/JWS/RFC 9421 use IEEE P1363 (r || s); cryptography expects DER.
146
+ coord = _EC_COORD_BYTES.get(alg)
147
+ if coord and len(sig) == 2 * coord:
148
+ r = int.from_bytes(sig[:coord], "big")
149
+ s = int.from_bytes(sig[coord:], "big")
150
+ der_sig = encode_dss_signature(r, s)
151
+ else:
152
+ der_sig = sig # fall through and let the library reject it
153
+
154
+ pub_key.verify(der_sig, payload, ec.ECDSA(hash_cls()))
155
+ return True
156
+
157
+
158
+ def _verify_okp(raw: dict[str, Any], payload: bytes, sig: bytes) -> bool:
159
+ crv = raw.get("crv", "")
160
+ if crv != "Ed25519":
161
+ raise ValueError(f"unsupported OKP curve: {crv!r}")
162
+ pub_key = ed25519.Ed25519PublicKey.from_public_bytes(b64url_decode(raw["x"]))
163
+ pub_key.verify(sig, payload)
164
+ return True
165
+
166
+
167
+ def _verify_rsa(raw: dict[str, Any], alg: str, payload: bytes, sig: bytes) -> bool:
168
+ hash_cls = _RSA_HASH.get(alg)
169
+ if not hash_cls:
170
+ raise ValueError(f"unsupported RSA alg: {alg!r}")
171
+ n = int.from_bytes(b64url_decode(raw["n"]), "big")
172
+ e = int.from_bytes(b64url_decode(raw["e"]), "big")
173
+ pub_key = rsa.RSAPublicNumbers(e=e, n=n).public_key()
174
+ pub_key.verify(sig, payload, padding.PKCS1v15(), hash_cls())
175
+ return True
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # JWK construction from wire format
180
+ # ---------------------------------------------------------------------------
181
+
182
+
183
+ def jwk_from_dict(d: dict[str, Any]) -> JWK:
184
+ """Build a JWK dataclass from a raw dict (e.g. from a parsed JWKS JSON response)."""
185
+ from .models import JWK # lazy import — models imports _utils, not _crypto
186
+
187
+ return JWK(
188
+ kty=d.get("kty", ""),
189
+ alg=d.get("alg", ""),
190
+ kid=d.get("kid", ""),
191
+ use=d.get("use", ""),
192
+ _raw=dict(d),
193
+ )
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Helpers for building a JWK dict from a cryptography private/public key
198
+ # (used in tests to produce valid _raw dicts)
199
+ # ---------------------------------------------------------------------------
200
+
201
+
202
+ def ec_public_jwk_dict(
203
+ private_key: ec.EllipticCurvePrivateKey, kid: str, alg: str
204
+ ) -> dict[str, Any]:
205
+ """Produce a public JWK dict from a cryptography EC private key (ES256/P-256 only)."""
206
+ pub = private_key.public_key()
207
+ numbers = pub.public_numbers()
208
+ crv = {"secp256r1": "P-256"}[pub.curve.name]
209
+ coord_len = _EC_COORD_BYTES[alg]
210
+ return {
211
+ "kty": "EC",
212
+ "kid": kid,
213
+ "alg": alg,
214
+ "use": "sig",
215
+ "crv": crv,
216
+ "x": b64url_encode(numbers.x.to_bytes(coord_len, "big")),
217
+ "y": b64url_encode(numbers.y.to_bytes(coord_len, "big")),
218
+ }
219
+
220
+
221
+ def ed25519_public_jwk_dict(private_key: ed25519.Ed25519PrivateKey, kid: str) -> dict[str, Any]:
222
+ """Produce a public JWK dict from a cryptography Ed25519 private key."""
223
+ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
224
+
225
+ pub_bytes = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
226
+ return {
227
+ "kty": "OKP",
228
+ "kid": kid,
229
+ "alg": "EdDSA",
230
+ "use": "sig",
231
+ "crv": "Ed25519",
232
+ "x": b64url_encode(pub_bytes),
233
+ }
234
+
235
+
236
+ def ec_sign(private_key: ec.EllipticCurvePrivateKey, alg: str, payload: bytes) -> bytes:
237
+ """Sign *payload* with an EC private key; return IEEE P1363 (r||s) bytes."""
238
+ from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
239
+
240
+ hash_cls = _EC_HASH[alg]
241
+ der_sig = private_key.sign(payload, ec.ECDSA(hash_cls()))
242
+ r, s = decode_dss_signature(der_sig)
243
+ coord = _EC_COORD_BYTES[alg]
244
+ return r.to_bytes(coord, "big") + s.to_bytes(coord, "big")
245
+
246
+
247
+ def ed25519_sign(private_key: ed25519.Ed25519PrivateKey, payload: bytes) -> bytes:
248
+ """Sign *payload* with an Ed25519 private key."""
249
+ return private_key.sign(payload)