Pith.

Pith records as JSON

Each paper's Pith record can be fetched with a plain GET and no key: the record, its signed bundle, the automatic checks on it, its claims. This page lists each address with what it answers, for one real record.

A record's signature is checked with Pith's public key and nothing else. The recipe is on this page.

The record, and what an address takes for it

The examples below use one real record · arXiv:1706.03762

Attention Is All You Need

pith:2017:CXPXXMQRMHRHYZ2C7KEJ4A4ORY

A Pith number's 26 characters

CXPXXMQRMHRHYZ2C7KEJ4A4ORY

Taken by every address below.

Its first eight characters

CXPXXMQR

Taken by every address below.

An arXiv identifier

1706.03762

Taken by integrity.json, claims.json and the badge. For the rest, ask /api/pith-number/resolve for the record first.

The addresses

  1. GET /pith/{id}.json

    The whole record: what the number was made from, the signed receipt, a snapshot of the paper, and what has been added since.

    curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY.json'

    Answers 200, JSON with these fields: record_type, schema_url, pith_number, short_pith_number, schema_version, canonical_sha256, source, attestation_state, paper, verification_status, canonical_record, receipt, graph_snapshot, aliases, events, event_summary, paper_claims, inbound_citations, links, agent_actions, created_at, updated_at.

    GET /pith/{id} with the header Accept: application/json answers the same record as application/ld+json.

    open this answer

  2. GET /pith/{id}/bundle.json

    The signed bundle: the record, its other names, every signed addition, the timestamp proofs and the public keys to check them with.

    curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/bundle.json'

    Answers 200, JSON with these fields: bundle_type, bundle_version, pith_number, short_pith_number, canonical_record, canonical_sha256, source, source_aliases, events, timestamp_proofs, mirror_hints, public_keys, merge_version, built_at, links, state, bundle_signature.

    The same bundle is at /.well-known/pith/{id}/bundle.json and at /api/v1/bundles/{id}.

    open this answer

  3. GET /pith/{id}/state.json

    The record as it stands, worked out from the bundle's additions by a fixed rule. Work it out again from the bundle and the answers must match.

    curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/state.json'

    Answers 200, JSON with these fields: state_type, state_version, pith_number, merge_version, event_count, valid_event_count, invalid_event_count, equivocation_count, current, equivocations, invalid_events, applied_event_ids, state_sha256.

    open this answer

  4. GET /pith/{id}/integrity.json

    The automatic checks on the paper: which ran, what each found, and the newest signed record of each finding.

    curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/integrity.json'

    Answers 200, JSON with these fields: schema, pith_number, arxiv_id, integrity, events, endpoint_self, protocol_url.

    Also takes an arXiv identifier in place of the number.

    open this answer

  5. GET /pith/{id}/claims.json

    The claims Pith recorded for the paper, each with what it rests on.

    curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/claims.json'

    Answers 200, JSON with these fields: schema, pith_number, arxiv_id, summary, claims, endpoint_self, integrity_endpoint, bundle_endpoint.

    Also takes an arXiv identifier in place of the number.

    open this answer

  6. GET /api/pith-number/{id}/graph.json

    The snapshot of the paper alone: its details, its references and its checks, without the rest of the record.

    curl -s 'https://pith.science/api/pith-number/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/graph.json'

    Answers 200, JSON with these fields: paper, claims, source, verdict, integrity, references, author_claims, builder_version.

    open this answer

  7. GET /api/pith-number/{id}/events.json

    What has been added to the record, alone: a list, empty for a record nothing has been added to.

    curl -s 'https://pith.science/api/pith-number/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/events.json'

    Answers 200, a JSON list.

    open this answer

  8. GET /api/pith-number/resolve?arxiv={arxiv_id}&json=true

    The record of a paper you know only by its arXiv identifier (or ?doi= for a DOI).

    curl -s 'https://pith.science/api/pith-number/resolve?arxiv=1706.03762&json=true'

    Answers 200, JSON with these fields: record_type, schema_url, pith_number, short_pith_number, schema_version, canonical_sha256, source, attestation_state, paper, verification_status, canonical_record, receipt, graph_snapshot, aliases, events, event_summary, paper_claims, inbound_citations, links, agent_actions, created_at, updated_at.

    Without json=true it answers 302 to the record's page.

    open this answer

  9. GET /badge/{id}.svg

    A small picture that reads "pith:<number> reviewed", for an author's or a journal's page.

    curl -s 'https://pith.science/badge/CXPXXMQRMHRHYZ2C7KEJ4A4ORY.svg'

    Answers 200, an SVG picture.

    Also takes an arXiv identifier in place of the number.

    open this answer

  10. GET /pith-signing-key.json

    Pith's public key.

    curl -s 'https://pith.science/pith-signing-key.json'

    Answers 200, JSON with these fields: key_id, algorithm, format, public_key_b64, public_key_hex, fingerprint_sha256_b32_first128bits, fingerprint_sha256_hex, rotates_at, url, notes.

    open this answer

  11. GET /pith-mirrors.json

    The places that serve bundles. Today the list has one entry, Pith itself.

    curl -s 'https://pith.science/pith-mirrors.json'

    Answers 200, JSON with these fields: manifest_type, manifest_version, generated_at, merge_version, mirrors, notes.

    open this answer

Check a signature

Pith signs with one Ed25519 key. What is signed is always the 32 bytes of a SHA-256: for a record, its fingerprint (canonical_sha256). This raises an error when the signature is bad and prints nothing when it is good:

import base64, json, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

key = json.load(urllib.request.urlopen("https://pith.science/pith-signing-key.json"))
rec = json.load(urllib.request.urlopen("https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY.json"))

pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(key["public_key_b64"]))
pub.verify(base64.b64decode(rec["receipt"]["signature_b64"]),
           bytes.fromhex(rec["canonical_sha256"]))

The signed record of a finding follows the same rule: its payload_sha256 is the SHA-256 of the payload written as JSON with sorted keys and no spaces, and its signature is over those 32 bytes.

The badge

The badge says the paper was reviewed by Pith. It does not say the paper is right.

pith:CXPXXMQRMHRHYZ2C7KEJ4A4ORY reviewed

<a href="https://pith.science/paper/CXPXXMQR">
  <img src="https://pith.science/badge/CXPXXMQR.svg"
       alt="pith:CXPXXMQRMHRHYZ2C7KEJ4A4ORY reviewed" height="24">
</a>

The badge on a page of its own

The schemas

What to expect

  • No key and no account are needed for any address on this page: each is a plain GET.
  • integrity.json and claims.json send Cache-Control: public, max-age=300. The other addresses on this page send no caching header.
  • None of the addresses on this page sends Access-Control-Allow-Origin, so fetch them from a server, not from a page on another site. The same bundle at /api/v1/bundles/{id} does send it.
  • An identifier that is not a record: /pith/{id}.json, bundle.json, state.json, integrity.json, claims.json, graph.json, events.json and resolve answer 404 with {"detail": "Pith Number not found"}. The badge answers 200 all the same.
  • The version is in each schema's address; v1 is the only one. A change that would break a reader gets a new address.