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.
Files changed (80) hide show
  1. pysci/__init__.py +77 -0
  2. pysci/__main__.py +8 -0
  3. pysci/_cli/__init__.py +5 -0
  4. pysci/_cli/checks.py +173 -0
  5. pysci/_cli/clients/__init__.py +0 -0
  6. pysci/_cli/clients/archive.py +456 -0
  7. pysci/_cli/clients/credentials.py +54 -0
  8. pysci/_cli/clients/github.py +734 -0
  9. pysci/_cli/clients/hosting.py +238 -0
  10. pysci/_cli/clients/http.py +481 -0
  11. pysci/_cli/clients/huggingface.py +492 -0
  12. pysci/_cli/clients/index.py +435 -0
  13. pysci/_cli/clients/tsa.py +223 -0
  14. pysci/_cli/commands/__init__.py +44 -0
  15. pysci/_cli/commands/check.py +115 -0
  16. pysci/_cli/commands/claim.py +179 -0
  17. pysci/_cli/commands/common.py +91 -0
  18. pysci/_cli/commands/folders.py +83 -0
  19. pysci/_cli/commands/index.py +51 -0
  20. pysci/_cli/commands/keygen.py +58 -0
  21. pysci/_cli/commands/license.py +75 -0
  22. pysci/_cli/commands/protocol.py +72 -0
  23. pysci/_cli/commands/publish.py +282 -0
  24. pysci/_cli/commands/pull.py +337 -0
  25. pysci/_cli/commands/read.py +364 -0
  26. pysci/_cli/commands/revoke.py +207 -0
  27. pysci/_cli/commands/search.py +400 -0
  28. pysci/_cli/commands/sign.py +284 -0
  29. pysci/_cli/commands/submit.py +172 -0
  30. pysci/_cli/commands/timestamp.py +196 -0
  31. pysci/_cli/commands/verify.py +163 -0
  32. pysci/_cli/config.py +233 -0
  33. pysci/_cli/context.py +79 -0
  34. pysci/_cli/destinations.py +163 -0
  35. pysci/_cli/errors.py +168 -0
  36. pysci/_cli/main.py +229 -0
  37. pysci/_cli/output.py +80 -0
  38. pysci/_cli/retrieval.py +51 -0
  39. pysci/_cli/spec.py +113 -0
  40. pysci/_cli/storage/__init__.py +0 -0
  41. pysci/_cli/storage/catalog.py +371 -0
  42. pysci/_cli/storage/files.py +252 -0
  43. pysci/_cli/storage/inputs.py +205 -0
  44. pysci/_cli/storage/keys.py +132 -0
  45. pysci/_cli/storage/local_publish.py +88 -0
  46. pysci/accounts.py +135 -0
  47. pysci/baseline.py +142 -0
  48. pysci/bundle.py +878 -0
  49. pysci/citations.py +233 -0
  50. pysci/data/freetsa-cacert.pem +45 -0
  51. pysci/data/sci-baseline-LICENSE.txt +457 -0
  52. pysci/data/sci-protocol.md +613 -0
  53. pysci/der.py +146 -0
  54. pysci/discovery.py +461 -0
  55. pysci/draft.py +374 -0
  56. pysci/ed25519.py +153 -0
  57. pysci/errors.py +204 -0
  58. pysci/ids.py +61 -0
  59. pysci/jsonstrict.py +225 -0
  60. pysci/keys.py +247 -0
  61. pysci/manifest.py +302 -0
  62. pysci/metadata.py +406 -0
  63. pysci/paths.py +136 -0
  64. pysci/png.py +217 -0
  65. pysci/py.typed +0 -0
  66. pysci/records.py +384 -0
  67. pysci/rfc3339.py +66 -0
  68. pysci/schemas.py +75 -0
  69. pysci/source.py +166 -0
  70. pysci/specification.py +58 -0
  71. pysci/submission.py +225 -0
  72. pysci/templates.py +129 -0
  73. pysci/text.py +226 -0
  74. pysci/timestamps.py +867 -0
  75. pysci/zipformat.py +400 -0
  76. sci_protocol-0.2.0.dist-info/METADATA +112 -0
  77. sci_protocol-0.2.0.dist-info/RECORD +80 -0
  78. sci_protocol-0.2.0.dist-info/WHEEL +4 -0
  79. sci_protocol-0.2.0.dist-info/entry_points.txt +2 -0
  80. 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
@@ -0,0 +1,8 @@
1
+ """Run the SCI command as ``python -m pysci``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._cli.main import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
pysci/_cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Private implementation of the ``sci`` command; use :mod:`pysci` for the public API."""
2
+
3
+ from pysci import __version__
4
+
5
+ __all__ = ["__version__"]
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