sci-protocol 0.2.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.
- pysci/__init__.py +77 -0
- pysci/__main__.py +8 -0
- pysci/_cli/__init__.py +5 -0
- pysci/_cli/checks.py +173 -0
- pysci/_cli/clients/__init__.py +0 -0
- pysci/_cli/clients/archive.py +456 -0
- pysci/_cli/clients/credentials.py +54 -0
- pysci/_cli/clients/github.py +734 -0
- pysci/_cli/clients/hosting.py +238 -0
- pysci/_cli/clients/http.py +481 -0
- pysci/_cli/clients/huggingface.py +492 -0
- pysci/_cli/clients/index.py +435 -0
- pysci/_cli/clients/tsa.py +223 -0
- pysci/_cli/commands/__init__.py +44 -0
- pysci/_cli/commands/check.py +115 -0
- pysci/_cli/commands/claim.py +179 -0
- pysci/_cli/commands/common.py +91 -0
- pysci/_cli/commands/folders.py +83 -0
- pysci/_cli/commands/index.py +51 -0
- pysci/_cli/commands/keygen.py +58 -0
- pysci/_cli/commands/license.py +75 -0
- pysci/_cli/commands/protocol.py +72 -0
- pysci/_cli/commands/publish.py +282 -0
- pysci/_cli/commands/pull.py +337 -0
- pysci/_cli/commands/read.py +364 -0
- pysci/_cli/commands/revoke.py +207 -0
- pysci/_cli/commands/search.py +400 -0
- pysci/_cli/commands/sign.py +284 -0
- pysci/_cli/commands/submit.py +172 -0
- pysci/_cli/commands/timestamp.py +196 -0
- pysci/_cli/commands/verify.py +163 -0
- pysci/_cli/config.py +233 -0
- pysci/_cli/context.py +79 -0
- pysci/_cli/destinations.py +163 -0
- pysci/_cli/errors.py +168 -0
- pysci/_cli/main.py +229 -0
- pysci/_cli/output.py +80 -0
- pysci/_cli/retrieval.py +51 -0
- pysci/_cli/spec.py +113 -0
- pysci/_cli/storage/__init__.py +0 -0
- pysci/_cli/storage/catalog.py +371 -0
- pysci/_cli/storage/files.py +252 -0
- pysci/_cli/storage/inputs.py +205 -0
- pysci/_cli/storage/keys.py +132 -0
- pysci/_cli/storage/local_publish.py +88 -0
- pysci/accounts.py +135 -0
- pysci/baseline.py +142 -0
- pysci/bundle.py +878 -0
- pysci/citations.py +233 -0
- pysci/data/freetsa-cacert.pem +45 -0
- pysci/data/sci-baseline-LICENSE.txt +457 -0
- pysci/data/sci-protocol.md +613 -0
- pysci/der.py +146 -0
- pysci/discovery.py +461 -0
- pysci/draft.py +374 -0
- pysci/ed25519.py +153 -0
- pysci/errors.py +204 -0
- pysci/ids.py +61 -0
- pysci/jsonstrict.py +225 -0
- pysci/keys.py +247 -0
- pysci/manifest.py +302 -0
- pysci/metadata.py +406 -0
- pysci/paths.py +136 -0
- pysci/png.py +217 -0
- pysci/py.typed +0 -0
- pysci/records.py +384 -0
- pysci/rfc3339.py +66 -0
- pysci/schemas.py +75 -0
- pysci/source.py +166 -0
- pysci/specification.py +58 -0
- pysci/submission.py +225 -0
- pysci/templates.py +129 -0
- pysci/text.py +226 -0
- pysci/timestamps.py +867 -0
- pysci/zipformat.py +400 -0
- sci_protocol-0.2.0.dist-info/METADATA +112 -0
- sci_protocol-0.2.0.dist-info/RECORD +80 -0
- sci_protocol-0.2.0.dist-info/WHEEL +4 -0
- sci_protocol-0.2.0.dist-info/entry_points.txt +2 -0
- sci_protocol-0.2.0.dist-info/licenses/LICENSE +202 -0
pysci/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""pysci: a Python implementation of the SCI protocol (draft 0.1).
|
|
2
|
+
|
|
3
|
+
The most common entry points are re-exported here; see the submodules for
|
|
4
|
+
the full API:
|
|
5
|
+
|
|
6
|
+
* :mod:`pysci.bundle` -- verify publications (ZIP or loose files), write ZIPs;
|
|
7
|
+
* :mod:`pysci.draft` -- check and sign drafts;
|
|
8
|
+
* :mod:`pysci.records` -- key claims and revocations;
|
|
9
|
+
* :mod:`pysci.timestamps` -- RFC 3161 requests and proof verification;
|
|
10
|
+
* :mod:`pysci.discovery`, :mod:`pysci.submission`, :mod:`pysci.templates` -- §8.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .baseline import BASELINE_LICENSE_SHA256, baseline_license_bytes, evaluate_baseline
|
|
16
|
+
from .bundle import (
|
|
17
|
+
Scope,
|
|
18
|
+
VerificationLimits,
|
|
19
|
+
VerificationResult,
|
|
20
|
+
verify_directory,
|
|
21
|
+
verify_file,
|
|
22
|
+
verify_loose,
|
|
23
|
+
verify_path,
|
|
24
|
+
verify_zip,
|
|
25
|
+
write_bundle_zip,
|
|
26
|
+
)
|
|
27
|
+
from .draft import BaselineRefused, DraftReport, SignedPublication, check_draft, sign_draft
|
|
28
|
+
from .errors import Incomplete, Invalid, Issue, Outcome, SciError, Unsupported
|
|
29
|
+
from .ids import parse_publication_id, publication_id_from_manifest
|
|
30
|
+
from .keys import PublicKey, generate_private_key, load_private_key_pem, public_key_of
|
|
31
|
+
from .manifest import Manifest, build_manifest, parse_manifest, verify_manifest
|
|
32
|
+
from .records import create_claim, create_revocation, verify_claim, verify_revocation
|
|
33
|
+
from .schemas import PROTOCOL_VERSION
|
|
34
|
+
|
|
35
|
+
__version__ = "0.2.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"BASELINE_LICENSE_SHA256",
|
|
39
|
+
"PROTOCOL_VERSION",
|
|
40
|
+
"BaselineRefused",
|
|
41
|
+
"DraftReport",
|
|
42
|
+
"Incomplete",
|
|
43
|
+
"Invalid",
|
|
44
|
+
"Issue",
|
|
45
|
+
"Manifest",
|
|
46
|
+
"Outcome",
|
|
47
|
+
"PublicKey",
|
|
48
|
+
"SciError",
|
|
49
|
+
"Scope",
|
|
50
|
+
"SignedPublication",
|
|
51
|
+
"Unsupported",
|
|
52
|
+
"VerificationLimits",
|
|
53
|
+
"VerificationResult",
|
|
54
|
+
"__version__",
|
|
55
|
+
"baseline_license_bytes",
|
|
56
|
+
"build_manifest",
|
|
57
|
+
"check_draft",
|
|
58
|
+
"create_claim",
|
|
59
|
+
"create_revocation",
|
|
60
|
+
"evaluate_baseline",
|
|
61
|
+
"generate_private_key",
|
|
62
|
+
"load_private_key_pem",
|
|
63
|
+
"parse_manifest",
|
|
64
|
+
"parse_publication_id",
|
|
65
|
+
"public_key_of",
|
|
66
|
+
"publication_id_from_manifest",
|
|
67
|
+
"sign_draft",
|
|
68
|
+
"verify_claim",
|
|
69
|
+
"verify_directory",
|
|
70
|
+
"verify_file",
|
|
71
|
+
"verify_loose",
|
|
72
|
+
"verify_manifest",
|
|
73
|
+
"verify_path",
|
|
74
|
+
"verify_revocation",
|
|
75
|
+
"verify_zip",
|
|
76
|
+
"write_bundle_zip",
|
|
77
|
+
]
|
pysci/__main__.py
ADDED
pysci/_cli/__init__.py
ADDED
pysci/_cli/checks.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Consistent check reports: baseline, bundle, timestamps, associations, and revocation.
|
|
2
|
+
|
|
3
|
+
Every verification-style result carries a ``checks`` object with these five
|
|
4
|
+
keys, so that agents can tell failed checks from checks that were not
|
|
5
|
+
performed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pysci.bundle import VerificationResult
|
|
14
|
+
from pysci.timestamps import ProofResult, ProofVerification, TimestampFileVerification
|
|
15
|
+
|
|
16
|
+
from .errors import issue_json
|
|
17
|
+
|
|
18
|
+
CHECK_NAMES = ("bundle", "baseline", "timestamps", "associations", "revocation")
|
|
19
|
+
|
|
20
|
+
OFFLINE_ASSOCIATIONS = (
|
|
21
|
+
"account associations require retrieving hosted claims (§6.6); not checked by this command"
|
|
22
|
+
)
|
|
23
|
+
OFFLINE_REVOCATION = (
|
|
24
|
+
"a missing revocation record does not prove a key was never revoked (§5.3); "
|
|
25
|
+
"not checked by this command"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def not_performed(reason: str) -> dict[str, Any]:
|
|
30
|
+
return {"status": "not_performed", "reason": reason}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def bundle_check(result: VerificationResult, *, scope_label: str | None = None) -> dict[str, Any]:
|
|
34
|
+
out: dict[str, Any] = {
|
|
35
|
+
"status": str(result.outcome),
|
|
36
|
+
"scope": scope_label or str(result.scope),
|
|
37
|
+
"publication_id": result.publication_id,
|
|
38
|
+
"issues": [issue_json(issue) for issue in result.issues],
|
|
39
|
+
}
|
|
40
|
+
if result.zip_bytes is not None:
|
|
41
|
+
out["zip_bytes"] = result.zip_bytes
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def baseline_check(result: VerificationResult) -> dict[str, Any]:
|
|
46
|
+
if result.baseline is None or result.baseline_report is None:
|
|
47
|
+
return not_performed("baseline is evaluated only after complete bundle verification")
|
|
48
|
+
return {
|
|
49
|
+
"status": "eligible" if result.baseline else "not_eligible",
|
|
50
|
+
"reasons": [issue_json(issue) for issue in result.baseline_report.reasons],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def proof_json(proof: ProofVerification) -> dict[str, Any]:
|
|
55
|
+
out: dict[str, Any] = {
|
|
56
|
+
"offset": proof.offset,
|
|
57
|
+
"length": proof.length,
|
|
58
|
+
"result": str(proof.result),
|
|
59
|
+
"issues": [issue_json(issue) for issue in proof.issues],
|
|
60
|
+
}
|
|
61
|
+
if proof.gen_time is not None:
|
|
62
|
+
out["gen_time"] = proof.gen_time.isoformat().replace("+00:00", "Z")
|
|
63
|
+
if proof.accuracy is not None:
|
|
64
|
+
out["accuracy_seconds"] = str(proof.accuracy.total_seconds())
|
|
65
|
+
if proof.tsa_name is not None:
|
|
66
|
+
out["tsa"] = proof.tsa_name
|
|
67
|
+
if proof.signer_subject is not None:
|
|
68
|
+
out["signer"] = proof.signer_subject
|
|
69
|
+
if proof.result is ProofResult.VERIFIED:
|
|
70
|
+
out["certificate_revocation_checked"] = proof.revocation_checked
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def timestamps_check(
|
|
75
|
+
verification: TimestampFileVerification | None,
|
|
76
|
+
*,
|
|
77
|
+
source: str | None,
|
|
78
|
+
online: bool,
|
|
79
|
+
absent_reason: str = "no timestamp sidecar found",
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
if verification is None:
|
|
82
|
+
return {"status": "absent", "reason": absent_reason, "source": source}
|
|
83
|
+
out: dict[str, Any] = {
|
|
84
|
+
"status": str(verification.outcome),
|
|
85
|
+
"source": source,
|
|
86
|
+
"framing": verification.framing,
|
|
87
|
+
"proofs": [proof_json(p) for p in verification.proofs],
|
|
88
|
+
"trust_checks": "online (current CRLs required)" if online else "offline",
|
|
89
|
+
}
|
|
90
|
+
if not online and any(p.result is ProofResult.VERIFIED for p in verification.proofs):
|
|
91
|
+
out["note"] = (
|
|
92
|
+
"certificate revocation status was checked only against locally configured CRLs; "
|
|
93
|
+
"use --check-timestamp-online for current CRLs"
|
|
94
|
+
)
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def checks(
|
|
99
|
+
*,
|
|
100
|
+
bundle: dict[str, Any] | None = None,
|
|
101
|
+
baseline: dict[str, Any] | None = None,
|
|
102
|
+
timestamps: dict[str, Any] | None = None,
|
|
103
|
+
associations: dict[str, Any] | None = None,
|
|
104
|
+
revocation: dict[str, Any] | None = None,
|
|
105
|
+
reasons: dict[str, str] | None = None,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
"""Assemble the five-part check report, marking missing parts as not performed."""
|
|
108
|
+
given = {
|
|
109
|
+
"bundle": bundle,
|
|
110
|
+
"baseline": baseline,
|
|
111
|
+
"timestamps": timestamps,
|
|
112
|
+
"associations": associations,
|
|
113
|
+
"revocation": revocation,
|
|
114
|
+
}
|
|
115
|
+
defaults = {
|
|
116
|
+
"bundle": "complete bundle checks were not performed",
|
|
117
|
+
"baseline": "baseline eligibility was not evaluated",
|
|
118
|
+
"timestamps": "timestamp proofs were not checked",
|
|
119
|
+
"associations": OFFLINE_ASSOCIATIONS,
|
|
120
|
+
"revocation": OFFLINE_REVOCATION,
|
|
121
|
+
}
|
|
122
|
+
defaults.update(reasons or {})
|
|
123
|
+
out: dict[str, Any] = {}
|
|
124
|
+
for name in CHECK_NAMES:
|
|
125
|
+
value = given[name]
|
|
126
|
+
out[name] = value if value is not None else not_performed(defaults[name])
|
|
127
|
+
out["not_performed"] = [n for n in CHECK_NAMES if out[n].get("status") == "not_performed"]
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def describe_checks(report: dict[str, Any]) -> list[str]:
|
|
132
|
+
"""Human lines for a check report."""
|
|
133
|
+
lines: list[str] = []
|
|
134
|
+
for name in CHECK_NAMES:
|
|
135
|
+
entry = report[name]
|
|
136
|
+
status = entry.get("status")
|
|
137
|
+
detail = entry.get("reason") or ""
|
|
138
|
+
if name == "timestamps" and entry.get("proofs"):
|
|
139
|
+
verified = [p for p in entry["proofs"] if p["result"] == "verified"]
|
|
140
|
+
detail = f"{len(verified)} of {len(entry['proofs'])} proof(s) verified"
|
|
141
|
+
times = ", ".join(p["gen_time"] for p in verified if "gen_time" in p)
|
|
142
|
+
if times:
|
|
143
|
+
detail += f"; genTime {times}"
|
|
144
|
+
others = [p for p in entry["proofs"] if p["result"] != "verified"]
|
|
145
|
+
for proof in others:
|
|
146
|
+
first = proof["issues"][0]["message"] if proof["issues"] else ""
|
|
147
|
+
detail += f"; proof at {proof['offset']}: {proof['result']} {first}".rstrip()
|
|
148
|
+
elif name == "baseline" and entry.get("reasons"):
|
|
149
|
+
detail = "; ".join(
|
|
150
|
+
f"{r.get('message')} (observed {r.get('observed')})" for r in entry["reasons"]
|
|
151
|
+
)
|
|
152
|
+
elif name == "bundle" and entry.get("issues"):
|
|
153
|
+
detail = f"{len(entry['issues'])} issue(s)"
|
|
154
|
+
lines.append(f" {name:<13}{status}" + (f" - {detail}" if detail else ""))
|
|
155
|
+
return lines
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def issue_lines(issues: Sequence[dict[str, Any]], prefix: str = " ") -> list[str]:
|
|
159
|
+
lines = []
|
|
160
|
+
for issue in issues:
|
|
161
|
+
where = issue.get("file") or ""
|
|
162
|
+
if issue.get("field"):
|
|
163
|
+
where += ("#" if where else "") + str(issue["field"])
|
|
164
|
+
text = f"{prefix}[{issue['code']}] "
|
|
165
|
+
if where:
|
|
166
|
+
text += where + ": "
|
|
167
|
+
text += str(issue.get("requirement") or issue.get("message"))
|
|
168
|
+
if issue.get("section"):
|
|
169
|
+
text += f" (§{issue['section']})"
|
|
170
|
+
if issue.get("observed") is not None:
|
|
171
|
+
text += f"; observed {issue['observed']}"
|
|
172
|
+
lines.append(text)
|
|
173
|
+
return lines
|
|
File without changes
|