Skip to content

Python API

noqtis: post-quantum signatures, KEMs, hashes and sealed capsules.

Python binding of the noqtis C library (FIPS 203, 204, 205, 202, 180-4 and HQC), compiled into the package; no runtime dependencies.

>>> import noqtis
>>> pk, sk = noqtis.sign.keygen("ML-DSA-44")
>>> noqtis.seal("results", sk, pk, meta={"project": "x"}) # doctest: +SKIP
>>> noqtis.check("results", pk).ok # doctest: +SKIP

True

algs(kind: Optional[str]=None) -> List[Alg]

Every algorithm with its sizes; kind ‘sign’ or ‘kem’ filters.

hashes() -> List[str]

The hash names hash, hmac and prehash= take.

hash(alg: str, data: Bytes, n: Optional[int]=None) -> bytes

The digest of data. alg is ‘SHA-256’, ‘SHA3-512’, ‘SHAKE128’, … (hashlib spellings such as ‘sha3_256’ work too). n is the output length for SHAKE (default 32 for SHAKE128, 64 for SHAKE256); other hashes have a fixed length and take no n.

hmac(alg: str, key: Bytes, data: Bytes) -> bytes

HMAC of data under key with a SHA-2 or SHA-3 hash.

class Row(NamedTuple)

One check of a capsule: its name, whether it passed, and why.

class Report()

What check found: rows lists every check; ok is True only when the library passed the capsule as a whole.

  • failed: The rows that did not pass.
seal(dir: PathLike, sk: Bytes, pk: Bytes, meta: Union[None, str, Mapping[str, Any]]=None, alg: Optional[AlgLike]=None, format: Optional[int]=None, inputs: Optional[Iterable[Any]]=None) -> None

Writes noqtis.json and noqtis.sig into dir, covering every regular file in it. meta is a JSON object (a dict, or its text). alg defaults to the algorithm of the key pair. The creation time is SOURCE_DATE_EPOCH when set, else the clock.

format=2 writes capsule format 2: the manifest carries a Merkle tree over its files, so that prove can show some files without the others. inputs (format 2, the default with it) records the capsules this one was made from: each a capsule folder (named by its last component) or a (name, folder) pair, or (name, id) with the 32-byte id from capid. Check the inputs before you name them; check(lineage=...) follows them back.

capid(dir: PathLike) -> bytes

The capsule’s id (32 bytes): the SHA-256 of what its seal signs, as seal(inputs=...) records it. Nothing is checked: check the capsule first.

check(dir: Optional[PathLike]=None, pk: Optional[Bytes]=None, anchors: Optional[Iterable[Bytes]]=None, only: Union[None, str, Iterable[str]]=None, lineage: Union[None, PathLike, Iterable[PathLike]]=None, keys: Optional[Iterable[Union[Bytes, PathLike]]]=None, proof: Union[None, Bytes, PathLike]=None) -> Report

Checks dir against its seal. With pk the seal must be by that key; without, the key in the seal is trusted (a row says so). anchors are DER TSA certificates (at most 32): the seal must then carry an RFC 3161 timestamp that verifies under one of them.

only checks just these files (paths as the manifest lists them, ‘/’ between folders); “partial” rows say what was left out. lineage are capsule folders in which the inputs of a format 2 capsule are found by id and checked, to 64 levels (“lineage” rows). keys are key statements (from rotate or revoke, as bytes or a file path; at most 32) and need pk: the seal may then be by a key pk was rotated to, and must carry a timestamp from before its key was rotated away or revoked.

proof (bytes, or a file path) checks a proof from prove instead of a folder: its seal, its signature and each proven entry; with dir too, the proven files there. only, lineage and keys do not apply to a proof.

prove(dir: PathLike, paths: Union[str, Iterable[str]]) -> bytes

A proof (noqtis-proof-1, JSON) that paths are files of the format 2 capsule in dir: the seal, the manifest’s head and each file’s entry and Merkle audit path, and nothing of the other files. check(proof=...) checks it. Check the capsule first.

class File(NamedTuple)

One file a manifest lists: its path, size in bytes and SHA-256 (64 hex digits).

files(dir: PathLike) -> List[File]

The files the capsule’s manifest lists, in manifest order, read without checking anything (check does that).

rotate(sk: Bytes, pk: Bytes, nsk: Bytes, npk: Bytes, at: Union[None, int, float, datetime.datetime]=None, reason: Optional[str]=None, alg: Optional[AlgLike]=None, nalg: Optional[AlgLike]=None) -> bytes

A key statement (noqtis-keys-1, JSON) that the key pair (sk, pk) is replaced by (nsk, npk) from at (seconds since 1970 or an aware datetime; default now), signed by both keys. reason is up to 255 bytes of text. alg and nalg default to the pairs’ algorithms; both must be post-quantum (Ed25519 alone is refused). Give it to check(keys=...).

revoke(sk: Bytes, pk: Bytes, at: Union[None, int, float, datetime.datetime]=None, reason: Optional[str]=None, alg: Optional[AlgLike]=None) -> bytes

A key statement (noqtis-keys-1, JSON) that the key pair (sk, pk) is revoked from at (default now), signed by it; as rotate.

class Card(NamedTuple)

An identity card that verified: who it names (None for a field it has not), when it was made, and its key: the algorithm, the public key and its SHA-256 in hex, as noqtis inspect shows a seal’s.

idcard(sk: Bytes, pk: Bytes, name: str, orcid: Optional[str]=None, email: Optional[str]=None, url: Optional[str]=None, affil: Optional[str]=None, at: Union[None, int, float, datetime.datetime]=None, alg: Optional[AlgLike]=None) -> bytes

An identity card (noqtis-idcard-1, JSON): the public key pk with name and, when given, an ORCID iD (its check digit verified; its https://orcid.org/ address is stored bare), an e-mail address, an https:// web page and an affiliation, made at at (default now) and signed by sk under the card’s own context. A field out of its limits (see noqtis-idcard(1)) raises ArgError; Ed25519 alone raises UnsupportedError.

idcheck(card: Union[Bytes, str, 'os.PathLike[str]'], pk: Optional[Bytes]=None) -> Optional[Card]

The card’s fields when it verifies (and, with pk, is for that key); None when its signature does not verify or it is for another key. card is the card’s bytes or the path of a card file. Input that is not a card raises DecodeError. A card is self-signed: compare key_sha256 with the one its holder published, or pass pk.

Signatures: ML-DSA (FIPS 204), SLH-DSA (FIPS 205), Ed25519 (RFC 8032) and the hybrids ML-DSA-44/65/87+Ed25519, which need both parts to verify.

>>> from noqtis import sign
>>> pk, sk = sign.keygen("ML-DSA-65")
>>> sig = sign.sign("ML-DSA-65", sk, b"data")
>>> sign.verify("ML-DSA-65", pk, b"data", sig)

True

noqtis.sign.rndlen(alg: AlgLike) -> int

Bytes of per-signature randomness: 32 for ML-DSA and the hybrids (ML-DSA-65+Ed25519, …), n for SLH-DSA, 0 for Ed25519.

noqtis.sign.keygen(alg: AlgLike, seed: Optional[Bytes]=None) -> Tuple[bytes, bytes]

A new key pair (pk, sk). With seed (xi for ML-DSA, 32 bytes; SK.seed || SK.prf || PK.seed for SLH-DSA, 3n bytes; the 32-byte private key for Ed25519; xi || the Ed25519 key, 64 bytes, for a hybrid) the pair is the standard’s deterministic one; without, it comes from the OS CSPRNG.

noqtis.sign.sign(alg: AlgLike, sk: Bytes, msg: Bytes, ctx: Bytes=b'', deterministic: bool=False, prehash: Optional[str]=None, rnd: Optional[Bytes]=None) -> bytes

The signature of msg. ctx is the context string (at most 255 bytes). deterministic uses all-zero randomness (the standard’s deterministic variant); rnd gives the randomness explicitly. prehash names a hash (‘SHA-512’, ‘SHAKE256’, …) for HashML-DSA / HashSLH-DSA: msg is still the message, hashed here.

noqtis.sign.verify(alg: AlgLike, pk: Bytes, msg: Bytes, sig: Bytes, ctx: Bytes=b'', prehash: Optional[str]=None) -> bool

True when sig is a valid signature of msg under pk; False for any invalid or malformed signature. A public key of the wrong size raises DecodeError; a context over 255 bytes, ArgError.

Key encapsulation: ML-KEM (FIPS 203) and HQC.

>>> from noqtis import kem
>>> pk, sk = kem.keygen("ML-KEM-768")
>>> ct, ss = kem.encap("ML-KEM-768", pk)
>>> kem.decap("ML-KEM-768", sk, ct) == ss

True

noqtis.kem.keygen(alg: AlgLike, seed: Optional[Bytes]=None) -> Tuple[bytes, bytes]

A new key pair (pk, sk); with seed (d || z for ML-KEM) the standard’s deterministic one.

noqtis.kem.encap(alg: AlgLike, pk: Bytes, seed: Optional[Bytes]=None) -> Tuple[bytes, bytes]

(ciphertext, shared secret) for pk; seed (m for ML-KEM) makes it deterministic, for test vectors.

noqtis.kem.decap(alg: AlgLike, sk: Bytes, ct: Bytes) -> bytes

The shared secret in ct. A tampered ciphertext gives an unrelated pseudorandom secret, not an error (implicit rejection); only a ciphertext or key of the wrong size raises.

DSSE envelopes (the in-toto and SLSA signing wrapper), signed with any noqtis signature algorithm.

>>> from noqtis import dsse, sign
>>> pk, sk = sign.keygen("ML-DSA-65")
>>> env = dsse.sign("ML-DSA-65", sk, pk, "application/vnd.in-toto+json",

… b’{“a”:1}’)

>>> dsse.verify("ML-DSA-65", pk, env)

Signed(type=‘application/vnd.in-toto+json’, payload=b’{“a”:1}’)

class Signed(NamedTuple)

What a verified envelope carries: its payloadType and payload.

noqtis.dsse.sign(alg: AlgLike, sk: Bytes, pk: Bytes, type: str, payload: Union[Bytes, str]) -> bytes

A DSSE envelope (canonical JSON) of payload (bytes, or a str as its UTF-8) with payloadType type (1 to 255 printable ASCII bytes, no space, quote or backslash), signed by the key pair (sk, pk) over the envelope’s pre-authentication encoding; its keyid is the hex SHA-256 of pk. The signature is checked before the envelope is returned.

noqtis.dsse.verify(alg: AlgLike, pk: Bytes, env: Union[Bytes, str]) -> Optional[Signed]

The envelope’s (type, payload) when one of its signatures (at most 16) verifies under pk; None when none does. An env that is not a DSSE envelope raises DecodeError.

The CLI’s key files (DESIGN.md, “Key files”): a header line noqtis-key-1 <algorithm> <pub|sec> and the key bytes as padded base64 in lines of 64 characters, as noqtis keygen writes them.

decode is as strict as the CLI’s reader: the exact header, \n or \r\n line ends and nothing else outside the base64 alphabet, canonical padding, and the algorithm’s exact key size. Python’s base64 is not constant time; decode secret key files where timing is not observable.

>>> pk, sk = noqtis.sign.keygen("ML-DSA-44") # doctest: +SKIP
>>> noqtis.keyfile.write("me", "ML-DSA-44", "sec", sk) # doctest: +SKIP
>>> noqtis.keyfile.read("me").alg # doctest: +SKIP

‘ML-DSA-44’

class Key(NamedTuple)

A decoded key file: the algorithm’s name, ‘pub’ or ‘sec’, the key.

noqtis.keyfile.encode(alg: str, kind: str, key: Bytes) -> str

The key file text for key: kind is ‘pub’ or ‘sec’.

noqtis.keyfile.decode(text: Union[str, Bytes]) -> Key

The algorithm, kind and key of key file text (str or bytes).

noqtis.keyfile.read(file: object, kind: Optional[str]=None) -> Key

Reads a key file; kind ‘pub’ or ‘sec’ requires that kind.

noqtis.keyfile.write(file: object, alg: str, kind: str, key: Bytes, force: bool=False) -> None

Writes a key file. A secret key file is created owner-only, never through a link; an existing file is kept unless force, which removes it first so the new one never inherits its permissions.

class NoqtisError(Exception)

A noqtis call failed; code is the library’s NQE* code.

class ArgError(NoqtisError, ValueError)

An argument is out of range (NQEARG).

class BufError(NoqtisError)

An output buffer is too small (NQEBUF).

class VerifyError(NoqtisError)

A signature, MAC or digest does not verify (NQEVRFY).

class DecodeError(NoqtisError, ValueError)

Input is not in the encoding the standard defines (NQEDEC).

class RandError(NoqtisError)

The operating system’s random source failed (NQERNG).

class MemError(NoqtisError, MemoryError)

An allocation failed (NQEMEM).

class FileError(NoqtisError)

A file could not be read or written (NQEIO).

class UnsupportedError(NoqtisError)

An algorithm or option noqtis does not provide (NQENOSUP).

class LimitError(NoqtisError, ValueError)

Input exceeds a size or depth limit (NQELIM).

class StateError(NoqtisError)

A context was used out of order (NQESTATE).