Use from Python
The noqtis Python package has the C library compiled into it: no
runtime dependencies, one wheel per platform for CPython 3.9 and later.
pip install noqtis # or: uv add noqtisSeal, check, sign
Section titled “Seal, check, sign”Save this as prov.py and run python prov.py:
import pathlibimport tempfile
import noqtisfrom noqtis import kem, sign
# a folder with some resultsd = pathlib.Path(tempfile.mkdtemp()) / "results"(d / "data").mkdir(parents=True)(d / "data" / "in.csv").write_text("a,b\n1,2\n")(d / "report.txt").write_text("r\n")
# seal it, then check it with the public key pinnedpk, sk = sign.keygen("ML-DSA-65")noqtis.seal(d, sk, pk, meta={"project": "tutorial"})report = noqtis.check(d, pk)print("check:", report.ok, len(report), "checks")
# change a file: the check fails and says whywith open(d / "report.txt", "a") as f: f.write("edited\n")report = noqtis.check(d, pk)print("after edit:", report.ok)for row in report.failed: print("FAIL", row.check, row.detail)
# a detached signature with a context stringsig = sign.sign("ML-DSA-65", sk, b"the results in table 2", ctx=b"app-v1")print("verify:", sign.verify("ML-DSA-65", pk, b"the results in table 2", sig, ctx=b"app-v1"))print("other context:", sign.verify("ML-DSA-65", pk, b"the results in table 2", sig))
# key encapsulation and hasheskpk, ksk = kem.keygen("ML-KEM-768")ct, ss = kem.encap("ML-KEM-768", kpk)print("decap matches:", kem.decap("ML-KEM-768", ksk, ct) == ss)print("sha3-256:", noqtis.hash("SHA3-256", b"abc").hex())It prints:
check: True 22 checksafter edit: FalseFAIL file report.txt: size differsverify: Trueother context: Falsedecap matches: Truesha3-256: 3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532noqtis.seal(dir, sk, pk, meta=...)writesnoqtis.jsonandnoqtis.siginto the folder;metais any JSON-serialisable object, signed with the manifest and never interpreted.noqtis.check(dir, pk)returns aReport:okisTrueonly when every check passed,rowslists every(check, ok, detail), andfailedthe rows that did not pass. Withoutpkit accepts the key in the seal, and its report says so.sign.verifyreturnsTrueorFalseand never raises for a bad signature. A context string (ctx) binds a signature to one use.- Every other failure raises a subclass of
noqtis.NoqtisError(ArgError,FileError,UnsupportedError, …) with the library’s code in.code.
Identity cards
Section titled “Identity cards”An identity card ties your signing key to who you are, signed by the key
itself; noqtis check --card and idcheck read it. Save this as
card.py:
import noqtisfrom noqtis import sign
pk, sk = sign.keygen("ML-DSA-65")card = noqtis.idcard(sk, pk, "Ada Lovelace", orcid="https://orcid.org/0000-0002-1825-0097", url="https://example.org/ada")
c = noqtis.idcheck(card, pk) # None unless it verifies and is pk'sprint("name:", c.name)print("orcid:", c.orcid)print("key:", c.key_sha256 == noqtis.hash("SHA-256", pk).hex())
# a changed card does not verify; a wrong check digit is refusedprint("changed:", noqtis.idcheck(card.replace(b"Ada", b"Eve")))try: noqtis.idcard(sk, pk, "Ada Lovelace", orcid="0000-0002-1825-0098")except noqtis.ArgError: print("bad ORCID iD refused")It prints:
name: Ada Lovelaceorcid: 0000-0002-1825-0097key: Truechanged: Nonebad ORCID iD refusedThe card holds the bare ORCID iD. It is self-signed: it shows that the
holder of the key wrote it, not that the name is true, so give the key’s
SHA-256 (c.key_sha256) where people already know you, or let readers
pin your public key.
- Inputs may be
bytes,bytearrayormemoryview; outputs arebytes. Python cannot wipebytes, so a secret key you hold in Python lives until the garbage collector frees it. Keep long-term keys in key files made bynoqtis keygenand seal from a short-lived process;noqtis.keyfile.read("me", "sec")loads one. - Long operations release the GIL, so threads sign and verify in parallel.
- The package is typed (
py.typed).
The full API is in the package’s README.