anip-crypto 0.11.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,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: anip-crypto
3
+ Version: 0.11.0
4
+ Summary: ANIP cryptographic primitives — key management, JWT, JWS, JWKS
5
+ Author-email: ANIP Protocol <team@anip.dev>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/anip-protocol/anip
8
+ Keywords: anip,agent,protocol
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: anip-core==0.11.0
16
+ Requires-Dist: cryptography>=42.0
17
+ Requires-Dist: PyJWT[crypto]>=2.8
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
@@ -0,0 +1,15 @@
1
+ # anip-crypto
2
+
3
+ ANIP cryptographic primitives — key management, JWT, JWS, JWKS
4
+
5
+ Part of the [ANIP](https://github.com/anip-protocol/anip) protocol ecosystem.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install anip-crypto
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ See the [ANIP repository](https://github.com/anip-protocol/anip) for full documentation.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "anip-crypto"
3
+ version = "0.11.0"
4
+ description = "ANIP cryptographic primitives — key management, JWT, JWS, JWKS"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "anip-core==0.11.0",
8
+ "cryptography>=42.0",
9
+ "PyJWT[crypto]>=2.8",
10
+ ]
11
+ authors = [{ name = "ANIP Protocol", email = "team@anip.dev" }]
12
+ license = { text = "Apache-2.0" }
13
+ keywords = ["anip", "agent", "protocol"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ dev = ["pytest>=8.0"]
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/anip-protocol/anip"
27
+
28
+ [build-system]
29
+ requires = ["setuptools>=68.0"]
30
+ build-backend = "setuptools.build_meta"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ """ANIP Crypto — key management, JWT, JWS, JWKS."""
2
+
3
+ from .canonicalize import canonicalize
4
+ from .jwks import build_jwks
5
+ from .jws import (
6
+ sign_jws_detached,
7
+ sign_jws_detached_audit,
8
+ verify_jws_detached,
9
+ verify_jws_detached_audit,
10
+ )
11
+ from .jwt import sign_jwt, verify_jwt
12
+ from .keys import KeyManager
13
+ from .verify import verify_audit_entry_signature, verify_manifest_signature
14
+
15
+ __all__ = [
16
+ "KeyManager",
17
+ "canonicalize",
18
+ "sign_jwt",
19
+ "verify_jwt",
20
+ "sign_jws_detached",
21
+ "verify_jws_detached",
22
+ "sign_jws_detached_audit",
23
+ "verify_jws_detached_audit",
24
+ "build_jwks",
25
+ "verify_audit_entry_signature",
26
+ "verify_manifest_signature",
27
+ ]
@@ -0,0 +1,13 @@
1
+ """Canonical JSON helpers for verifiable ANIP artifacts."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def canonicalize(data: dict[str, Any], *, exclude: set[str] | None = None) -> bytes:
8
+ """Produce canonical JSON bytes for signing/hashing.
9
+
10
+ Sorts keys, uses compact separators, optionally excludes fields.
11
+ """
12
+ filtered = {k: v for k, v in sorted(data.items()) if k not in (exclude or set())}
13
+ return json.dumps(filtered, separators=(",", ":"), sort_keys=True).encode()
@@ -0,0 +1,8 @@
1
+ """JWKS construction for ANIP services."""
2
+
3
+ from .keys import KeyManager
4
+
5
+
6
+ def build_jwks(key_manager: KeyManager) -> dict:
7
+ """Build a JWKS response containing both public keys."""
8
+ return key_manager.get_jwks()
@@ -0,0 +1,23 @@
1
+ """Detached JWS operations for ANIP signed artifacts."""
2
+
3
+ from .keys import KeyManager
4
+
5
+
6
+ def sign_jws_detached(key_manager: KeyManager, payload: bytes) -> str:
7
+ """Sign with the delegation key (for manifests)."""
8
+ return key_manager.sign_jws_detached(payload)
9
+
10
+
11
+ def verify_jws_detached(key_manager: KeyManager, jws: str, payload: bytes) -> None:
12
+ """Verify with the delegation public key."""
13
+ key_manager.verify_jws_detached(jws, payload)
14
+
15
+
16
+ def sign_jws_detached_audit(key_manager: KeyManager, payload: bytes) -> str:
17
+ """Sign with the audit key (for checkpoints)."""
18
+ return key_manager.sign_jws_detached_audit(payload)
19
+
20
+
21
+ def verify_jws_detached_audit(key_manager: KeyManager, jws: str, payload: bytes) -> None:
22
+ """Verify with the audit public key."""
23
+ key_manager.verify_jws_detached_audit(jws, payload)
@@ -0,0 +1,13 @@
1
+ """JWT signing and verification for ANIP delegation tokens."""
2
+
3
+ from .keys import KeyManager
4
+
5
+
6
+ def sign_jwt(key_manager: KeyManager, payload: dict) -> str:
7
+ """Sign a JWT with the delegation key (ES256)."""
8
+ return key_manager.sign_jwt(payload)
9
+
10
+
11
+ def verify_jwt(key_manager: KeyManager, token: str, *, audience: str) -> dict:
12
+ """Verify a JWT signature and audience."""
13
+ return key_manager.verify_jwt(token, audience=audience)
@@ -0,0 +1,272 @@
1
+ """Key management and cryptographic operations for ANIP trust layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+
10
+ import jwt
11
+ from cryptography.hazmat.primitives import hashes, serialization
12
+ from cryptography.hazmat.primitives.asymmetric import ec
13
+ from cryptography.hazmat.primitives.asymmetric.utils import (
14
+ decode_dss_signature,
15
+ encode_dss_signature,
16
+ )
17
+
18
+
19
+ def _b64url_encode(data: bytes) -> str:
20
+ """Base64url encode without padding."""
21
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
22
+
23
+
24
+ def _b64url_decode(s: str) -> bytes:
25
+ """Base64url decode, adding padding as needed."""
26
+ s += "=" * (4 - len(s) % 4)
27
+ return base64.urlsafe_b64decode(s)
28
+
29
+
30
+ class KeyManager:
31
+ """Manages EC P-256 key pairs for JWT signing and JWKS publication."""
32
+
33
+ def __init__(self, key_path: str | None = None) -> None:
34
+ if key_path and Path(key_path).exists():
35
+ self._load_keys(key_path)
36
+ else:
37
+ self._generate_keys()
38
+ if key_path:
39
+ self._save_keys(key_path)
40
+
41
+ # ------------------------------------------------------------------ #
42
+ # Key lifecycle
43
+ # ------------------------------------------------------------------ #
44
+
45
+ def _generate_keys(self) -> None:
46
+ self._private_key = ec.generate_private_key(ec.SECP256R1())
47
+ self._public_key = self._private_key.public_key()
48
+ self._kid = self._derive_kid(self._public_key)
49
+ # Audit key pair (separate from delegation signing key)
50
+ self._audit_private_key = ec.generate_private_key(ec.SECP256R1())
51
+ self._audit_public_key = self._audit_private_key.public_key()
52
+ self._audit_kid = self._derive_kid(self._audit_public_key)
53
+
54
+ def _derive_kid(self, public_key: ec.EllipticCurvePublicKey) -> str:
55
+ pub_der = public_key.public_bytes(
56
+ serialization.Encoding.DER,
57
+ serialization.PublicFormat.SubjectPublicKeyInfo,
58
+ )
59
+ return hashlib.sha256(pub_der).hexdigest()[:16]
60
+
61
+ def _save_keys(self, path: str) -> None:
62
+ pem = self._private_key.private_bytes(
63
+ serialization.Encoding.PEM,
64
+ serialization.PrivateFormat.PKCS8,
65
+ serialization.NoEncryption(),
66
+ ).decode("utf-8")
67
+ audit_pem = self._audit_private_key.private_bytes(
68
+ serialization.Encoding.PEM,
69
+ serialization.PrivateFormat.PKCS8,
70
+ serialization.NoEncryption(),
71
+ ).decode("utf-8")
72
+ data = {
73
+ "private_key_pem": pem,
74
+ "kid": self._kid,
75
+ "audit_private_key_pem": audit_pem,
76
+ "audit_kid": self._audit_kid,
77
+ }
78
+ Path(path).write_text(json.dumps(data))
79
+
80
+ def _load_keys(self, path: str) -> None:
81
+ data = json.loads(Path(path).read_text())
82
+ loaded_key = serialization.load_pem_private_key(
83
+ data["private_key_pem"].encode("utf-8"), password=None
84
+ )
85
+ if not isinstance(loaded_key, ec.EllipticCurvePrivateKey):
86
+ raise TypeError("Expected EC private key, got " + type(loaded_key).__name__)
87
+ self._private_key = loaded_key
88
+ self._public_key = self._private_key.public_key()
89
+ self._kid = data["kid"]
90
+ # Load audit key pair (generate if missing for backward compatibility)
91
+ if "audit_private_key_pem" in data:
92
+ audit_key = serialization.load_pem_private_key(
93
+ data["audit_private_key_pem"].encode("utf-8"), password=None
94
+ )
95
+ if not isinstance(audit_key, ec.EllipticCurvePrivateKey):
96
+ raise TypeError("Expected EC private key for audit, got " + type(audit_key).__name__)
97
+ self._audit_private_key = audit_key
98
+ self._audit_public_key = self._audit_private_key.public_key()
99
+ self._audit_kid = data["audit_kid"]
100
+ else:
101
+ self._audit_private_key = ec.generate_private_key(ec.SECP256R1())
102
+ self._audit_public_key = self._audit_private_key.public_key()
103
+ self._audit_kid = self._derive_kid(self._audit_public_key)
104
+
105
+ # ------------------------------------------------------------------ #
106
+ # Properties
107
+ # ------------------------------------------------------------------ #
108
+
109
+ @property
110
+ def private_key(self) -> ec.EllipticCurvePrivateKey:
111
+ return self._private_key
112
+
113
+ @property
114
+ def public_key(self) -> ec.EllipticCurvePublicKey:
115
+ return self._public_key
116
+
117
+ @property
118
+ def kid(self) -> str:
119
+ """Delegation key ID."""
120
+ return self._kid
121
+
122
+ @property
123
+ def audit_private_key(self) -> ec.EllipticCurvePrivateKey:
124
+ """Audit private key."""
125
+ return self._audit_private_key
126
+
127
+ @property
128
+ def audit_public_key(self) -> ec.EllipticCurvePublicKey:
129
+ """Audit public key."""
130
+ return self._audit_public_key
131
+
132
+ @property
133
+ def audit_kid(self) -> str:
134
+ """Audit key ID."""
135
+ return self._audit_kid
136
+
137
+ # ------------------------------------------------------------------ #
138
+ # JWKS
139
+ # ------------------------------------------------------------------ #
140
+
141
+ def get_jwks(self) -> dict:
142
+ """Return a JWKS containing both public keys (no private material)."""
143
+ # Delegation signing key
144
+ numbers = self._public_key.public_numbers()
145
+ x_bytes = numbers.x.to_bytes(32, "big")
146
+ y_bytes = numbers.y.to_bytes(32, "big")
147
+ # Audit signing key
148
+ audit_numbers = self._audit_public_key.public_numbers()
149
+ audit_x_bytes = audit_numbers.x.to_bytes(32, "big")
150
+ audit_y_bytes = audit_numbers.y.to_bytes(32, "big")
151
+ return {
152
+ "keys": [
153
+ {
154
+ "kty": "EC",
155
+ "crv": "P-256",
156
+ "alg": "ES256",
157
+ "use": "sig",
158
+ "kid": self._kid,
159
+ "x": _b64url_encode(x_bytes),
160
+ "y": _b64url_encode(y_bytes),
161
+ },
162
+ {
163
+ "kty": "EC",
164
+ "crv": "P-256",
165
+ "alg": "ES256",
166
+ "use": "audit",
167
+ "kid": self._audit_kid,
168
+ "x": _b64url_encode(audit_x_bytes),
169
+ "y": _b64url_encode(audit_y_bytes),
170
+ },
171
+ ]
172
+ }
173
+
174
+ # ------------------------------------------------------------------ #
175
+ # Audit entry signing
176
+ # ------------------------------------------------------------------ #
177
+
178
+ def sign_audit_entry(self, entry_data: dict) -> str:
179
+ """Sign an audit entry with the dedicated audit key.
180
+
181
+ Creates canonical JSON (excluding 'signature' and 'id'), hashes it,
182
+ and produces a JWT containing the hash, signed with the audit key.
183
+ """
184
+ canonical = json.dumps(
185
+ {k: v for k, v in sorted(entry_data.items()) if k not in ("signature", "id")},
186
+ separators=(",", ":"),
187
+ sort_keys=True,
188
+ ).encode()
189
+ hash_hex = hashlib.sha256(canonical).hexdigest()
190
+ return jwt.encode(
191
+ {"audit_hash": hash_hex},
192
+ self._audit_private_key,
193
+ algorithm="ES256",
194
+ headers={"kid": self._audit_kid},
195
+ )
196
+
197
+ # ------------------------------------------------------------------ #
198
+ # JWT operations
199
+ # ------------------------------------------------------------------ #
200
+
201
+ def sign_jwt(self, payload: dict) -> str:
202
+ """Sign a JWT with ES256, including kid in the header."""
203
+ return jwt.encode(
204
+ payload,
205
+ self._private_key,
206
+ algorithm="ES256",
207
+ headers={"kid": self._kid},
208
+ )
209
+
210
+ def verify_jwt(self, token: str, *, audience: str) -> dict:
211
+ """Verify a JWT signature and audience claim."""
212
+ return jwt.decode(
213
+ token,
214
+ self._public_key,
215
+ algorithms=["ES256"],
216
+ audience=audience,
217
+ )
218
+
219
+ # ------------------------------------------------------------------ #
220
+ # Detached JWS
221
+ # ------------------------------------------------------------------ #
222
+
223
+ def _sign_jws_detached_with(self, payload: bytes, private_key: ec.EllipticCurvePrivateKey, kid: str) -> str:
224
+ """Create a detached JWS using the specified key."""
225
+ header = json.dumps(
226
+ {"alg": "ES256", "kid": kid}, separators=(",", ":")
227
+ )
228
+ header_b64 = _b64url_encode(header.encode("utf-8"))
229
+ payload_b64 = _b64url_encode(payload)
230
+
231
+ signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
232
+ der_sig = private_key.sign(signing_input, ec.ECDSA(hashes.SHA256()))
233
+
234
+ # Convert DER signature to raw r||s (32 bytes each for P-256)
235
+ r, s = decode_dss_signature(der_sig)
236
+ raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big")
237
+ sig_b64 = _b64url_encode(raw_sig)
238
+
239
+ return f"{header_b64}..{sig_b64}"
240
+
241
+ def sign_jws_detached(self, payload: bytes) -> str:
242
+ """Create a detached JWS with the delegation key (for manifests)."""
243
+ return self._sign_jws_detached_with(payload, self._private_key, self._kid)
244
+
245
+ def sign_jws_detached_audit(self, payload: bytes) -> str:
246
+ """Create a detached JWS with the audit key (for checkpoints)."""
247
+ return self._sign_jws_detached_with(payload, self._audit_private_key, self._audit_kid)
248
+
249
+ def _verify_jws_detached_with(self, jws: str, payload: bytes, public_key: ec.EllipticCurvePublicKey) -> None:
250
+ """Verify a detached JWS against the provided payload using the specified key."""
251
+ parts = jws.split(".")
252
+ if len(parts) != 3 or parts[1] != "":
253
+ raise ValueError("Invalid detached JWS format: expected 'header..signature'")
254
+ header_b64, _, sig_b64 = parts
255
+
256
+ payload_b64 = _b64url_encode(payload)
257
+ signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
258
+
259
+ raw_sig = _b64url_decode(sig_b64)
260
+ r = int.from_bytes(raw_sig[:32], "big")
261
+ s = int.from_bytes(raw_sig[32:], "big")
262
+ der_sig = encode_dss_signature(r, s)
263
+
264
+ public_key.verify(der_sig, signing_input, ec.ECDSA(hashes.SHA256()))
265
+
266
+ def verify_jws_detached(self, jws: str, payload: bytes) -> None:
267
+ """Verify a detached JWS with the delegation public key (for manifests)."""
268
+ self._verify_jws_detached_with(jws, payload, self._public_key)
269
+
270
+ def verify_jws_detached_audit(self, jws: str, payload: bytes) -> None:
271
+ """Verify a detached JWS with the audit public key (for checkpoints)."""
272
+ self._verify_jws_detached_with(jws, payload, self._audit_public_key)
@@ -0,0 +1,40 @@
1
+ """Verification helpers for ANIP signed artifacts."""
2
+
3
+ import hashlib
4
+
5
+ import jwt as pyjwt
6
+
7
+ from .canonicalize import canonicalize
8
+ from .keys import KeyManager
9
+
10
+
11
+ def verify_audit_entry_signature(
12
+ key_manager: KeyManager, entry: dict, signature: str
13
+ ) -> dict:
14
+ """Verify an audit entry's signature using the audit public key.
15
+
16
+ Returns the decoded JWT claims on success, raises on failure.
17
+ """
18
+ claims = pyjwt.decode(
19
+ signature,
20
+ key_manager.audit_public_key,
21
+ algorithms=["ES256"],
22
+ )
23
+ canonical = canonicalize(entry, exclude={"signature", "id"})
24
+ expected_hash = hashlib.sha256(canonical).hexdigest()
25
+ if claims.get("audit_hash") != expected_hash:
26
+ raise ValueError("Audit hash mismatch")
27
+ return claims
28
+
29
+
30
+ def verify_manifest_signature(
31
+ key_manager: KeyManager, manifest_bytes: bytes, signature: str
32
+ ) -> None:
33
+ """Verify a manifest's detached JWS signature using the delegation public key.
34
+
35
+ *manifest_bytes* is the raw JSON body returned by ``GET /anip/manifest``.
36
+ *signature* is the ``X-ANIP-Signature`` header value (detached JWS).
37
+
38
+ Raises on verification failure.
39
+ """
40
+ key_manager.verify_jws_detached(signature, manifest_bytes)
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: anip-crypto
3
+ Version: 0.11.0
4
+ Summary: ANIP cryptographic primitives — key management, JWT, JWS, JWKS
5
+ Author-email: ANIP Protocol <team@anip.dev>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/anip-protocol/anip
8
+ Keywords: anip,agent,protocol
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: anip-core==0.11.0
16
+ Requires-Dist: cryptography>=42.0
17
+ Requires-Dist: PyJWT[crypto]>=2.8
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/anip_crypto/__init__.py
4
+ src/anip_crypto/canonicalize.py
5
+ src/anip_crypto/jwks.py
6
+ src/anip_crypto/jws.py
7
+ src/anip_crypto/jwt.py
8
+ src/anip_crypto/keys.py
9
+ src/anip_crypto/verify.py
10
+ src/anip_crypto.egg-info/PKG-INFO
11
+ src/anip_crypto.egg-info/SOURCES.txt
12
+ src/anip_crypto.egg-info/dependency_links.txt
13
+ src/anip_crypto.egg-info/requires.txt
14
+ src/anip_crypto.egg-info/top_level.txt
15
+ tests/test_jwt_jws.py
16
+ tests/test_keys.py
@@ -0,0 +1,6 @@
1
+ anip-core==0.11.0
2
+ cryptography>=42.0
3
+ PyJWT[crypto]>=2.8
4
+
5
+ [dev]
6
+ pytest>=8.0
@@ -0,0 +1 @@
1
+ anip_crypto
@@ -0,0 +1,93 @@
1
+ """Tests for JWT, JWS, JWKS, and verification."""
2
+ import json
3
+
4
+ import pytest
5
+
6
+ from anip_crypto import (
7
+ KeyManager,
8
+ build_jwks,
9
+ canonicalize,
10
+ sign_jws_detached,
11
+ sign_jws_detached_audit,
12
+ sign_jwt,
13
+ verify_audit_entry_signature,
14
+ verify_jws_detached,
15
+ verify_jws_detached_audit,
16
+ verify_jwt,
17
+ verify_manifest_signature,
18
+ )
19
+
20
+
21
+ def test_jwt_sign_verify():
22
+ km = KeyManager()
23
+ token = sign_jwt(km, {"sub": "agent", "aud": "test-svc"})
24
+ claims = verify_jwt(km, token, audience="test-svc")
25
+ assert claims["sub"] == "agent"
26
+
27
+
28
+ def test_jwt_wrong_audience_fails():
29
+ km = KeyManager()
30
+ token = sign_jwt(km, {"sub": "agent", "aud": "svc-a"})
31
+ with pytest.raises(Exception):
32
+ verify_jwt(km, token, audience="svc-b")
33
+
34
+
35
+ def test_jws_detached_delegation():
36
+ km = KeyManager()
37
+ payload = b"manifest-bytes"
38
+ jws = sign_jws_detached(km, payload)
39
+ parts = jws.split(".")
40
+ assert len(parts) == 3
41
+ assert parts[1] == ""
42
+ verify_jws_detached(km, jws, payload)
43
+
44
+
45
+ def test_jws_detached_audit():
46
+ km = KeyManager()
47
+ payload = b"checkpoint-body"
48
+ jws = sign_jws_detached_audit(km, payload)
49
+ verify_jws_detached_audit(km, jws, payload)
50
+
51
+
52
+ def test_jws_delegation_key_cannot_verify_audit_signature():
53
+ km = KeyManager()
54
+ payload = b"data"
55
+ jws = sign_jws_detached_audit(km, payload)
56
+ with pytest.raises(Exception):
57
+ verify_jws_detached(km, jws, payload)
58
+
59
+
60
+ def test_build_jwks():
61
+ km = KeyManager()
62
+ jwks = build_jwks(km)
63
+ assert len(jwks["keys"]) == 2
64
+ assert jwks["keys"][0]["alg"] == "ES256"
65
+
66
+
67
+ def test_canonicalize():
68
+ data = {"b": 2, "a": 1, "signature": "remove-me"}
69
+ canonical = canonicalize(data, exclude={"signature"})
70
+ parsed = json.loads(canonical)
71
+ assert list(parsed.keys()) == ["a", "b"]
72
+
73
+
74
+ def test_verify_audit_entry_signature():
75
+ km = KeyManager()
76
+ entry = {"capability": "test", "timestamp": "2026-01-01T00:00:00Z", "success": True}
77
+ sig = km.sign_audit_entry(entry)
78
+ verify_audit_entry_signature(km, entry, sig)
79
+
80
+
81
+ def test_verify_manifest_signature():
82
+ km = KeyManager()
83
+ manifest_bytes = b'{"protocol":"anip/0.11","capabilities":{}}'
84
+ sig = sign_jws_detached(km, manifest_bytes)
85
+ verify_manifest_signature(km, manifest_bytes, sig)
86
+
87
+
88
+ def test_verify_manifest_signature_wrong_bytes_fails():
89
+ km = KeyManager()
90
+ manifest_bytes = b'{"protocol":"anip/0.11"}'
91
+ sig = sign_jws_detached(km, manifest_bytes)
92
+ with pytest.raises(Exception):
93
+ verify_manifest_signature(km, b"tampered", sig)
@@ -0,0 +1,48 @@
1
+ """Tests for KeyManager and key operations."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ from anip_crypto.keys import KeyManager
7
+
8
+
9
+ def test_generate_keys():
10
+ km = KeyManager()
11
+ jwks = km.get_jwks()
12
+ assert len(jwks["keys"]) == 2
13
+ assert jwks["keys"][0]["use"] == "sig"
14
+ assert jwks["keys"][1]["use"] == "audit"
15
+
16
+
17
+ def test_separate_key_ids():
18
+ km = KeyManager()
19
+ jwks = km.get_jwks()
20
+ delegation_kid = jwks["keys"][0]["kid"]
21
+ audit_kid = jwks["keys"][1]["kid"]
22
+ assert delegation_kid != audit_kid
23
+
24
+
25
+ def test_persist_and_load():
26
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
27
+ path = f.name
28
+ # Remove the empty file so KeyManager generates fresh keys
29
+ Path(path).unlink()
30
+ km1 = KeyManager(key_path=path)
31
+ jwks1 = km1.get_jwks()
32
+ km2 = KeyManager(key_path=path)
33
+ jwks2 = km2.get_jwks()
34
+ assert jwks1["keys"][0]["kid"] == jwks2["keys"][0]["kid"]
35
+ assert jwks1["keys"][1]["kid"] == jwks2["keys"][1]["kid"]
36
+ Path(path).unlink()
37
+
38
+
39
+ def test_kid_property():
40
+ km = KeyManager()
41
+ assert isinstance(km.kid, str)
42
+ assert len(km.kid) == 16
43
+
44
+
45
+ def test_audit_kid_property():
46
+ km = KeyManager()
47
+ assert isinstance(km.audit_kid, str)
48
+ assert km.audit_kid != km.kid