primarysource 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- primarysource/__init__.py +5 -0
- primarysource/bbf/__init__.py +56 -0
- primarysource/bbf/_xml.py +63 -0
- primarysource/bbf/catalogue.json +777 -0
- primarysource/bbf/catalogue.py +137 -0
- primarysource/bbf/cli.py +79 -0
- primarysource/bbf/datamodel.py +281 -0
- primarysource/bbf/importer.py +151 -0
- primarysource/bbf/library.py +380 -0
- primarysource/bbf/server.py +72 -0
- primarysource/budget.py +63 -0
- primarysource/c2sp/__init__.py +53 -0
- primarysource/c2sp/_fetch.py +5 -0
- primarysource/c2sp/catalogue.json +231 -0
- primarysource/c2sp/catalogue.py +95 -0
- primarysource/c2sp/cli.py +62 -0
- primarysource/c2sp/importer.py +90 -0
- primarysource/c2sp/library.py +309 -0
- primarysource/c2sp/markdown.py +31 -0
- primarysource/c2sp/server.py +68 -0
- primarysource/cavp/__init__.py +42 -0
- primarysource/cavp/cli.py +84 -0
- primarysource/cavp/data/vectors.toml +47 -0
- primarysource/cavp/importer.py +156 -0
- primarysource/cavp/model.py +279 -0
- primarysource/cavp/parse.py +269 -0
- primarysource/cavp/paths.py +17 -0
- primarysource/cavp/server.py +67 -0
- primarysource/cavp/tools.py +169 -0
- primarysource/cli.py +107 -0
- primarysource/cursor.py +82 -0
- primarysource/cve/__init__.py +5 -0
- primarysource/cve/__main__.py +6 -0
- primarysource/cve/cli.py +144 -0
- primarysource/cve/collection.py +20 -0
- primarysource/cve/config.py +58 -0
- primarysource/cve/errors.py +16 -0
- primarysource/cve/kernel.py +232 -0
- primarysource/cve/mcp.py +139 -0
- primarysource/cve/parse.py +318 -0
- primarysource/cve/py.typed +0 -0
- primarysource/cve/service.py +760 -0
- primarysource/cve/store.py +214 -0
- primarysource/cve/sync.py +438 -0
- primarysource/cve/views.py +304 -0
- primarysource/debpolicy/__init__.py +16 -0
- primarysource/debpolicy/catalogue.py +263 -0
- primarysource/debpolicy/cli.py +12 -0
- primarysource/debpolicy/server.py +11 -0
- primarysource/draft/__init__.py +11 -0
- primarysource/draft/catalogue.py +117 -0
- primarysource/draft/cli.py +149 -0
- primarysource/draft/collection.py +21 -0
- primarysource/draft/datatracker.py +158 -0
- primarysource/draft/importer.py +163 -0
- primarysource/draft/licence.py +89 -0
- primarysource/draft/names.py +64 -0
- primarysource/draft/net.py +133 -0
- primarysource/draft/segment.py +192 -0
- primarysource/draft/server.py +91 -0
- primarysource/draft/status.py +146 -0
- primarysource/draft/store.py +199 -0
- primarysource/draft/tracked.json +104 -0
- primarysource/draft/views.py +430 -0
- primarysource/draft/xmlmeta.py +74 -0
- primarysource/ecma/__init__.py +52 -0
- primarysource/ecma/_pdf.py +143 -0
- primarysource/ecma/catalogue.json +22 -0
- primarysource/ecma/catalogue.py +100 -0
- primarysource/ecma/cli.py +67 -0
- primarysource/ecma/identify.py +25 -0
- primarysource/ecma/importer.py +108 -0
- primarysource/ecma/library.py +240 -0
- primarysource/ecma/server.py +67 -0
- primarysource/errors.py +29 -0
- primarysource/fetch.py +360 -0
- primarysource/fsutil.py +150 -0
- primarysource/gitdocs/__init__.py +15 -0
- primarysource/gitdocs/catalogue.py +116 -0
- primarysource/gitdocs/cli.py +12 -0
- primarysource/gitdocs/server.py +8 -0
- primarysource/home.py +65 -0
- primarysource/iana/__init__.py +32 -0
- primarysource/iana/catalogue.json +244 -0
- primarysource/iana/catalogue.py +69 -0
- primarysource/iana/cli.py +69 -0
- primarysource/iana/importer.py +157 -0
- primarysource/iana/model.py +325 -0
- primarysource/iana/reader.py +315 -0
- primarysource/iana/server.py +67 -0
- primarysource/iana/store.py +83 -0
- primarysource/iana/xmlsafe.py +47 -0
- primarysource/identify.py +186 -0
- primarysource/ieee/__init__.py +58 -0
- primarysource/ieee/_html.py +49 -0
- primarysource/ieee/catalogue.json +74 -0
- primarysource/ieee/catalogue.py +123 -0
- primarysource/ieee/cli.py +79 -0
- primarysource/ieee/identify.py +24 -0
- primarysource/ieee/ieee802.py +47 -0
- primarysource/ieee/importer.py +305 -0
- primarysource/ieee/posix.py +233 -0
- primarysource/ieee/server.py +79 -0
- primarysource/index.py +446 -0
- primarysource/iso/__init__.py +51 -0
- primarysource/iso/cli.py +98 -0
- primarysource/iso/designation.py +95 -0
- primarysource/iso/identify.py +100 -0
- primarysource/iso/importer.py +224 -0
- primarysource/iso/library.py +249 -0
- primarysource/iso/server.py +69 -0
- primarysource/itu/__init__.py +41 -0
- primarysource/itu/__main__.py +8 -0
- primarysource/itu/asn1mod.py +478 -0
- primarysource/itu/catalog.py +472 -0
- primarysource/itu/cli.py +445 -0
- primarysource/itu/collection.py +20 -0
- primarysource/itu/corpus.py +118 -0
- primarysource/itu/errata.py +16 -0
- primarysource/itu/formal.py +565 -0
- primarysource/itu/identify.py +26 -0
- primarysource/itu/index.py +490 -0
- primarysource/itu/mcp.py +474 -0
- primarysource/itu/py.typed +0 -0
- primarysource/itu/sources.py +600 -0
- primarysource/itu/text.py +609 -0
- primarysource/kconfig/__init__.py +16 -0
- primarysource/kconfig/catalogue.py +98 -0
- primarysource/kconfig/cli.py +12 -0
- primarysource/kconfig/server.py +8 -0
- primarysource/model.py +63 -0
- primarysource/nist/__init__.py +5 -0
- primarysource/nist/collection.py +46 -0
- primarysource/nist/data/acquired-nist.json +1657 -0
- primarysource/nist/data/nist-catalog.json +12090 -0
- primarysource/nist/data/sources.toml +1996 -0
- primarysource/nist/identify.py +41 -0
- primarysource/nist/ingest/__init__.py +1 -0
- primarysource/nist/ingest/acquire_nist.py +128 -0
- primarysource/nist/ingest/controls.py +82 -0
- primarysource/nist/ingest/discover_nist.py +218 -0
- primarysource/nist/ingest/extract.py +48 -0
- primarysource/nist/ingest/fetch.py +211 -0
- primarysource/nist/ingest/index.py +14 -0
- primarysource/nist/ingest/oscal.py +480 -0
- primarysource/nist/ingest/prose_layouts.py +236 -0
- primarysource/nist/ingest/sources.py +94 -0
- primarysource/nist/ingest/status.py +255 -0
- primarysource/nist/models.py +7 -0
- primarysource/nist/paths.py +33 -0
- primarysource/nist/server/__init__.py +3 -0
- primarysource/nist/server/corpus.py +38 -0
- primarysource/nist/server/mcp.py +19 -0
- primarysource/nist/server/tools.py +35 -0
- primarysource/nist/spec.py +38 -0
- primarysource/pdfcorpus/__init__.py +16 -0
- primarysource/pdfcorpus/corpus.py +159 -0
- primarysource/pdfcorpus/fts.py +175 -0
- primarysource/pdfcorpus/index.py +70 -0
- primarysource/pdfcorpus/models.py +30 -0
- primarysource/pdfcorpus/server.py +96 -0
- primarysource/pdfcorpus/spec.py +37 -0
- primarysource/pdfcorpus/split.py +391 -0
- primarysource/pdfcorpus/tools.py +244 -0
- primarysource/pep/__init__.py +15 -0
- primarysource/pep/catalogue.py +191 -0
- primarysource/pep/cli.py +12 -0
- primarysource/pep/server.py +8 -0
- primarysource/pkgdocs/__init__.py +19 -0
- primarysource/pkgdocs/fetch.py +356 -0
- primarysource/pkgdocs/index.py +89 -0
- primarysource/pkgdocs/model.py +76 -0
- primarysource/pkgdocs/readers/__init__.py +1 -0
- primarysource/pkgdocs/readers/_common.py +134 -0
- primarysource/pkgdocs/readers/asciidoc.py +160 -0
- primarysource/pkgdocs/readers/latex.py +315 -0
- primarysource/pkgdocs/readers/markdown.py +173 -0
- primarysource/pkgdocs/readers/pod.py +132 -0
- primarysource/pkgdocs/readers/rst.py +239 -0
- primarysource/pkgdocs/readers/scdoc.py +108 -0
- primarysource/pkgdocs/runtime.py +552 -0
- primarysource/pkgdocs/serving.py +158 -0
- primarysource/pkgdocs/store.py +167 -0
- primarysource/pms/__init__.py +16 -0
- primarysource/pms/catalogue.py +88 -0
- primarysource/pms/cli.py +12 -0
- primarysource/pms/server.py +8 -0
- primarysource/py.typed +0 -0
- primarysource/readers/__init__.py +10 -0
- primarysource/readers/bookmarks.py +192 -0
- primarysource/readers/html.py +1312 -0
- primarysource/readers/markdown.py +189 -0
- primarysource/readers/pdf.py +187 -0
- primarysource/readers/xml.py +230 -0
- primarysource/refs.py +87 -0
- primarysource/registry.py +161 -0
- primarysource/render.py +49 -0
- primarysource/rfc/__init__.py +3 -0
- primarysource/rfc/__main__.py +6 -0
- primarysource/rfc/cite.py +204 -0
- primarysource/rfc/cli.py +903 -0
- primarysource/rfc/collection.py +19 -0
- primarysource/rfc/config.py +138 -0
- primarysource/rfc/errata.py +203 -0
- primarysource/rfc/fsutil.py +82 -0
- primarysource/rfc/graph.py +213 -0
- primarysource/rfc/index/__init__.py +1 -0
- primarysource/rfc/index/chunker.py +281 -0
- primarysource/rfc/index/embed.py +166 -0
- primarysource/rfc/index/hybrid.py +305 -0
- primarysource/rfc/index/lexical.py +223 -0
- primarysource/rfc/index/rfcxml.py +673 -0
- primarysource/rfc/index/search.py +508 -0
- primarysource/rfc/index/txtseg.py +585 -0
- primarysource/rfc/library.py +294 -0
- primarysource/rfc/mcp/__init__.py +1 -0
- primarysource/rfc/mcp/server.py +381 -0
- primarysource/rfc/packs/__init__.py +1 -0
- primarysource/rfc/packs/builder.py +228 -0
- primarysource/rfc/packs/collections/all-current-txt.json +10 -0
- primarysource/rfc/packs/collections/essentials.json +19 -0
- primarysource/rfc/packs/collections/fyi-current.json +10 -0
- primarysource/rfc/packs/collections/standards-current.json +10 -0
- primarysource/rfc/packs/collections/year-2025.json +10 -0
- primarysource/rfc/packs/collections/year-2026.json +10 -0
- primarysource/rfc/packs/collections.py +163 -0
- primarysource/rfc/packs/docsetexport.py +163 -0
- primarysource/rfc/packs/epub.py +159 -0
- primarysource/rfc/packs/export.py +306 -0
- primarysource/rfc/packs/htmlexport.py +82 -0
- primarysource/rfc/packs/serve.py +143 -0
- primarysource/rfc/packs/zimexport.py +171 -0
- primarysource/rfc/query.py +466 -0
- primarysource/rfc/subjects.py +457 -0
- primarysource/rfc/sync/__init__.py +1 -0
- primarysource/rfc/sync/downloader.py +132 -0
- primarysource/rfc/sync/manifest.py +162 -0
- primarysource/rfc/sync/scanner.py +49 -0
- primarysource/rfc/sync/state.py +464 -0
- primarysource/rfc/views.py +1067 -0
- primarysource/rpmdocs/__init__.py +15 -0
- primarysource/rpmdocs/catalogue.py +117 -0
- primarysource/rpmdocs/cli.py +12 -0
- primarysource/rpmdocs/server.py +8 -0
- primarysource/scaffold.py +223 -0
- primarysource/serve.py +75 -0
- primarysource/settings.py +152 -0
- primarysource/usb/__init__.py +5 -0
- primarysource/usb/collection.py +37 -0
- primarysource/usb/data/acquired.json +2025 -0
- primarysource/usb/data/sources-usb.toml +852 -0
- primarysource/usb/data/sources.toml +10 -0
- primarysource/usb/data/usb-catalog.json +1663 -0
- primarysource/usb/data/usb-classification.json +1986 -0
- primarysource/usb/data/usb-corpus-build.json +608 -0
- primarysource/usb/data/usb-pdf-choice.json +839 -0
- primarysource/usb/data/usb-split-measure.json +1803 -0
- primarysource/usb/ingest/__init__.py +1 -0
- primarysource/usb/ingest/acquire.py +130 -0
- primarysource/usb/ingest/discover_usb.py +88 -0
- primarysource/usb/ingest/extract.py +48 -0
- primarysource/usb/ingest/fetch.py +197 -0
- primarysource/usb/ingest/index.py +14 -0
- primarysource/usb/ingest/sources.py +85 -0
- primarysource/usb/models.py +7 -0
- primarysource/usb/paths.py +33 -0
- primarysource/usb/server/__init__.py +3 -0
- primarysource/usb/server/corpus.py +38 -0
- primarysource/usb/server/mcp.py +19 -0
- primarysource/usb/server/tools.py +35 -0
- primarysource/usb/spec.py +20 -0
- primarysource/usbids/__init__.py +56 -0
- primarysource/usbids/cli.py +65 -0
- primarysource/usbids/importer.py +272 -0
- primarysource/usbids/parse.py +283 -0
- primarysource/usbids/server.py +55 -0
- primarysource/usbids/store.py +80 -0
- primarysource/usbids/tools.py +174 -0
- primarysource/zim/__init__.py +5 -0
- primarysource/zim/__main__.py +8 -0
- primarysource/zim/_libzim.py +37 -0
- primarysource/zim/catalog.py +594 -0
- primarysource/zim/cli.py +555 -0
- primarysource/zim/collection.py +41 -0
- primarysource/zim/errors.py +100 -0
- primarysource/zim/groups.py +30 -0
- primarysource/zim/library.py +668 -0
- primarysource/zim/manage.py +636 -0
- primarysource/zim/markdown.py +1247 -0
- primarysource/zim/mcp.py +201 -0
- primarysource/zim/purl.py +130 -0
- primarysource/zim/py.typed +0 -0
- primarysource/zim/refs.py +90 -0
- primarysource/zim/service.py +1182 -0
- primarysource/zim/snapshot.py +197 -0
- primarysource/zim/sync.py +269 -0
- primarysource/zim/views.py +230 -0
- primarysource-0.1.0.dist-info/METADATA +118 -0
- primarysource-0.1.0.dist-info/RECORD +303 -0
- primarysource-0.1.0.dist-info/WHEEL +4 -0
- primarysource-0.1.0.dist-info/entry_points.txt +2 -0
- primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
- primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Import vector sets named in `vectors.toml`: download, check the pinned sha256, parse, store.
|
|
2
|
+
|
|
3
|
+
Every entry pins the sha256 of what it downloads (the zip, or the ACVP `prompt.json`), so a first fetch never
|
|
4
|
+
defines the truth. An entry without a pin is refused, and `primarysource cavp pin <file>` prints the digest to
|
|
5
|
+
write. Nothing here unpacks an archive to disk.
|
|
6
|
+
|
|
7
|
+
[[vector]]
|
|
8
|
+
id = "cavp/ecdsa/SigVer.rsp"
|
|
9
|
+
kind = "cavp"
|
|
10
|
+
algorithm = "ECDSA"
|
|
11
|
+
url = "https://csrc.nist.gov/.../186-4ecdsatestvectors.zip"
|
|
12
|
+
sha256 = "..." # of the zip
|
|
13
|
+
member = "186-4ecdsatestvectors/SigVer.rsp"
|
|
14
|
+
|
|
15
|
+
[[vector]]
|
|
16
|
+
id = "acvp/ECDSA-SigVer-1.0"
|
|
17
|
+
kind = "acvp"
|
|
18
|
+
url = ".../prompt.json" # pin a commit, not a branch
|
|
19
|
+
sha256 = "..."
|
|
20
|
+
expected_url = ".../expectedResults.json"
|
|
21
|
+
expected_sha256 = "..."
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import tomllib
|
|
27
|
+
import urllib.error
|
|
28
|
+
import urllib.request
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import IO, Any
|
|
33
|
+
|
|
34
|
+
from primarysource.cavp.model import MAX_SOURCE_BYTES, IndexEntry, Store, VectorSet, sha256_bytes
|
|
35
|
+
from primarysource.cavp.parse import check_set_id, parse_acvp, parse_rsp, read_member
|
|
36
|
+
from primarysource.errors import LibError
|
|
37
|
+
|
|
38
|
+
PACKAGE_DATA = Path(__file__).resolve().parent / "data"
|
|
39
|
+
USER_AGENT = "primarysource-cavp/0.1 (pinned test-vector fetch)"
|
|
40
|
+
TIMEOUT_SECONDS = 120
|
|
41
|
+
_FIELDS = {"id", "kind", "algorithm", "mode", "url", "sha256", "member", "expected_url", "expected_sha256"}
|
|
42
|
+
|
|
43
|
+
Opener = Callable[[urllib.request.Request, float], IO[bytes]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class Source:
|
|
48
|
+
id: str
|
|
49
|
+
kind: str
|
|
50
|
+
algorithm: str
|
|
51
|
+
url: str
|
|
52
|
+
sha256: str
|
|
53
|
+
member: str = ""
|
|
54
|
+
mode: str = ""
|
|
55
|
+
expected_url: str = ""
|
|
56
|
+
expected_sha256: str = ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_sources(path: Path | None = None) -> tuple[Source, ...]:
|
|
60
|
+
"""The entries of `vectors.toml`: the file given, else the one shipped in the package."""
|
|
61
|
+
file = path if path is not None else PACKAGE_DATA / "vectors.toml"
|
|
62
|
+
try:
|
|
63
|
+
raw: dict[str, Any] = tomllib.loads(file.read_text(encoding="utf-8"))
|
|
64
|
+
except (OSError, ValueError) as exc:
|
|
65
|
+
raise LibError(f"cannot read {file}: {exc}", "") from None
|
|
66
|
+
out: list[Source] = []
|
|
67
|
+
for row in raw.get("vector", []):
|
|
68
|
+
unknown = sorted(set(row) - _FIELDS)
|
|
69
|
+
if unknown:
|
|
70
|
+
raise LibError(f"{file}: unknown field(s) {', '.join(unknown)} in {row.get('id')!r}", "")
|
|
71
|
+
try:
|
|
72
|
+
source = Source(**row)
|
|
73
|
+
except TypeError as exc:
|
|
74
|
+
raise LibError(f"{file}: bad entry {row.get('id')!r} ({exc})", "") from None
|
|
75
|
+
check_set_id(source.id, source.kind)
|
|
76
|
+
if source.kind == "acvp" and bool(source.expected_url) != bool(source.expected_sha256):
|
|
77
|
+
raise LibError(f"{source.id}: expected_url and expected_sha256 go together", "")
|
|
78
|
+
out.append(source)
|
|
79
|
+
ids = [s.id for s in out]
|
|
80
|
+
if len(set(ids)) != len(ids):
|
|
81
|
+
raise LibError(f"{file}: duplicate vector ids", "")
|
|
82
|
+
return tuple(out)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def download(url: str, opener: Opener | None = None) -> bytes:
|
|
86
|
+
"""GET `url`, at most `MAX_SOURCE_BYTES`. https only (a test may pass its own opener)."""
|
|
87
|
+
if opener is None and not url.startswith("https://"):
|
|
88
|
+
raise LibError(f"refusing {url!r}: only https downloads", "")
|
|
89
|
+
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) # noqa: S310
|
|
90
|
+
try:
|
|
91
|
+
if opener is None:
|
|
92
|
+
stream = urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) # noqa: S310
|
|
93
|
+
else:
|
|
94
|
+
stream = opener(request, TIMEOUT_SECONDS)
|
|
95
|
+
with stream:
|
|
96
|
+
body: bytes = stream.read(MAX_SOURCE_BYTES + 1)
|
|
97
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
98
|
+
raise LibError(f"download failed for {url}: {exc}", "check the network and the url") from None
|
|
99
|
+
if len(body) > MAX_SOURCE_BYTES:
|
|
100
|
+
raise LibError(f"{url}: larger than {MAX_SOURCE_BYTES} bytes, refusing", "")
|
|
101
|
+
return body
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_pin(what: str, data: bytes, pinned: str) -> str:
|
|
105
|
+
"""The digest of `data`, or a `LibError` when it is not the pinned one or nothing is pinned."""
|
|
106
|
+
actual = sha256_bytes(data)
|
|
107
|
+
if not pinned:
|
|
108
|
+
raise LibError(f"{what}: no sha256 pinned (this file is {actual})", "add sha256 to its vectors.toml entry")
|
|
109
|
+
if actual != pinned.lower():
|
|
110
|
+
raise LibError(
|
|
111
|
+
f"{what}: sha256 {actual} is not the pinned {pinned}", "the upstream file changed: review, then re-pin"
|
|
112
|
+
)
|
|
113
|
+
return actual
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build(source: Source, artifact: bytes, expected: bytes | None = None) -> VectorSet:
|
|
117
|
+
"""Check pins and parse. `artifact` is the downloaded file (zip or json); `expected` the ACVP answers file."""
|
|
118
|
+
digest = check_pin(source.id, artifact, source.sha256)
|
|
119
|
+
if source.kind == "cavp":
|
|
120
|
+
member = read_member(artifact, source.member) if source.member else artifact
|
|
121
|
+
return parse_rsp(
|
|
122
|
+
member,
|
|
123
|
+
set_id=source.id,
|
|
124
|
+
algorithm=source.algorithm,
|
|
125
|
+
source_url=source.url,
|
|
126
|
+
sha256=digest,
|
|
127
|
+
member_sha256=sha256_bytes(member),
|
|
128
|
+
mode=source.mode,
|
|
129
|
+
)
|
|
130
|
+
if source.expected_url:
|
|
131
|
+
if expected is None:
|
|
132
|
+
raise LibError(f"{source.id}: the expectedResults file is needed", "")
|
|
133
|
+
check_pin(f"{source.id} expectedResults", expected, source.expected_sha256)
|
|
134
|
+
return parse_acvp(
|
|
135
|
+
artifact,
|
|
136
|
+
expected if source.expected_url else None,
|
|
137
|
+
set_id=source.id,
|
|
138
|
+
source_url=source.url,
|
|
139
|
+
sha256=digest,
|
|
140
|
+
member_sha256=digest,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def import_files(store: Store, source: Source, artifact: Path, expected: Path | None = None) -> IndexEntry:
|
|
145
|
+
"""Import from files already on disk (the same pins apply)."""
|
|
146
|
+
size = artifact.stat().st_size
|
|
147
|
+
if size > MAX_SOURCE_BYTES:
|
|
148
|
+
raise LibError(f"{artifact}: {size} bytes is over the {MAX_SOURCE_BYTES} byte cap", "")
|
|
149
|
+
return store.write(build(source, artifact.read_bytes(), expected.read_bytes() if expected else None))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def fetch(store: Store, source: Source, opener: Opener | None = None) -> IndexEntry:
|
|
153
|
+
"""Download `source`, verify its pins, parse and store it."""
|
|
154
|
+
artifact = download(source.url, opener)
|
|
155
|
+
expected = download(source.expected_url, opener) if source.expected_url else None
|
|
156
|
+
return store.write(build(source, artifact, expected))
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Test vectors: the data model, the ref grammar and the on-disk store.
|
|
2
|
+
|
|
3
|
+
A *set* is one vector file: a CAVP `.rsp` response file, or one ACVP vector set (`prompt.json` joined with
|
|
4
|
+
`expectedResults.json`). A set holds *groups* (CAVP `[...]` header lines, ACVP `testGroups`) and a group holds
|
|
5
|
+
*cases*. Refs, with `nist:` optional on input:
|
|
6
|
+
|
|
7
|
+
nist:cavp/ecdsa/SigVer.rsp a set
|
|
8
|
+
nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256] a group
|
|
9
|
+
nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256]/3 a case
|
|
10
|
+
nist:acvp/ECDSA-SigVer-1.0#1/3 ACVP: the group is the tgId, the case the tcId
|
|
11
|
+
|
|
12
|
+
`docs/cavp.md` has the full grammar.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import asdict, dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from primarysource.errors import LibError
|
|
25
|
+
from primarysource.home import atomic_write
|
|
26
|
+
|
|
27
|
+
SCHEMA_VERSION = 1
|
|
28
|
+
REF_PREFIX = "nist:"
|
|
29
|
+
INDEX_FILE = "index.json"
|
|
30
|
+
SETS_DIR = "sets"
|
|
31
|
+
|
|
32
|
+
# Bounds. A source over these is refused, never read.
|
|
33
|
+
MAX_SOURCE_BYTES = 64 * 1024 * 1024
|
|
34
|
+
MAX_LINE_BYTES = 1024 * 1024
|
|
35
|
+
MAX_GROUPS = 100_000
|
|
36
|
+
MAX_CASES = 2_000_000
|
|
37
|
+
MAX_FIELDS_PER_CASE = 64
|
|
38
|
+
MAX_HEADER_LINES = 100
|
|
39
|
+
|
|
40
|
+
SET_ID = re.compile(r"(cavp|acvp)(/[A-Za-z0-9][A-Za-z0-9._+-]*)+")
|
|
41
|
+
KINDS = ("cavp", "acvp")
|
|
42
|
+
|
|
43
|
+
CREDIT = (
|
|
44
|
+
"Source: NIST Cryptographic Algorithm Validation Program (CAVP/ACVP). Created by employees of the National "
|
|
45
|
+
"Institute of Standards and Technology (NIST), not subject to copyright in the United States (17 U.S.C. 105); "
|
|
46
|
+
"provided AS IS. https://www.nist.gov/open/copyright-fair-use-and-licensing-statements-srd-data-software-"
|
|
47
|
+
"and-technical-series-publications"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class Case:
|
|
53
|
+
"""`id` is COUNT or tcId when the source has one, else the 1-based position in the group. `fields` are the
|
|
54
|
+
inputs (and, in CAVP, the expected result as well); `expected` is the joined ACVP expectedResults."""
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
fields: dict[str, Any]
|
|
58
|
+
expected: dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class Group:
|
|
63
|
+
label: str
|
|
64
|
+
params: dict[str, Any]
|
|
65
|
+
cases: tuple[Case, ...]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class VectorSet:
|
|
70
|
+
id: str
|
|
71
|
+
kind: str
|
|
72
|
+
algorithm: str
|
|
73
|
+
mode: str
|
|
74
|
+
source_url: str
|
|
75
|
+
sha256: str # of the downloaded artifact (the zip, or prompt.json)
|
|
76
|
+
member_sha256: str # of the file inside the zip; equal to sha256 when the artifact is the file
|
|
77
|
+
header: tuple[str, ...]
|
|
78
|
+
groups: tuple[Group, ...]
|
|
79
|
+
revision: str = ""
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def case_count(self) -> int:
|
|
83
|
+
return sum(len(g.cases) for g in self.groups)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class VectorRef:
|
|
88
|
+
set_id: str
|
|
89
|
+
group: str | None = None
|
|
90
|
+
case: str | None = None
|
|
91
|
+
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
return format_ref(self.set_id, self.group, self.case)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def sha256_bytes(data: bytes) -> str:
|
|
97
|
+
return hashlib.sha256(data).hexdigest()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def format_ref(set_id: str, group: str | None = None, case: str | None = None) -> str:
|
|
101
|
+
out = REF_PREFIX + set_id
|
|
102
|
+
if group is not None:
|
|
103
|
+
out += "#" + (f"[{group}]" if set_id.startswith("cavp/") else group)
|
|
104
|
+
if case is not None:
|
|
105
|
+
out += "/" + case
|
|
106
|
+
elif case is not None:
|
|
107
|
+
raise LibError("a case ref needs its group", "nist:<set>#<group>/<case>")
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_ref(text: str) -> VectorRef:
|
|
112
|
+
"""`nist:<set>[#<group>[/<case>]]`. The case is what follows the last `/`, so a group label may hold one."""
|
|
113
|
+
shown = text.strip().removeprefix(REF_PREFIX)
|
|
114
|
+
set_id, sep, rest = shown.partition("#")
|
|
115
|
+
hint = "e.g. nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256]/3 or nist:acvp/ECDSA-SigVer-1.0#1/3"
|
|
116
|
+
if not SET_ID.fullmatch(set_id):
|
|
117
|
+
raise LibError(f"not a vector ref: {text!r}", hint)
|
|
118
|
+
if not sep:
|
|
119
|
+
return VectorRef(set_id)
|
|
120
|
+
group, slash, case = rest.rpartition("/")
|
|
121
|
+
if not slash:
|
|
122
|
+
group, case = rest, ""
|
|
123
|
+
if not group:
|
|
124
|
+
raise LibError(f"not a vector ref: {text!r} (empty group)", hint)
|
|
125
|
+
if set_id.startswith("cavp/"):
|
|
126
|
+
if not (group.startswith("[") and group.endswith("]") and len(group) > 2):
|
|
127
|
+
raise LibError(f"not a vector ref: {text!r} (a CAVP group is written in brackets)", hint)
|
|
128
|
+
group = group[1:-1]
|
|
129
|
+
return VectorRef(set_id, group, case or None)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---- serialisation -----------------------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def set_to_json(s: VectorSet) -> dict[str, Any]:
|
|
136
|
+
return {
|
|
137
|
+
"schema": SCHEMA_VERSION,
|
|
138
|
+
"id": s.id,
|
|
139
|
+
"kind": s.kind,
|
|
140
|
+
"algorithm": s.algorithm,
|
|
141
|
+
"mode": s.mode,
|
|
142
|
+
"revision": s.revision,
|
|
143
|
+
"source_url": s.source_url,
|
|
144
|
+
"sha256": s.sha256,
|
|
145
|
+
"member_sha256": s.member_sha256,
|
|
146
|
+
"header": list(s.header),
|
|
147
|
+
"groups": [
|
|
148
|
+
{
|
|
149
|
+
"label": g.label,
|
|
150
|
+
"params": g.params,
|
|
151
|
+
"cases": [{"id": c.id, "fields": c.fields, "expected": c.expected} for c in g.cases],
|
|
152
|
+
}
|
|
153
|
+
for g in s.groups
|
|
154
|
+
],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _text(where: str, value: object) -> str:
|
|
159
|
+
if not isinstance(value, str):
|
|
160
|
+
raise LibError(f"{where}: expected a string", "", "primarysource cavp fetch")
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def set_from_json(raw: object, where: str) -> VectorSet:
|
|
165
|
+
if not isinstance(raw, dict) or raw.get("schema") != SCHEMA_VERSION:
|
|
166
|
+
raise LibError(f"{where}: not a schema {SCHEMA_VERSION} vector set", "", "primarysource cavp fetch")
|
|
167
|
+
groups: list[Group] = []
|
|
168
|
+
for g in raw.get("groups", []):
|
|
169
|
+
cases = tuple(
|
|
170
|
+
Case(_text("id", c["id"]), dict(c["fields"]), dict(c.get("expected") or {})) for c in g["cases"]
|
|
171
|
+
)
|
|
172
|
+
groups.append(Group(_text("label", g["label"]), dict(g.get("params") or {}), cases))
|
|
173
|
+
return VectorSet(
|
|
174
|
+
id=_text("id", raw.get("id")),
|
|
175
|
+
kind=_text("kind", raw.get("kind")),
|
|
176
|
+
algorithm=_text("algorithm", raw.get("algorithm")),
|
|
177
|
+
mode=_text("mode", raw.get("mode")),
|
|
178
|
+
source_url=_text("source_url", raw.get("source_url")),
|
|
179
|
+
sha256=_text("sha256", raw.get("sha256")),
|
|
180
|
+
member_sha256=_text("member_sha256", raw.get("member_sha256")),
|
|
181
|
+
header=tuple(_text("header", h) for h in raw.get("header", [])),
|
|
182
|
+
groups=tuple(groups),
|
|
183
|
+
revision=_text("revision", raw.get("revision", "")),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---- the store -----------------------------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True, slots=True)
|
|
191
|
+
class IndexEntry:
|
|
192
|
+
id: str
|
|
193
|
+
kind: str
|
|
194
|
+
algorithm: str
|
|
195
|
+
mode: str
|
|
196
|
+
revision: str
|
|
197
|
+
source_url: str
|
|
198
|
+
sha256: str
|
|
199
|
+
groups: int
|
|
200
|
+
cases: int
|
|
201
|
+
file: str
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class Store:
|
|
205
|
+
"""A directory of imported sets: `index.json` plus `sets/<id with / as __>.json`."""
|
|
206
|
+
|
|
207
|
+
def __init__(self, root: Path) -> None:
|
|
208
|
+
self.root = root
|
|
209
|
+
|
|
210
|
+
def entries(self) -> list[IndexEntry]:
|
|
211
|
+
path = self.root / INDEX_FILE
|
|
212
|
+
if not path.is_file():
|
|
213
|
+
return []
|
|
214
|
+
try:
|
|
215
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
216
|
+
if data["schema"] != SCHEMA_VERSION:
|
|
217
|
+
raise KeyError("schema")
|
|
218
|
+
return [IndexEntry(**e) for e in data["sets"]]
|
|
219
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
220
|
+
raise LibError(
|
|
221
|
+
f"{path} is not a schema {SCHEMA_VERSION} index ({exc})", "", "primarysource cavp fetch"
|
|
222
|
+
) from None
|
|
223
|
+
|
|
224
|
+
def require(self) -> list[IndexEntry]:
|
|
225
|
+
"""The entries, or an error that names the import command when there are none."""
|
|
226
|
+
found = self.entries()
|
|
227
|
+
if not found:
|
|
228
|
+
raise LibError(f"no vector sets imported under {self.root}", "", "primarysource cavp fetch")
|
|
229
|
+
return found
|
|
230
|
+
|
|
231
|
+
def load(self, set_id: str) -> VectorSet:
|
|
232
|
+
found = self.require()
|
|
233
|
+
entry = next((e for e in found if e.id == set_id), None)
|
|
234
|
+
if entry is None:
|
|
235
|
+
names = ", ".join(e.id for e in found)
|
|
236
|
+
raise LibError(f"no vector set {set_id!r} (imported: {names})", "cavp_sets lists what is imported")
|
|
237
|
+
path = (self.root / entry.file).resolve()
|
|
238
|
+
if not path.is_relative_to(self.root.resolve()):
|
|
239
|
+
raise LibError(f"{set_id}: {entry.file!r} escapes the store", "", "primarysource cavp fetch")
|
|
240
|
+
try:
|
|
241
|
+
return set_from_json(json.loads(path.read_text(encoding="utf-8")), set_id)
|
|
242
|
+
except FileNotFoundError:
|
|
243
|
+
raise LibError(f"{set_id}: {entry.file} is missing", "", "primarysource cavp fetch") from None
|
|
244
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
245
|
+
raise LibError(f"{path} is damaged ({exc})", "", "primarysource cavp fetch") from None
|
|
246
|
+
|
|
247
|
+
def write(self, s: VectorSet) -> IndexEntry:
|
|
248
|
+
rel = f"{SETS_DIR}/{s.id.replace('/', '__')}.json"
|
|
249
|
+
body = json.dumps(set_to_json(s), ensure_ascii=False, separators=(",", ":"))
|
|
250
|
+
atomic_write(self.root / rel, body.encode("utf-8"))
|
|
251
|
+
entry = IndexEntry(
|
|
252
|
+
s.id, s.kind, s.algorithm, s.mode, s.revision, s.source_url, s.sha256, len(s.groups), s.case_count, rel
|
|
253
|
+
)
|
|
254
|
+
entries = sorted([e for e in self.entries() if e.id != s.id] + [entry], key=lambda e: e.id)
|
|
255
|
+
index = {"schema": SCHEMA_VERSION, "sets": [asdict(e) for e in entries]}
|
|
256
|
+
atomic_write(self.root / INDEX_FILE, (json.dumps(index, indent=2) + "\n").encode("utf-8"))
|
|
257
|
+
return entry
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def norm_value(value: object) -> str:
|
|
261
|
+
"""Compare as hex is compared: case-insensitive, without spaces or a `0x` prefix. CAVP writes lower case,
|
|
262
|
+
ACVP upper case."""
|
|
263
|
+
s = str(value).strip().lower().replace(" ", "")
|
|
264
|
+
return s[2:] if s.startswith("0x") else s
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def find_group(s: VectorSet, label: str) -> Group:
|
|
268
|
+
for g in s.groups:
|
|
269
|
+
if g.label == label:
|
|
270
|
+
return g
|
|
271
|
+
shown = ", ".join(g.label for g in s.groups[:10]) + (", ..." if len(s.groups) > 10 else "")
|
|
272
|
+
raise LibError(f"{s.id} has no group {label!r} (groups: {shown})", f"cavp_groups('nist:{s.id}') lists them")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def find_case(g: Group, case_id: str) -> Case:
|
|
276
|
+
for c in g.cases:
|
|
277
|
+
if c.id == case_id:
|
|
278
|
+
return c
|
|
279
|
+
raise LibError(f"group {g.label!r} has no case {case_id!r} (it has {len(g.cases)} cases)", "")
|