Metadata-Version: 2.4
Name: citesig
Version: 0.1.0
Summary: CiteSig reference implementation — sign and verify cryptographic attestations for factual claims per the CiteSig v0.1 specification.
Author: CiteSig Contributors
License: MIT
Project-URL: Homepage, https://citesig.org
Project-URL: Repository, https://github.com/citesig/spec
Project-URL: Documentation, https://github.com/citesig/spec/blob/main/spec/v0.1.md
Project-URL: Issues, https://github.com/citesig/spec/issues
Keywords: citesig,signatures,verification,trust,provenance,ed25519,did-key,jcs,rfc8785,signed-claims,ai-trust,attestation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41.0.0
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Dynamic: license-file

# citesig

**CiteSig** is an open protocol for embedding cryptographic verification signatures inside factual claims — a trust layer for the AI era.

`citesig` is the reference Python implementation of [CiteSig v0.1](https://github.com/citesig/spec/blob/main/spec/v0.1.md). It provides `sign()`, `verify()`, canonical serialization, and the compact form defined in the specification.

## Install

```bash
pip install citesig
```

Requires **Python 3.9 or later**. Depends on `cryptography` for Ed25519 primitives.

## Quick example

```python
import os
from datetime import datetime, timezone

from citesig import (
    sign,
    verify,
    ed25519,
    did_key_from_ed25519_public_key,
)

# Generate a signing key (32-byte seed).
seed = os.urandom(32)
public_key = ed25519.public_key_from_seed(seed)
signer = did_key_from_ed25519_public_key(public_key)

# Sign a claim.
attestation = sign(
    {
        "claim": "The Great Barrier Reef is approximately 2,300 km long.",
        "signer": signer,
        "sources": [
            {"url": "https://barrierreef.org/the-reef/facts"},
        ],
        "issued_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
    },
    seed,
)

# Verify it.
result = verify(attestation)
if result.ok:
    print("Signature verified.")
else:
    print(f"Rejected: {result.reason} — {result.detail}")
```

## API

### `sign(claim, private_key_seed) -> dict`

Produces a signed CiteSig attestation.

- **`claim`** — partial attestation mapping. `claim`, `signer`, `sources`, `issued_at` are required; `v` defaults to `citesig/0.1`. Extension fields with names containing a colon (e.g. `myapp:field`) are included in the signed content per spec §3.2.
- **`private_key_seed`** — 32-byte Ed25519 seed.
- **Returns** — a new dict with all input fields plus a populated `sig` (base64url-encoded 64-byte Ed25519 signature).

### `verify(attestation, *, resolve_signer=None) -> VerifyResult`

Verifies a CiteSig attestation per spec §5.3.

- **`attestation`** — a signed attestation mapping.
- **`resolve_signer`** — optional callable `(signer: str) -> Iterable[bytes]`. Called for `https://` signers. Not called for `did:key:` signers, which resolve offline.
- **Returns** — a `VerifyResult(ok, reason=None, detail=None)`. On failure, `reason` is one of:
  - `version-unknown` — `v` is not `citesig/0.1`
  - `field-missing` — a REQUIRED field is absent
  - `field-malformed` — a field has the wrong type or shape
  - `signature-invalid` — signature does not match the signed content
  - `signer-unresolvable` — signer identifier could not be resolved to a public key

Constants live on `RejectionReason` (e.g. `RejectionReason.SIGNATURE_INVALID`).

Use `verify_async(...)` if you need an `async` `resolve_signer`.

### `canonicalize(value) -> bytes`

Returns the [JCS (RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785) canonicalization of `value` as UTF-8 bytes. This is the signing input format per spec §4.

### `to_compact(attestation) -> str`

Encodes a signed attestation as its compact form per spec §6:

```
citesig:0.1:<b64u(claim)>:<b64u(signer)>:<b64u(sourcedigest)>:<b64u(issued_at)>:<b64u(sig)>
```

where `sourcedigest` is `SHA-256(JCS(sources))`. Compact form is **lossy for sources** — recipients can verify the signature but must obtain the full attestation to enumerate sources.

### `parse_compact(compact) -> CompactAttestation`

Parses a compact-form string into a `CompactAttestation` dataclass with `v`, `claim`, `signer`, `source_digest` (32 bytes), `issued_at`, `sig`. The original `sources` list cannot be recovered from a compact-form string.

### Utility exports

- **`ed25519.public_key_from_seed(seed) -> bytes`** — derive the public key from a 32-byte seed.
- **`ed25519.sign(message, seed) -> bytes`** — raw Ed25519 signing (64-byte signature).
- **`ed25519.verify(message, signature, public_key) -> bool`** — raw Ed25519 verification.
- **`ed25519_public_key_from_did_key(did) -> bytes`** — decode a `did:key:z...` to its 32-byte public key.
- **`did_key_from_ed25519_public_key(public_key) -> str`** — encode a 32-byte public key as `did:key:z...`.
- **`base64url.encode(bytes) -> str`** / **`base64url.decode(str) -> bytes`** — RFC 4648 §5 (no padding).

## Conformance

This implementation passes all v0.1 conformance vectors from the specification:

- 3 ACCEPT signature vectors (two sources / empty sources / extension field)
- 3 REJECT signature vectors (claim tampering / unknown version / extension stripping)
- 4 canonicalization vectors (key ordering / whitespace / UTF-8 / empty arrays)

Run the test suite yourself:

```bash
git clone https://github.com/citesig/spec
cd spec/impl/py
pip install -e ".[test]"
pytest
```

The JavaScript reference implementation (`@citesig/core` on npm) and this Python implementation are byte-for-byte compatible — signatures produced by one verify with the other.

## Specification

- [CiteSig v0.1 specification](https://github.com/citesig/spec/blob/main/spec/v0.1.md)
- [Test vectors](https://github.com/citesig/spec/tree/main/test-vectors/v0.1)
- [Website](https://citesig.org)

## License

MIT for code (this package). CC-BY-4.0 for specification prose in the main repository.

## Security

**A valid CiteSig signature does not mean the claim is true.** It means the signer identified by `signer` produced the attestation, and the claim + sources + timestamp + extension fields have not been altered since. Consumers building trust decisions on top of CiteSig must layer signer reputation, source quality, and independent verification on top.

Report vulnerabilities to security@citesig.org (or open an issue at [github.com/citesig/spec/issues](https://github.com/citesig/spec/issues) for non-sensitive reports).
