Metadata-Version: 2.4
Name: citesig
Version: 0.1.1
Summary: CiteSig reference implementation — sign and verify cryptographic attestations for factual claims per the CiteSig v0.1 specification.
Author-email: Machine Accountability Corporation <security@deepinquiry.ai>
License: MIT
Project-URL: Homepage, https://citesig.org
Project-URL: Documentation, https://citesig.org/spec/
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://citesig.org/spec/). 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 every v0.1 conformance vector, which ship in the source distribution:

- 20 canonicalization vectors (`tests/vectors/v0.1/canonical.json`)
- 17 signing vectors, 6 accept and 11 reject — one for every rejection reason in spec §5.3 (`tests/vectors/v0.1/signatures.json`)

The signing vectors use a published, deliberately trivial test seed (the bytes 0x00 through 0x1f). It is not a key and must never be used as one.

Run the test suite from the source distribution:

```bash
pip download citesig==0.1.1 --no-binary :all: --no-deps
tar xzf citesig-0.1.1.tar.gz && cd citesig-0.1.1
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://citesig.org/spec/)
- [Website](https://citesig.org)

## Changes in 0.1.1

- Number canonicalization now follows RFC 8785 exactly (ECMA-262 `Number::toString`): whole-valued floats, exponent thresholds, exponent padding and negative zero now serialize byte-identically to the JavaScript implementation. Integers outside the exactly representable double range are rejected rather than rounded.
- `sign()` now fills in the default version when `v` is passed explicitly as `None`, matching the JavaScript implementation. Previously this produced an attestation nothing could verify.
- The conformance vectors now ship with the source distribution.
- Package metadata points to https://citesig.org/spec/.

## License

MIT for code and test vectors (this package). CiteSig is maintained by Machine Accountability Corporation.

## 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 and other issues to security@deepinquiry.ai.
