Metadata-Version: 2.4
Name: qhermes-a2a
Version: 26.0.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Security :: Cryptography
Summary: Post-quantum A2A identity, authorization, and KEM session handshake
Author-email: Copertino <hello@copertino.world>
License: LicenseRef-Copertino-1.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# qhermes-a2a

Python bindings for `qhermes-a2a`.

Part of **QHermes 26** by Copertino.

Thank you for choosing QHermes 26.

Agent Card signing, credential chain authorization, and ML-KEM-768 session
handshake for Python applications implementing the A2A protocol.

Wheels target the stable ABI (abi3) and are compatible with Python 3.9
and later.

## Installation

```
pip install qhermes-a2a
```

## Design

**Two layers for two audiences.** `qhermes.a2a` is the Python API for application code — `NamedTuple` types, automatic nonce management, base64 handling, and Agent Card helpers. `qhermes_a2a` is the low-level extension module with direct bindings to the Rust primitives. The high-level layer is appropriate for application code. The low-level layer provides direct control over wire bytes and nonces.

**`Session` is immutable and single-use per call.** `seal` returns `(blob, next_session)`. The previous session must not be reused. Enforcement is by convention — Python does not consume values. The counter in `Session` is monotonic and starts at zero. At 2^96 calls `seal` raises `OverflowError`.

**Nonce management is automatic in the high-level layer.** `seal` derives the 12-byte nonce from the session counter and prepends it to the blob. `open` reads it back. When using `SessionKey` directly from the low-level layer, nonce uniqueness is the caller's responsibility.

**`context` in the KEM handshake must match on both sides.** It is forwarded to HKDF as domain separation material. Both parties must supply the same value. Supply a stable task ID, session ID, or connection ID.

**`seal_auth` accepts any sequence.** The Python high-level `seal_auth` accepts a `Sequence[Credential]`. Order is significant: root credential first, in chain order.

**Agent Card bytes must be canonicalized before signing.** `sign_agent_card` signs the bytes supplied. A verifier that re-canonicalizes before checking will reject signatures over non-canonical input. JCS (RFC 8785) is the recommended format: keys sorted, no extra whitespace, signature field omitted before signing.

___

## High-level API — `qhermes.a2a`

### KEM endpoint derivation

```python
from qhermes.a2a import derive_endpoint

endpoint = derive_endpoint(dsa_seed=seed_32)
ek = endpoint.encapsulation_key  # publish in Agent Card
```

### KEM session handshake

```python
from qhermes.a2a import kem_offer, kem_accept

offer_blob, session = kem_offer(peer_ek=ek, context=task_id.encode())
session = kem_accept(endpoint, offer=offer_blob, context=task_id.encode())
```

### Encrypting and decrypting message bodies

```python
from qhermes.a2a import seal, open_blob

blob, next_session = seal(session, plaintext=b"hello", aad=metadata_bytes)
plaintext          = open_blob(next_session, blob, aad=metadata_bytes)
```

### Agent Card signing and extension

```python
from qhermes.a2a import sign_agent_card, verify_agent_card
from qhermes.a2a import agent_card_security_extension, extract_agent_card_ek

sig = sign_agent_card(master=seed_32, deployment=b"prod", context=b"agent-0", card_bytes=canonical_json)
verify_agent_card(root_pk=pk, card_bytes=canonical_json, sig=sig)

card = {"name": "my-agent", "extensions": {**agent_card_security_extension(root_pk, endpoint)}}
root_pk, ek = extract_agent_card_ek(card)
```

### Authorization metadata

```python
from qhermes.a2a import seal_auth, verify_auth
import time

wire = seal_auth([root_cred, agent_cred])
n    = verify_auth(root_pk=pk, wire=wire, now=int(time.time()))
```

### `message.metadata` helpers

```python
from qhermes.a2a import pack_metadata, unpack_metadata

metadata = pack_metadata(auth_wire=wire, offer_blob=offer_blob)
auth_wire, offer_blob = unpack_metadata(message["metadata"] or {})
```

___

## Low-level API — `qhermes_a2a`

### Constants

| Name | Value | Description |
|---|---|---|
| `PK_SIZE` | 1952 | ML-DSA-65 public key, bytes |
| `SIG_SIZE` | 3309 | ML-DSA-65 signature, bytes |
| `EK_SIZE` | 1184 | ML-KEM-768 encapsulation key, bytes |
| `DK_SEED_SIZE` | 64 | KEM decapsulation key seed, bytes |
| `NONCE_SIZE` | 12 | ChaCha20-Poly1305 nonce, bytes |
| `TAG_SIZE` | 16 | ChaCha20-Poly1305 authentication tag, bytes |
| `KEM_OFFER_SIZE` | 1088 | KEM offer blob, bytes |

### KEM handshake

```python
import qhermes_a2a as a2a

ek, dk_seed = a2a.derive_kem_keypair(dsa_seed=seed_32)
offer_blob, initiator_key = a2a.kem_offer(peer_ek=ek, context=b"task-1")
responder_key             = a2a.kem_accept(dk_seed=dk_seed, offer=offer_blob, context=b"task-1")
```

### `SessionKey`

```python
ct = initiator_key.seal(nonce=nonce_12, plaintext=b"msg", aad=b"")
pt = responder_key.open(nonce=nonce_12, ciphertext=ct, aad=b"")
```

Nonce must be exactly `NONCE_SIZE` (12) bytes and must not be reused under the same key.

___

## License

Copertino Source License 1.0. Change Date: 2036-01-01. Change License: Apache-2.0.
See [LICENSE](../LICENSE).

Copyright © 2026 Copertino. All rights reserved.

