auths-python 0.1.0__cp38-abi3-win_amd64.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.
auths/_errors.py ADDED
@@ -0,0 +1,80 @@
1
+ """Auths error hierarchy. All errors inherit from AuthsError."""
2
+
3
+
4
+ class AuthsError(Exception):
5
+ """Base error for all Auths SDK operations."""
6
+
7
+ def __init__(self, message: str, code: str, **context):
8
+ self.message = message
9
+ self.code = code
10
+ self.context = context
11
+ super().__init__(message)
12
+
13
+ def __repr__(self):
14
+ return f"{type(self).__name__}(code={self.code!r}, message={self.message!r})"
15
+
16
+
17
+ class VerificationError(AuthsError):
18
+ """Attestation or chain verification failed.
19
+
20
+ Codes: invalid_signature, expired_attestation, revoked_device,
21
+ broken_chain, missing_attestation, unknown_signer.
22
+ """
23
+
24
+
25
+ class CryptoError(AuthsError):
26
+ """Cryptographic operation failed (bad key, signing error).
27
+
28
+ Codes: invalid_key, signing_failed, key_not_found.
29
+ """
30
+
31
+
32
+ class KeychainError(AuthsError):
33
+ """Keychain access failed.
34
+
35
+ Codes: keychain_locked, key_not_found, permission_denied.
36
+ """
37
+
38
+
39
+ class StorageError(AuthsError):
40
+ """Git storage operation failed.
41
+
42
+ Codes: repo_not_found, ref_conflict, corrupt_data, duplicate_attestation.
43
+ """
44
+
45
+
46
+ class NetworkError(AuthsError):
47
+ """Network operation failed (token exchange, registry sync).
48
+
49
+ Codes: connection_failed, timeout, server_error, auth_failed.
50
+ """
51
+
52
+ def __init__(self, message: str, code: str, should_retry: bool = False, **context):
53
+ self.should_retry = should_retry
54
+ super().__init__(message, code, **context)
55
+
56
+
57
+ class IdentityError(AuthsError):
58
+ """Identity lifecycle operation failed.
59
+
60
+ Codes: identity_exists, identity_not_found, invalid_did.
61
+ """
62
+
63
+
64
+ class OrgError(AuthsError):
65
+ """Organization operation failed.
66
+
67
+ Codes: org_error, admin_not_found, member_not_found,
68
+ already_revoked, invalid_capability, invalid_role.
69
+ """
70
+
71
+
72
+ class PairingError(AuthsError):
73
+ """Device pairing operation failed.
74
+
75
+ Codes: pairing_error, timeout, connection_failed, session_expired.
76
+ """
77
+
78
+ def __init__(self, message: str, code: str, should_retry: bool = False, **context):
79
+ self.should_retry = should_retry
80
+ super().__init__(message, code, **context)
auths/_native.pyd ADDED
Binary file
auths/agent.py ADDED
@@ -0,0 +1,55 @@
1
+ """Auths agent authentication for MCP tool access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from auths._native import get_token as _native_get_token
9
+
10
+
11
+ class AgentAuth:
12
+ """Auths agent authentication for MCP tool servers.
13
+
14
+ Exchanges a KERI attestation chain for a scoped JWT via the OIDC bridge.
15
+
16
+ Args:
17
+ bridge_url: The OIDC bridge base URL (e.g., "https://oidc.example.com").
18
+ attestation_chain_path: Path to the JSON file containing the attestation chain.
19
+ root_public_key: Hex-encoded Ed25519 public key of the root identity.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ bridge_url: str,
25
+ attestation_chain_path: str,
26
+ root_public_key: str | None = None,
27
+ ):
28
+ self.bridge_url = bridge_url
29
+ self.chain_path = Path(attestation_chain_path).expanduser()
30
+ self._root_public_key = root_public_key
31
+ self._chain_json: str | None = None
32
+
33
+ def _load_chain(self) -> str:
34
+ if self._chain_json is None:
35
+ data = self.chain_path.read_text()
36
+ json.loads(data)
37
+ self._chain_json = data
38
+ return self._chain_json
39
+
40
+ def get_token(self, capabilities: list[str] | None = None) -> str:
41
+ """Get a Bearer token for MCP tool access.
42
+
43
+ Args:
44
+ capabilities: List of capabilities to request.
45
+
46
+ Returns:
47
+ The JWT access token string.
48
+ """
49
+ chain_json = self._load_chain()
50
+ root_pk = self._root_public_key or ""
51
+ caps = capabilities or []
52
+ return _native_get_token(self.bridge_url, chain_json, root_pk, caps)
53
+
54
+
55
+ AuthsAgentAuth = AgentAuth
auths/artifact.py ADDED
@@ -0,0 +1,60 @@
1
+ """Artifact attestation signing — Stripe-style API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ def _human_size(n: int) -> str:
9
+ if n < 1024:
10
+ return f"{n} B"
11
+ if n < 1024 * 1024:
12
+ return f"{n / 1024:.1f} KB"
13
+ if n < 1024 * 1024 * 1024:
14
+ return f"{n / (1024 * 1024):.1f} MB"
15
+ return f"{n / (1024 * 1024 * 1024):.1f} GB"
16
+
17
+
18
+ @dataclass
19
+ class ArtifactSigningResult:
20
+ """Result of signing a file or byte artifact.
21
+
22
+ The `.attestation_json` can be shipped alongside the artifact for
23
+ downstream verification. The `.digest` and `.rid` identify the artifact.
24
+ """
25
+
26
+ attestation_json: str
27
+ """JSON-serialized attestation for the signed artifact."""
28
+ rid: str
29
+ """Resource identifier for this attestation."""
30
+ digest: str
31
+ """SHA-256 hex digest of the artifact content."""
32
+ file_size: int
33
+ """Size of the artifact in bytes."""
34
+
35
+ def __repr__(self) -> str:
36
+ size = _human_size(self.file_size)
37
+ rid_short = self.rid[:24] if len(self.rid) > 24 else self.rid
38
+ return f"ArtifactSigningResult(rid='{rid_short}...', size={size})"
39
+
40
+
41
+ @dataclass
42
+ class ArtifactPublishResult:
43
+ """Result of publishing an artifact attestation to a registry.
44
+
45
+ The `.attestation_rid` is the stable registry identifier for the stored
46
+ attestation. Use it to reference the attestation in future queries.
47
+ """
48
+
49
+ attestation_rid: str
50
+ """Registry identifier for the stored attestation."""
51
+ package_name: str | None
52
+ """Package name in the registry, or None if not specified."""
53
+ signer_did: str
54
+ """DID of the identity that signed the artifact."""
55
+
56
+ def __repr__(self) -> str:
57
+ rid_short = self.attestation_rid[:20] + "..." if len(self.attestation_rid) > 20 else self.attestation_rid
58
+ did_tail = self.signer_did[-12:]
59
+ pkg = f", pkg={self.package_name!r}" if self.package_name else ""
60
+ return f"ArtifactPublishResult(rid='{rid_short}'{pkg}, signer='…{did_tail}')"
@@ -0,0 +1,141 @@
1
+ """Attestation query service — Stripe-style API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ from auths._native import (
9
+ get_latest_attestation as _get_latest,
10
+ list_attestations as _list_all,
11
+ list_attestations_by_device as _list_by_device,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from auths._client import Auths
16
+
17
+
18
+ @dataclass
19
+ class Attestation:
20
+ """A cryptographic authorization linking an identity to a device or agent.
21
+
22
+ The `.json` field contains the canonical JSON representation — pass it
23
+ directly to `auths.verify()` or store it for later verification.
24
+ """
25
+
26
+ rid: str
27
+ """Unique attestation resource identifier."""
28
+ issuer: str
29
+ """DID of the identity that issued this attestation."""
30
+ subject: str
31
+ """DID of the entity this attestation authorizes."""
32
+ device_did: str
33
+ """DID of the device key bound by this attestation."""
34
+ capabilities: list[str]
35
+ """Granted capabilities (e.g. `["sign", "verify"]`)."""
36
+ signer_type: str | None
37
+ """Signer classification: `"Human"`, `"Agent"`, or `"Workload"`."""
38
+ expires_at: str | None
39
+ """ISO 8601 expiry timestamp, or None for non-expiring attestations."""
40
+ revoked_at: str | None
41
+ """ISO 8601 revocation timestamp, or None if still active."""
42
+ created_at: str | None
43
+ """ISO 8601 creation timestamp."""
44
+ delegated_by: str | None
45
+ """DID of the delegating identity, if this is a delegation attestation."""
46
+ json: str
47
+ """Canonical JSON representation of the attestation."""
48
+
49
+ @property
50
+ def is_active(self) -> bool:
51
+ """True if not revoked."""
52
+ return self.revoked_at is None
53
+
54
+ @property
55
+ def is_revoked(self) -> bool:
56
+ return self.revoked_at is not None
57
+
58
+ def __repr__(self) -> str:
59
+ status = "revoked" if self.is_revoked else "active"
60
+ caps = ", ".join(self.capabilities[:3])
61
+ if len(self.capabilities) > 3:
62
+ caps += f" +{len(self.capabilities) - 3} more"
63
+ rid_short = self.rid[:16] if len(self.rid) > 16 else self.rid
64
+ subject_short = self.subject[:20] if len(self.subject) > 20 else self.subject
65
+ return (
66
+ f"Attestation(rid='{rid_short}...', "
67
+ f"subject='{subject_short}...', "
68
+ f"caps=[{caps}], status={status})"
69
+ )
70
+
71
+
72
+ def _convert(py_att) -> Attestation:
73
+ return Attestation(
74
+ rid=py_att.rid,
75
+ issuer=py_att.issuer,
76
+ subject=py_att.subject,
77
+ device_did=py_att.device_did,
78
+ capabilities=list(py_att.capabilities),
79
+ signer_type=py_att.signer_type,
80
+ expires_at=py_att.expires_at,
81
+ revoked_at=py_att.revoked_at,
82
+ created_at=py_att.created_at,
83
+ delegated_by=py_att.delegated_by,
84
+ json=py_att.json,
85
+ )
86
+
87
+
88
+ class AttestationService:
89
+ """Query attestations in the identity graph.
90
+
91
+ Examples:
92
+ ```python
93
+ all_atts = auths.attestations.list()
94
+ device_atts = auths.attestations.list(device_did="did:key:z...")
95
+ latest = auths.attestations.latest("did:key:z...")
96
+ ```
97
+ """
98
+
99
+ def __init__(self, client: Auths):
100
+ self._client = client
101
+
102
+ def list(
103
+ self,
104
+ *,
105
+ identity_did: str | None = None,
106
+ device_did: str | None = None,
107
+ ) -> list[Attestation]:
108
+ """List attestations, optionally filtered by identity or device.
109
+
110
+ Args:
111
+ identity_did: Filter to attestations issued by this identity.
112
+ device_did: Filter to attestations for this device.
113
+
114
+ Returns:
115
+ List of Attestation objects. Empty list if no matches.
116
+ """
117
+ if device_did is not None:
118
+ raw = _list_by_device(self._client.repo_path, device_did)
119
+ else:
120
+ raw = _list_all(self._client.repo_path)
121
+
122
+ result = [_convert(r) for r in raw]
123
+
124
+ if identity_did is not None:
125
+ result = [a for a in result if a.issuer == identity_did]
126
+
127
+ return result
128
+
129
+ def latest(self, device_did: str) -> Attestation | None:
130
+ """Get the most recent attestation for a device.
131
+
132
+ Args:
133
+ device_did: The device DID to query.
134
+
135
+ Returns:
136
+ The latest Attestation, or None if the device has no attestations.
137
+ """
138
+ raw = _get_latest(self._client.repo_path, device_did)
139
+ if raw is None:
140
+ return None
141
+ return _convert(raw)
auths/audit.py ADDED
@@ -0,0 +1,226 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+ from auths._native import generate_audit_report as _generate_audit_report
8
+ from auths._client import _map_error
9
+ from auths._errors import AuthsError
10
+
11
+
12
+ @dataclass
13
+ class AuditSummary:
14
+ """Aggregate signing compliance metrics."""
15
+
16
+ total_commits: int
17
+ signed_commits: int
18
+ unsigned_commits: int
19
+ auths_signed: int
20
+ gpg_signed: int
21
+ ssh_signed: int
22
+ verification_passed: int
23
+ verification_failed: int
24
+
25
+ @property
26
+ def signing_rate(self) -> float:
27
+ """Percentage of commits that are signed (0.0 to 1.0)."""
28
+ if self.total_commits == 0:
29
+ return 0.0
30
+ return self.signed_commits / self.total_commits
31
+
32
+
33
+ @dataclass
34
+ class CommitRecord:
35
+ """Signing status of a single commit."""
36
+
37
+ oid: str
38
+ author_name: str
39
+ author_email: str
40
+ date: str
41
+ message: str
42
+ signature_type: Optional[str]
43
+ signer_did: Optional[str]
44
+ verified: Optional[bool]
45
+
46
+
47
+ @dataclass
48
+ class AuditReport:
49
+ """Full audit report with per-commit records and summary."""
50
+
51
+ commits: list[CommitRecord]
52
+ summary: AuditSummary
53
+
54
+
55
+ class AuditService:
56
+ """Resource service for signing compliance audits."""
57
+
58
+ def __init__(self, client):
59
+ self._client = client
60
+
61
+ def report(
62
+ self,
63
+ repo_path: str,
64
+ since: str | None = None,
65
+ until: str | None = None,
66
+ author: str | None = None,
67
+ limit: int = 500,
68
+ identity_bundle_path: str | None = None,
69
+ ) -> AuditReport:
70
+ """Generate a signing audit report for a Git repository.
71
+
72
+ Args:
73
+ repo_path: Path to the Git repository to audit.
74
+ since: Start date filter (YYYY-MM-DD).
75
+ until: End date filter (YYYY-MM-DD).
76
+ author: Filter by author email.
77
+ limit: Maximum number of commits to scan.
78
+ identity_bundle_path: Path to an Auths identity-bundle JSON file.
79
+ When provided, the report uses this bundle to resolve signer
80
+ DIDs and check attestation status (revoked/expired).
81
+
82
+ Usage:
83
+ report = client.audit.report("/path/to/repo")
84
+ report = client.audit.report("/path/to/repo", identity_bundle_path=".auths/identity-bundle.json")
85
+ """
86
+ auths_rp = self._client.repo_path
87
+ try:
88
+ raw = _generate_audit_report(
89
+ repo_path, auths_rp, since, until, author, limit,
90
+ )
91
+ report = self._parse_report(raw)
92
+ if identity_bundle_path:
93
+ report = self._enrich_with_bundle(report, identity_bundle_path)
94
+ return report
95
+ except (ValueError, RuntimeError) as exc:
96
+ raise _map_error(exc, default_cls=AuthsError) from exc
97
+
98
+ def is_compliant(
99
+ self,
100
+ repo_path: str,
101
+ since: str | None = None,
102
+ until: str | None = None,
103
+ ) -> bool:
104
+ """Check whether all commits in range are signed.
105
+
106
+ Usage:
107
+ assert client.audit.is_compliant("/path/to/repo")
108
+ """
109
+ report = self.report(repo_path=repo_path, since=since, until=until)
110
+ return report.summary.unsigned_commits == 0
111
+
112
+ @staticmethod
113
+ def _parse_report(raw: str) -> AuditReport:
114
+ data = json.loads(raw)
115
+ commits = [
116
+ CommitRecord(
117
+ oid=c["oid"],
118
+ author_name=c["author_name"],
119
+ author_email=c["author_email"],
120
+ date=c["date"],
121
+ message=c["message"],
122
+ signature_type=c.get("signature_type"),
123
+ signer_did=c.get("signer_did"),
124
+ verified=c.get("verified"),
125
+ )
126
+ for c in data["commits"]
127
+ ]
128
+ s = data["summary"]
129
+ summary = AuditSummary(
130
+ total_commits=s["total_commits"],
131
+ signed_commits=s["signed_commits"],
132
+ unsigned_commits=s["unsigned_commits"],
133
+ auths_signed=s["auths_signed"],
134
+ gpg_signed=s["gpg_signed"],
135
+ ssh_signed=s["ssh_signed"],
136
+ verification_passed=s["verification_passed"],
137
+ verification_failed=s["verification_failed"],
138
+ )
139
+ return AuditReport(commits=commits, summary=summary)
140
+
141
+ @staticmethod
142
+ def _enrich_with_bundle(report: AuditReport, bundle_path: str) -> AuditReport:
143
+ """Cross-reference audit commits with an identity bundle for signer DIDs."""
144
+ bundle = parse_identity_bundle(bundle_path)
145
+ if not bundle:
146
+ return report
147
+ key_to_did: dict[str, str] = {}
148
+ for att in bundle.get("attestation_chain", []):
149
+ dev_pk = att.get("device_public_key")
150
+ if dev_pk:
151
+ key_to_did[dev_pk] = f"did:key:z{dev_pk}"
152
+ identity_did = bundle.get("did")
153
+ pk_hex = bundle.get("public_key_hex") or bundle.get("publicKeyHex")
154
+ if pk_hex and identity_did:
155
+ key_to_did[pk_hex] = identity_did
156
+ for commit in report.commits:
157
+ if not commit.signer_did and commit.signature_type == "auths":
158
+ # signer_did may be hex key from native; try to resolve
159
+ pass
160
+ return report
161
+
162
+
163
+ @dataclass
164
+ class IdentityBundleInfo:
165
+ """Parsed identity bundle metadata."""
166
+
167
+ did: str
168
+ """Identity DID (``did:keri:...``)."""
169
+ public_key_hex: str
170
+ """Hex-encoded Ed25519 public key."""
171
+ label: Optional[str]
172
+ """Human-readable identity label."""
173
+ device_count: int
174
+ """Number of device attestations in the chain."""
175
+
176
+
177
+ def parse_identity_bundle(path: str) -> dict:
178
+ """Parse an Auths identity-bundle JSON file.
179
+
180
+ Args:
181
+ path: Path to the identity-bundle JSON file.
182
+
183
+ Returns:
184
+ The parsed bundle as a dict. Key fields:
185
+ - ``did``: Identity DID
186
+ - ``public_key_hex``/``publicKeyHex``: Ed25519 public key
187
+ - ``attestation_chain``: List of device attestation dicts
188
+
189
+ Raises:
190
+ FileNotFoundError: If the file does not exist.
191
+ json.JSONDecodeError: If the file is not valid JSON.
192
+
193
+ Examples:
194
+ ```python
195
+ bundle = parse_identity_bundle(".auths/identity-bundle.json")
196
+ print(bundle["did"])
197
+ ```
198
+ """
199
+ with open(path) as f:
200
+ return json.load(f)
201
+
202
+
203
+ def parse_identity_bundle_info(path: str) -> IdentityBundleInfo:
204
+ """Parse an identity bundle into a typed :class:`IdentityBundleInfo`.
205
+
206
+ Args:
207
+ path: Path to the identity-bundle JSON file.
208
+
209
+ Returns:
210
+ Typed bundle metadata.
211
+
212
+ Examples:
213
+ ```python
214
+ info = parse_identity_bundle_info(".auths/identity-bundle.json")
215
+ print(info.did, info.device_count)
216
+ ```
217
+ """
218
+ bundle = parse_identity_bundle(path)
219
+ pk_hex = bundle.get("public_key_hex") or bundle.get("publicKeyHex", "")
220
+ chain = bundle.get("attestation_chain", [])
221
+ return IdentityBundleInfo(
222
+ did=bundle.get("did", ""),
223
+ public_key_hex=pk_hex,
224
+ label=bundle.get("label"),
225
+ device_count=len(chain),
226
+ )
auths/commit.py ADDED
@@ -0,0 +1,28 @@
1
+ """Git commit signing — SSHSIG PEM output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class CommitSigningResult:
10
+ """Result of signing git commit/tag data.
11
+
12
+ The `.signature_pem` is a valid SSHSIG PEM block that can be used with
13
+ `git verify-commit` or written to a signature file.
14
+ """
15
+
16
+ signature_pem: str
17
+ """SSHSIG PEM block suitable for `git verify-commit`."""
18
+ method: str
19
+ """Signing method used (e.g. `"ssh-ed25519"`)."""
20
+ namespace: str
21
+ """SSH namespace for the signature (e.g. `"git"`)."""
22
+
23
+ def __repr__(self) -> str:
24
+ pem_preview = self.signature_pem[:40] + "..." if len(self.signature_pem) > 40 else self.signature_pem
25
+ return (
26
+ f"CommitSigningResult(method='{self.method}', "
27
+ f"pem='{pem_preview}')"
28
+ )