capauth 0.2.3__tar.gz → 0.2.14__tar.gz
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.
- {capauth-0.2.3/src/capauth.egg-info → capauth-0.2.14}/PKG-INFO +69 -1
- {capauth-0.2.3 → capauth-0.2.14}/README.md +61 -0
- {capauth-0.2.3 → capauth-0.2.14}/pyproject.toml +8 -1
- capauth-0.2.14/src/capauth/__init__.py +177 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/agent_identity.py +28 -8
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/authentik/claims_mapper.py +1 -1
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/authentik/stage.py +70 -28
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/authentik/verifier.py +121 -9
- capauth-0.2.14/src/capauth/authz.py +380 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/cli.py +478 -0
- capauth-0.2.14/src/capauth/crypto/__init__.py +91 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/crypto/base.py +57 -2
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/crypto/gnupg_backend.py +28 -3
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/crypto/pgpy_backend.py +64 -2
- capauth-0.2.14/src/capauth/crypto/sequoia_backend.py +435 -0
- capauth-0.2.14/src/capauth/crypto/skpgp_backend.py +220 -0
- capauth-0.2.14/src/capauth/custody.py +638 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/did.py +34 -6
- capauth-0.2.14/src/capauth/exceptions.py +68 -0
- capauth-0.2.14/src/capauth/identity.py +256 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integration.py +41 -12
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/cli.py +8 -2
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/forgejo_api.py +2 -1
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/oidc_provider.py +4 -4
- capauth-0.2.14/src/capauth/integrations/nextcloud/tests/oidc/oidc_e2e_login_test.py +38 -0
- capauth-0.2.14/src/capauth/key_transparency.py +433 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/login.py +2 -2
- capauth-0.2.14/src/capauth/manifest.py +674 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/migrations/0001_initial.py +3 -1
- capauth-0.2.14/src/capauth/migrations/0002_widen_fingerprint_to_64.py +40 -0
- capauth-0.2.14/src/capauth/models.py +201 -0
- capauth-0.2.14/src/capauth/pairing/__init__.py +68 -0
- capauth-0.2.14/src/capauth/pairing/kernel.py +234 -0
- capauth-0.2.14/src/capauth/pairing/records.py +182 -0
- capauth-0.2.14/src/capauth/pairing/store.py +320 -0
- capauth-0.2.14/src/capauth/pairing/window.py +156 -0
- capauth-0.2.14/src/capauth/pqc_confidentiality.py +82 -0
- capauth-0.2.14/src/capauth/pqc_identity.py +220 -0
- capauth-0.2.14/src/capauth/pqc_root_identity.py +315 -0
- capauth-0.2.14/src/capauth/provisioning.py +80 -0
- capauth-0.2.14/src/capauth/seal.py +552 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/service/app.py +322 -9
- capauth-0.2.14/src/capauth/service/bunker.py +532 -0
- capauth-0.2.14/src/capauth/service/bunker_e2e.py +174 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/service/keystore.py +3 -3
- capauth-0.2.14/src/capauth/service/oidc/__init__.py +33 -0
- capauth-0.2.14/src/capauth/service/oidc/clients.py +111 -0
- capauth-0.2.14/src/capauth/service/oidc/passkey.py +205 -0
- capauth-0.2.14/src/capauth/service/oidc/provider.py +1049 -0
- capauth-0.2.14/src/capauth/service/oidc/signing_key.py +134 -0
- capauth-0.2.14/src/capauth/service/oidc/store.py +211 -0
- capauth-0.2.14/src/capauth/service/push.py +173 -0
- capauth-0.2.14/src/capauth/tokens.py +732 -0
- capauth-0.2.14/src/capauth/trust/__init__.py +53 -0
- capauth-0.2.14/src/capauth/trust/calibration.py +221 -0
- capauth-0.2.14/src/capauth/trust/graph.py +516 -0
- {capauth-0.2.3 → capauth-0.2.14/src/capauth.egg-info}/PKG-INFO +69 -1
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth.egg-info/SOURCES.txt +66 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth.egg-info/requires.txt +6 -0
- capauth-0.2.14/tests/conftest.py +83 -0
- capauth-0.2.14/tests/test_agent_identity.py +486 -0
- capauth-0.2.14/tests/test_audience_mint_helper.py +211 -0
- capauth-0.2.14/tests/test_audience_tokens.py +265 -0
- capauth-0.2.14/tests/test_authz.py +273 -0
- capauth-0.2.14/tests/test_backup_restore.py +304 -0
- capauth-0.2.14/tests/test_bunker.py +463 -0
- capauth-0.2.14/tests/test_bunker_e2e.py +143 -0
- capauth-0.2.14/tests/test_capauth_service_deploy.py +215 -0
- capauth-0.2.14/tests/test_custody.py +419 -0
- capauth-0.2.14/tests/test_did_capability_relationships.py +94 -0
- capauth-0.2.14/tests/test_fingerprint_v6.py +75 -0
- capauth-0.2.14/tests/test_forgejo_preflight.py +111 -0
- capauth-0.2.14/tests/test_identity_ttl.py +292 -0
- capauth-0.2.14/tests/test_key_revocation_expiry.py +263 -0
- capauth-0.2.14/tests/test_key_transparency.py +171 -0
- capauth-0.2.14/tests/test_manifest.py +402 -0
- capauth-0.2.14/tests/test_models_pqc_q0.py +90 -0
- capauth-0.2.14/tests/test_no_subapp_dependency.py +140 -0
- capauth-0.2.14/tests/test_oidc_idp.py +469 -0
- capauth-0.2.14/tests/test_oidc_idp_e2e.py +178 -0
- capauth-0.2.14/tests/test_pairing.py +450 -0
- capauth-0.2.14/tests/test_passkey.py +137 -0
- capauth-0.2.14/tests/test_pqc_confidentiality.py +44 -0
- capauth-0.2.14/tests/test_pqc_identity.py +121 -0
- capauth-0.2.14/tests/test_pqc_root_integration.py +118 -0
- capauth-0.2.14/tests/test_pqc_t3_gate.py +526 -0
- capauth-0.2.14/tests/test_provisioning.py +39 -0
- capauth-0.2.14/tests/test_push.py +76 -0
- capauth-0.2.14/tests/test_py313_compat.py +74 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_qr_login.py +12 -4
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_register_cli.py +9 -0
- capauth-0.2.14/tests/test_seal.py +324 -0
- capauth-0.2.14/tests/test_sequoia_backend.py +161 -0
- capauth-0.2.14/tests/test_service_v6_fingerprint.py +87 -0
- capauth-0.2.14/tests/test_skpgp_backend.py +143 -0
- capauth-0.2.14/tests/test_tokens.py +263 -0
- capauth-0.2.14/tests/test_trust_calibration.py +289 -0
- capauth-0.2.14/tests/test_trust_graph.py +238 -0
- capauth-0.2.14/tests/test_verifier.py +246 -0
- capauth-0.2.3/src/capauth/__init__.py +0 -48
- capauth-0.2.3/src/capauth/crypto/__init__.py +0 -42
- capauth-0.2.3/src/capauth/exceptions.py +0 -33
- capauth-0.2.3/src/capauth/identity.py +0 -130
- capauth-0.2.3/src/capauth/models.py +0 -101
- capauth-0.2.3/tests/conftest.py +0 -46
- capauth-0.2.3/tests/test_agent_identity.py +0 -251
- capauth-0.2.3/tests/test_verifier.py +0 -120
- {capauth-0.2.3 → capauth-0.2.14}/LICENSE +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/MANIFEST.in +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/setup.cfg +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/apps.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/authentik/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/authentik/api.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/authentik/nonce_store.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/authentik/urls.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/discovery/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/discovery/base.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/discovery/file_discovery.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/discovery/mdns.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/discovery/syncthing.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/auth_flow.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/config.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/tests/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/tests/test_auth_flow.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/tests/test_cli.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/tests/test_config.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/tests/test_forgejo_api.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/integrations/forgejo/tests/test_oidc_provider.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/mesh.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/migrations/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/pma.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/profile.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/registry.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/service/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/service/server.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth/sync.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth.egg-info/dependency_links.txt +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth.egg-info/entry_points.txt +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/src/capauth.egg-info/top_level.txt +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/__init__.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_claims_mapper.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_crypto.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_home_resolution.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_identity.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_integration.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_login.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_mesh.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_nextcloud_app.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_nonce_store.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_pma.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_profile.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_real_pgp_e2e.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_registry.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_service.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_syncthing_discovery.py +0 -0
- {capauth-0.2.3 → capauth-0.2.14}/tests/test_zk_profile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capauth
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
4
4
|
Summary: Capability-based Authentication — decentralized, PGP-based identity and authorization
|
|
5
5
|
Author-email: smilinTux <admin@smilintux.org>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
22
|
Classifier: Topic :: Security :: Cryptography
|
|
22
23
|
Requires-Python: >=3.10
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
@@ -35,10 +36,13 @@ Requires-Dist: python-gnupg>=0.5; extra == "gnupg"
|
|
|
35
36
|
Provides-Extra: service
|
|
36
37
|
Requires-Dist: fastapi>=0.110; extra == "service"
|
|
37
38
|
Requires-Dist: uvicorn[standard]>=0.27; extra == "service"
|
|
39
|
+
Requires-Dist: python-multipart>=0.0.6; extra == "service"
|
|
38
40
|
Requires-Dist: PyJWT>=2.8; extra == "service"
|
|
39
41
|
Requires-Dist: python-gnupg>=0.5; extra == "service"
|
|
40
42
|
Requires-Dist: httpx>=0.25; extra == "service"
|
|
41
43
|
Requires-Dist: segno>=1.6; extra == "service"
|
|
44
|
+
Requires-Dist: pywebpush>=1.14; extra == "service"
|
|
45
|
+
Requires-Dist: webauthn>=2.0; extra == "service"
|
|
42
46
|
Provides-Extra: dev
|
|
43
47
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
44
48
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -49,12 +53,17 @@ Requires-Dist: python-gnupg>=0.5; extra == "dev"
|
|
|
49
53
|
Requires-Dist: black>=24.0; extra == "dev"
|
|
50
54
|
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
51
55
|
Requires-Dist: zeroconf>=0.38; extra == "dev"
|
|
56
|
+
Requires-Dist: webauthn>=2.0; extra == "dev"
|
|
57
|
+
Requires-Dist: soft-webauthn>=0.1; extra == "dev"
|
|
58
|
+
Requires-Dist: pywebpush>=1.14; extra == "dev"
|
|
52
59
|
Provides-Extra: all
|
|
53
60
|
Requires-Dist: capauth[dev,gnupg,service]; extra == "all"
|
|
54
61
|
Dynamic: license-file
|
|
55
62
|
|
|
56
63
|
# capauth — Sovereign PGP Identity 🔐
|
|
57
64
|
|
|
65
|
+
[](https://github.com/smilinTux/capauth/actions/workflows/pytest.yml)
|
|
66
|
+
|
|
58
67
|
> **OAuth is dead. Long live sovereignty.**
|
|
59
68
|
> Your identity is a PGP keypair *you* generated, on hardware *you* own. No
|
|
60
69
|
> "Login with Google", no authorization server in the middle, no revocation risk
|
|
@@ -73,6 +82,20 @@ bearer token issued by a third party, you sign a random challenge with a key onl
|
|
|
73
82
|
you hold. The verifier checks the signature against your public key. Valid
|
|
74
83
|
signature = authenticated. Done. No middleman ever sees the secret.
|
|
75
84
|
|
|
85
|
+
> **Maturity tier: T0 (live).** The live sovereign root and the agent
|
|
86
|
+
> signing / DID / challenge-response keys are **classical** Ed25519 / RSA-4096
|
|
87
|
+
> (RFC 8032 / 4880) — Shor-breakable once a CRQC exists. capauth is a
|
|
88
|
+
> **signature / identity** layer (not a KEM), so signatures are not retroactively
|
|
89
|
+
> breakable and Harvest-Now-Decrypt-Later does not apply — migration is real but
|
|
90
|
+
> **deferrable**. The **T3 hybrid-signature path is additive and proven**: the
|
|
91
|
+
> Sequoia (`sq`) backend issues + verifies **ML-DSA-87 + Ed448** (FIPS 204) /
|
|
92
|
+
> **ML-KEM-1024 + X448** (FIPS 203) composite v6 keys end-to-end, but the live root
|
|
93
|
+
> stays classical until the gated root-rotation ceremony. **Honest claim:** this is
|
|
94
|
+
> *not* "quantum-proof" / "quantum-safe." See [SOP.md](SOP.md),
|
|
95
|
+
> [docs/CRYPTO_SPEC.md](docs/CRYPTO_SPEC.md), and the sk-standards
|
|
96
|
+
> [CRYPTOGRAPHY_STANDARD](https://github.com/smilinTux/sk-standards). Migration:
|
|
97
|
+
> epic `PQC-MIGRATION` (coord `e1d6ba2a`).
|
|
98
|
+
|
|
76
99
|
---
|
|
77
100
|
|
|
78
101
|
## The 60-second version
|
|
@@ -179,6 +202,31 @@ all Syncthing mesh nodes so every host shares one keypair.
|
|
|
179
202
|
| **Integration generators** | One-shot config for third-party login, e.g. Forgejo OAuth2/OIDC (`capauth setup forgejo`) |
|
|
180
203
|
| **skcapstone adapter** | Default-on-by-presence: routes auth events to `sk-alert`, registers a key-rotation check with `skscheduler` |
|
|
181
204
|
|
|
205
|
+
### Challenge TTL and replay contract (`identity.verify_challenge`)
|
|
206
|
+
|
|
207
|
+
`verify_challenge` enforces a **max challenge age of 5 minutes by default**
|
|
208
|
+
(`DEFAULT_MAX_CHALLENGE_AGE_SECONDS = 300`); older challenges raise
|
|
209
|
+
`ChallengeExpiredError`. Tune it with `max_age_seconds=...`, or pass
|
|
210
|
+
`max_age_seconds=None` to opt out **only** when your layer enforces TTL
|
|
211
|
+
itself (the service layer does, via its nonce store).
|
|
212
|
+
|
|
213
|
+
**Within the TTL the bare primitive is replayable**: the same signed
|
|
214
|
+
response verifies repeatedly unless you track seen challenge ids. For
|
|
215
|
+
single-use semantics pass a `replay_guard`:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from capauth.identity import InMemoryReplayGuard, verify_challenge
|
|
219
|
+
|
|
220
|
+
guard = InMemoryReplayGuard() # single-process reference implementation
|
|
221
|
+
verify_challenge(challenge, response, pubkey, replay_guard=guard)
|
|
222
|
+
verify_challenge(challenge, response, pubkey, replay_guard=guard) # raises ChallengeReplayError
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Any `(challenge_id, expires_at) -> bool` callable works as a guard. For
|
|
226
|
+
durable / multi-node deployments use a real nonce store; the reference
|
|
227
|
+
implementation is `capauth.authentik.nonce_store.NonceStore` (what the
|
|
228
|
+
verification service uses).
|
|
229
|
+
|
|
182
230
|
## Key CLI commands
|
|
183
231
|
|
|
184
232
|
```bash
|
|
@@ -231,6 +279,8 @@ rides in the payload `event` field so routing stays severity-based.
|
|
|
231
279
|
| **[Protocol](docs/PROTOCOL.md)** | the CapAuth wire protocol specification |
|
|
232
280
|
| **[Claims](docs/CLAIMS.md)** | capability claims and token format |
|
|
233
281
|
| **[Integration Blueprint](docs/INTEGRATION_BLUEPRINT.md)** | third-party integration guide |
|
|
282
|
+
| **[Cold-Machine Bootstrap & DR](docs/COLD_MACHINE_BOOTSTRAP_AND_DR.md)** | standing capauth back up on a blank box + disaster recovery: the restore-not-regenerate rule, the ordered restore chain, and the operator checklist |
|
|
283
|
+
| **[authentik-capauth](docs/authentik-capauth.md)** | the custom Authentik image with the CapAuth PGP stage baked in — build (`AK_VERSION`, version-agnostic venv install, frontend rebuild), SKStacks deploy + `lifecycle.migrate` override, and the four build/migrate gotchas |
|
|
234
284
|
| **[AI Advocate](AI-ADVOCATE.md)** | how AI advocates manage a sovereign profile on your behalf |
|
|
235
285
|
|
|
236
286
|
## Why it matters
|
|
@@ -245,6 +295,24 @@ verification instantly instead of going undetected.
|
|
|
245
295
|
|
|
246
296
|
> **"You are not a user. You are a sovereign."**
|
|
247
297
|
|
|
298
|
+
## Related projects / See also
|
|
299
|
+
|
|
300
|
+
capauth is the identity root of SKWorld — most of the stack links back here.
|
|
301
|
+
|
|
302
|
+
- ⬇️ **Used by:** [skchat](https://github.com/smilinTux/skchat) — routes messages by
|
|
303
|
+
the identity capauth resolves (`resolve_agent_identity()`); per-agent signing key.
|
|
304
|
+
- ⬇️ **Used by:** [skcomms](https://github.com/smilinTux/skcomms) — capauth-signed
|
|
305
|
+
envelopes + FQID (`<a>@<op>.<realm>`) sovereign addressing.
|
|
306
|
+
- ↔️ **Sibling (PQC signing root):** [sk_pgp](https://github.com/smilinTux/sk_pgp) —
|
|
307
|
+
the sovereign OpenPGP-PQC library (Sequoia-backed) capauth's PQC root migrates onto.
|
|
308
|
+
- ↔️ **Sibling (hybrid KEM):** [sk-pqc](https://github.com/smilinTux/sk-pqc-py) — the
|
|
309
|
+
`HKDF(X25519 ‖ ML-KEM-768)` KEM for confidentiality; capauth provides the
|
|
310
|
+
authentication that a KEM-only library deliberately does not (pair them).
|
|
311
|
+
- ↔️ **Sibling (Security capability):** [sksecurity](https://github.com/smilinTux/sksecurity)
|
|
312
|
+
— produces the runtime crypto self-report that makes capauth's claims evidence-backed.
|
|
313
|
+
- 📐 **Standards:** [sk-standards](https://github.com/smilinTux/sk-standards) — the
|
|
314
|
+
crypto, data-flow, version, and doc/SOP standards (incl. `CRYPTOGRAPHY_STANDARD`).
|
|
315
|
+
|
|
248
316
|
## License
|
|
249
317
|
|
|
250
318
|
**GPL-3.0-or-later** — Free as in freedom. Identity is a right, not a product.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# capauth — Sovereign PGP Identity 🔐
|
|
2
2
|
|
|
3
|
+
[](https://github.com/smilinTux/capauth/actions/workflows/pytest.yml)
|
|
4
|
+
|
|
3
5
|
> **OAuth is dead. Long live sovereignty.**
|
|
4
6
|
> Your identity is a PGP keypair *you* generated, on hardware *you* own. No
|
|
5
7
|
> "Login with Google", no authorization server in the middle, no revocation risk
|
|
@@ -18,6 +20,20 @@ bearer token issued by a third party, you sign a random challenge with a key onl
|
|
|
18
20
|
you hold. The verifier checks the signature against your public key. Valid
|
|
19
21
|
signature = authenticated. Done. No middleman ever sees the secret.
|
|
20
22
|
|
|
23
|
+
> **Maturity tier: T0 (live).** The live sovereign root and the agent
|
|
24
|
+
> signing / DID / challenge-response keys are **classical** Ed25519 / RSA-4096
|
|
25
|
+
> (RFC 8032 / 4880) — Shor-breakable once a CRQC exists. capauth is a
|
|
26
|
+
> **signature / identity** layer (not a KEM), so signatures are not retroactively
|
|
27
|
+
> breakable and Harvest-Now-Decrypt-Later does not apply — migration is real but
|
|
28
|
+
> **deferrable**. The **T3 hybrid-signature path is additive and proven**: the
|
|
29
|
+
> Sequoia (`sq`) backend issues + verifies **ML-DSA-87 + Ed448** (FIPS 204) /
|
|
30
|
+
> **ML-KEM-1024 + X448** (FIPS 203) composite v6 keys end-to-end, but the live root
|
|
31
|
+
> stays classical until the gated root-rotation ceremony. **Honest claim:** this is
|
|
32
|
+
> *not* "quantum-proof" / "quantum-safe." See [SOP.md](SOP.md),
|
|
33
|
+
> [docs/CRYPTO_SPEC.md](docs/CRYPTO_SPEC.md), and the sk-standards
|
|
34
|
+
> [CRYPTOGRAPHY_STANDARD](https://github.com/smilinTux/sk-standards). Migration:
|
|
35
|
+
> epic `PQC-MIGRATION` (coord `e1d6ba2a`).
|
|
36
|
+
|
|
21
37
|
---
|
|
22
38
|
|
|
23
39
|
## The 60-second version
|
|
@@ -124,6 +140,31 @@ all Syncthing mesh nodes so every host shares one keypair.
|
|
|
124
140
|
| **Integration generators** | One-shot config for third-party login, e.g. Forgejo OAuth2/OIDC (`capauth setup forgejo`) |
|
|
125
141
|
| **skcapstone adapter** | Default-on-by-presence: routes auth events to `sk-alert`, registers a key-rotation check with `skscheduler` |
|
|
126
142
|
|
|
143
|
+
### Challenge TTL and replay contract (`identity.verify_challenge`)
|
|
144
|
+
|
|
145
|
+
`verify_challenge` enforces a **max challenge age of 5 minutes by default**
|
|
146
|
+
(`DEFAULT_MAX_CHALLENGE_AGE_SECONDS = 300`); older challenges raise
|
|
147
|
+
`ChallengeExpiredError`. Tune it with `max_age_seconds=...`, or pass
|
|
148
|
+
`max_age_seconds=None` to opt out **only** when your layer enforces TTL
|
|
149
|
+
itself (the service layer does, via its nonce store).
|
|
150
|
+
|
|
151
|
+
**Within the TTL the bare primitive is replayable**: the same signed
|
|
152
|
+
response verifies repeatedly unless you track seen challenge ids. For
|
|
153
|
+
single-use semantics pass a `replay_guard`:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from capauth.identity import InMemoryReplayGuard, verify_challenge
|
|
157
|
+
|
|
158
|
+
guard = InMemoryReplayGuard() # single-process reference implementation
|
|
159
|
+
verify_challenge(challenge, response, pubkey, replay_guard=guard)
|
|
160
|
+
verify_challenge(challenge, response, pubkey, replay_guard=guard) # raises ChallengeReplayError
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Any `(challenge_id, expires_at) -> bool` callable works as a guard. For
|
|
164
|
+
durable / multi-node deployments use a real nonce store; the reference
|
|
165
|
+
implementation is `capauth.authentik.nonce_store.NonceStore` (what the
|
|
166
|
+
verification service uses).
|
|
167
|
+
|
|
127
168
|
## Key CLI commands
|
|
128
169
|
|
|
129
170
|
```bash
|
|
@@ -176,6 +217,8 @@ rides in the payload `event` field so routing stays severity-based.
|
|
|
176
217
|
| **[Protocol](docs/PROTOCOL.md)** | the CapAuth wire protocol specification |
|
|
177
218
|
| **[Claims](docs/CLAIMS.md)** | capability claims and token format |
|
|
178
219
|
| **[Integration Blueprint](docs/INTEGRATION_BLUEPRINT.md)** | third-party integration guide |
|
|
220
|
+
| **[Cold-Machine Bootstrap & DR](docs/COLD_MACHINE_BOOTSTRAP_AND_DR.md)** | standing capauth back up on a blank box + disaster recovery: the restore-not-regenerate rule, the ordered restore chain, and the operator checklist |
|
|
221
|
+
| **[authentik-capauth](docs/authentik-capauth.md)** | the custom Authentik image with the CapAuth PGP stage baked in — build (`AK_VERSION`, version-agnostic venv install, frontend rebuild), SKStacks deploy + `lifecycle.migrate` override, and the four build/migrate gotchas |
|
|
179
222
|
| **[AI Advocate](AI-ADVOCATE.md)** | how AI advocates manage a sovereign profile on your behalf |
|
|
180
223
|
|
|
181
224
|
## Why it matters
|
|
@@ -190,6 +233,24 @@ verification instantly instead of going undetected.
|
|
|
190
233
|
|
|
191
234
|
> **"You are not a user. You are a sovereign."**
|
|
192
235
|
|
|
236
|
+
## Related projects / See also
|
|
237
|
+
|
|
238
|
+
capauth is the identity root of SKWorld — most of the stack links back here.
|
|
239
|
+
|
|
240
|
+
- ⬇️ **Used by:** [skchat](https://github.com/smilinTux/skchat) — routes messages by
|
|
241
|
+
the identity capauth resolves (`resolve_agent_identity()`); per-agent signing key.
|
|
242
|
+
- ⬇️ **Used by:** [skcomms](https://github.com/smilinTux/skcomms) — capauth-signed
|
|
243
|
+
envelopes + FQID (`<a>@<op>.<realm>`) sovereign addressing.
|
|
244
|
+
- ↔️ **Sibling (PQC signing root):** [sk_pgp](https://github.com/smilinTux/sk_pgp) —
|
|
245
|
+
the sovereign OpenPGP-PQC library (Sequoia-backed) capauth's PQC root migrates onto.
|
|
246
|
+
- ↔️ **Sibling (hybrid KEM):** [sk-pqc](https://github.com/smilinTux/sk-pqc-py) — the
|
|
247
|
+
`HKDF(X25519 ‖ ML-KEM-768)` KEM for confidentiality; capauth provides the
|
|
248
|
+
authentication that a KEM-only library deliberately does not (pair them).
|
|
249
|
+
- ↔️ **Sibling (Security capability):** [sksecurity](https://github.com/smilinTux/sksecurity)
|
|
250
|
+
— produces the runtime crypto self-report that makes capauth's claims evidence-backed.
|
|
251
|
+
- 📐 **Standards:** [sk-standards](https://github.com/smilinTux/sk-standards) — the
|
|
252
|
+
crypto, data-flow, version, and doc/SOP standards (incl. `CRYPTOGRAPHY_STANDARD`).
|
|
253
|
+
|
|
193
254
|
## License
|
|
194
255
|
|
|
195
256
|
**GPL-3.0-or-later** — Free as in freedom. Identity is a right, not a product.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "capauth"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.14"
|
|
8
8
|
description = "Capability-based Authentication — decentralized, PGP-based identity and authorization"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "GPL-3.0-or-later"}
|
|
@@ -22,6 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.10",
|
|
23
23
|
"Programming Language :: Python :: 3.11",
|
|
24
24
|
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
25
26
|
"Topic :: Security :: Cryptography",
|
|
26
27
|
]
|
|
27
28
|
dependencies = [
|
|
@@ -43,10 +44,13 @@ gnupg = [
|
|
|
43
44
|
service = [
|
|
44
45
|
"fastapi>=0.110",
|
|
45
46
|
"uvicorn[standard]>=0.27",
|
|
47
|
+
"python-multipart>=0.0.6",
|
|
46
48
|
"PyJWT>=2.8",
|
|
47
49
|
"python-gnupg>=0.5",
|
|
48
50
|
"httpx>=0.25",
|
|
49
51
|
"segno>=1.6",
|
|
52
|
+
"pywebpush>=1.14",
|
|
53
|
+
"webauthn>=2.0",
|
|
50
54
|
]
|
|
51
55
|
dev = [
|
|
52
56
|
"pytest>=7.0",
|
|
@@ -58,6 +62,9 @@ dev = [
|
|
|
58
62
|
"black>=24.0",
|
|
59
63
|
"ruff>=0.3",
|
|
60
64
|
"zeroconf>=0.38",
|
|
65
|
+
"webauthn>=2.0",
|
|
66
|
+
"soft-webauthn>=0.1",
|
|
67
|
+
"pywebpush>=1.14",
|
|
61
68
|
]
|
|
62
69
|
all = [
|
|
63
70
|
"capauth[gnupg,service,dev]",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""CapAuth -- Capability-based Authentication.
|
|
2
|
+
|
|
3
|
+
Decentralized, PGP-based identity and authorization.
|
|
4
|
+
OAuth is dead. Long live sovereignty.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .agent_identity import AgentIdentity, resolve_agent_identity
|
|
13
|
+
from .manifest import (
|
|
14
|
+
DEFAULT_SIG_SUFFIX,
|
|
15
|
+
ManifestSigningError,
|
|
16
|
+
canonical_manifest_bytes,
|
|
17
|
+
is_canonical,
|
|
18
|
+
operator_fingerprint,
|
|
19
|
+
sign_manifest,
|
|
20
|
+
verify_manifest,
|
|
21
|
+
)
|
|
22
|
+
from .authz import (
|
|
23
|
+
DEFAULT_RULES,
|
|
24
|
+
OBLIGATION_AUDIT,
|
|
25
|
+
CapabilityRule,
|
|
26
|
+
Decision,
|
|
27
|
+
Obligation,
|
|
28
|
+
decide,
|
|
29
|
+
)
|
|
30
|
+
from .pairing import (
|
|
31
|
+
DeviceRecord,
|
|
32
|
+
Enrollment,
|
|
33
|
+
EnrollmentMode,
|
|
34
|
+
PairingError,
|
|
35
|
+
PairingStore,
|
|
36
|
+
PairingWindow,
|
|
37
|
+
approve,
|
|
38
|
+
enroll_device,
|
|
39
|
+
list_devices,
|
|
40
|
+
mode_satisfies,
|
|
41
|
+
open_window,
|
|
42
|
+
revoke,
|
|
43
|
+
)
|
|
44
|
+
from .provisioning import SKCHAT_SCOPES, provision_subject
|
|
45
|
+
from .tokens import (
|
|
46
|
+
AUDIENCE_SCOPES,
|
|
47
|
+
Capability,
|
|
48
|
+
SignedToken,
|
|
49
|
+
TokenPayload,
|
|
50
|
+
TokenType,
|
|
51
|
+
export_token,
|
|
52
|
+
has_scope,
|
|
53
|
+
import_token,
|
|
54
|
+
is_revoked,
|
|
55
|
+
issue_token,
|
|
56
|
+
list_tokens,
|
|
57
|
+
mint_agent_audience_token,
|
|
58
|
+
mint_audience_token,
|
|
59
|
+
revoke_token,
|
|
60
|
+
verify_audience_token,
|
|
61
|
+
verify_token,
|
|
62
|
+
)
|
|
63
|
+
from .trust import (
|
|
64
|
+
CALIBRATION_FILENAME,
|
|
65
|
+
DEFAULT_THRESHOLDS,
|
|
66
|
+
FORMATTERS,
|
|
67
|
+
TrustEdge,
|
|
68
|
+
TrustGraph,
|
|
69
|
+
TrustNode,
|
|
70
|
+
TrustThresholds,
|
|
71
|
+
apply_setting,
|
|
72
|
+
build_trust_graph,
|
|
73
|
+
format_dot,
|
|
74
|
+
format_json,
|
|
75
|
+
format_table,
|
|
76
|
+
load_calibration,
|
|
77
|
+
recommend_thresholds,
|
|
78
|
+
save_calibration,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"AgentIdentity",
|
|
83
|
+
"resolve_agent_identity",
|
|
84
|
+
"resolve_capauth_home",
|
|
85
|
+
# manifest signing (umbrella-shell signed module registry, section 5.3)
|
|
86
|
+
"sign_manifest",
|
|
87
|
+
"verify_manifest",
|
|
88
|
+
"canonical_manifest_bytes",
|
|
89
|
+
"is_canonical",
|
|
90
|
+
"operator_fingerprint",
|
|
91
|
+
"ManifestSigningError",
|
|
92
|
+
"DEFAULT_SIG_SUFFIX",
|
|
93
|
+
# authz kernel (spine M3: deterministic decide() PDP)
|
|
94
|
+
"decide",
|
|
95
|
+
"Decision",
|
|
96
|
+
"Obligation",
|
|
97
|
+
"CapabilityRule",
|
|
98
|
+
"DEFAULT_RULES",
|
|
99
|
+
"OBLIGATION_AUDIT",
|
|
100
|
+
# pairing kernel (spine M2: enrollment modes + operator window)
|
|
101
|
+
"enroll_device",
|
|
102
|
+
"approve",
|
|
103
|
+
"revoke",
|
|
104
|
+
"list_devices",
|
|
105
|
+
"open_window",
|
|
106
|
+
"mode_satisfies",
|
|
107
|
+
"PairingError",
|
|
108
|
+
"EnrollmentMode",
|
|
109
|
+
"Enrollment",
|
|
110
|
+
"DeviceRecord",
|
|
111
|
+
"PairingWindow",
|
|
112
|
+
"PairingStore",
|
|
113
|
+
# tokens (kernel track M1: moved verbatim from skcapstone)
|
|
114
|
+
"Capability",
|
|
115
|
+
"SignedToken",
|
|
116
|
+
"TokenPayload",
|
|
117
|
+
"TokenType",
|
|
118
|
+
"export_token",
|
|
119
|
+
"import_token",
|
|
120
|
+
"is_revoked",
|
|
121
|
+
"issue_token",
|
|
122
|
+
"list_tokens",
|
|
123
|
+
"revoke_token",
|
|
124
|
+
"verify_token",
|
|
125
|
+
# audience-scoped token minting (audience-mint, M1+/R4.2)
|
|
126
|
+
"AUDIENCE_SCOPES",
|
|
127
|
+
"mint_audience_token",
|
|
128
|
+
"mint_agent_audience_token",
|
|
129
|
+
"verify_audience_token",
|
|
130
|
+
"has_scope",
|
|
131
|
+
# trust (kernel track M1: moved verbatim from skcapstone)
|
|
132
|
+
"FORMATTERS",
|
|
133
|
+
"CALIBRATION_FILENAME",
|
|
134
|
+
"DEFAULT_THRESHOLDS",
|
|
135
|
+
"TrustEdge",
|
|
136
|
+
"TrustGraph",
|
|
137
|
+
"TrustNode",
|
|
138
|
+
"TrustThresholds",
|
|
139
|
+
"apply_setting",
|
|
140
|
+
"build_trust_graph",
|
|
141
|
+
"format_dot",
|
|
142
|
+
"format_json",
|
|
143
|
+
"format_table",
|
|
144
|
+
"load_calibration",
|
|
145
|
+
"recommend_thresholds",
|
|
146
|
+
"save_calibration",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
__version__ = "0.2.14"
|
|
150
|
+
|
|
151
|
+
SKCAPSTONE_HOME = Path.home() / ".skcapstone"
|
|
152
|
+
DEFAULT_CAPAUTH_DIR = SKCAPSTONE_HOME / "capauth"
|
|
153
|
+
LEGACY_CAPAUTH_DIR = Path.home() / ".capauth"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def resolve_capauth_home(base_dir: Path | None = None) -> Path:
|
|
157
|
+
"""Resolve the CapAuth home directory.
|
|
158
|
+
|
|
159
|
+
Priority:
|
|
160
|
+
1. Explicit ``base_dir``
|
|
161
|
+
2. ``CAPAUTH_HOME`` environment override
|
|
162
|
+
3. New default under ``~/.skcapstone/capauth`` when present
|
|
163
|
+
4. Legacy ``~/.capauth`` when it already exists
|
|
164
|
+
5. New default under ``~/.skcapstone/capauth``
|
|
165
|
+
"""
|
|
166
|
+
if base_dir is not None:
|
|
167
|
+
return Path(base_dir).expanduser()
|
|
168
|
+
|
|
169
|
+
env_home = os.environ.get("CAPAUTH_HOME")
|
|
170
|
+
if env_home:
|
|
171
|
+
return Path(env_home).expanduser()
|
|
172
|
+
|
|
173
|
+
if DEFAULT_CAPAUTH_DIR.exists():
|
|
174
|
+
return DEFAULT_CAPAUTH_DIR
|
|
175
|
+
if LEGACY_CAPAUTH_DIR.exists():
|
|
176
|
+
return LEGACY_CAPAUTH_DIR
|
|
177
|
+
return DEFAULT_CAPAUTH_DIR
|
|
@@ -24,8 +24,9 @@ A resolved identity carries **two complementary URIs**:
|
|
|
24
24
|
cluster.json is absent or malformed.
|
|
25
25
|
|
|
26
26
|
``fingerprint``
|
|
27
|
-
40
|
|
28
|
-
no real profile exists (placeholder identities are not
|
|
27
|
+
40 (v4) or 64 (v6) hex PGP fingerprint from the agent's CapAuth profile.
|
|
28
|
+
``None`` when no real profile exists (placeholder identities are not
|
|
29
|
+
surfaced here).
|
|
29
30
|
|
|
30
31
|
Public API
|
|
31
32
|
----------
|
|
@@ -85,8 +86,8 @@ class AgentIdentity:
|
|
|
85
86
|
fqid: Three-tier label — ``<agent>@<operator>.<realm>``
|
|
86
87
|
(e.g. ``"lumina@chef.skworld"``). ``None`` when
|
|
87
88
|
cluster.json is not available.
|
|
88
|
-
fingerprint: 40
|
|
89
|
-
profile. ``None`` when no real profile exists.
|
|
89
|
+
fingerprint: 40 (v4) or 64 (v6) hex PGP fingerprint from the agent's
|
|
90
|
+
CapAuth profile. ``None`` when no real profile exists.
|
|
90
91
|
"""
|
|
91
92
|
|
|
92
93
|
agent: str
|
|
@@ -110,6 +111,25 @@ class AgentIdentity:
|
|
|
110
111
|
"fingerprint": self.fingerprint,
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
def hybrid_prekey_available(self) -> bool:
|
|
115
|
+
"""Whether this agent advertises a hybrid PQ confidentiality prekey.
|
|
116
|
+
|
|
117
|
+
Honest capability lookup for the PQC cut-over (see
|
|
118
|
+
:mod:`capauth.pqc_confidentiality`): ``True`` only when a real
|
|
119
|
+
X25519+ML-KEM-768 prekey exists for this agent; ``False`` (classical /
|
|
120
|
+
negotiated downgrade) otherwise. CapAuth does not generate these keys
|
|
121
|
+
(Phase 2) — it reports them.
|
|
122
|
+
"""
|
|
123
|
+
from .pqc_confidentiality import hybrid_prekey_available
|
|
124
|
+
|
|
125
|
+
return hybrid_prekey_available(self.agent)
|
|
126
|
+
|
|
127
|
+
def confidentiality_suite(self) -> str:
|
|
128
|
+
"""The confidentiality suite a peer negotiates TO this agent today."""
|
|
129
|
+
from .pqc_confidentiality import confidentiality_suite_for
|
|
130
|
+
|
|
131
|
+
return confidentiality_suite_for(self.agent)
|
|
132
|
+
|
|
113
133
|
|
|
114
134
|
# ---------------------------------------------------------------------------
|
|
115
135
|
# Internal helpers
|
|
@@ -173,7 +193,7 @@ def _load_fingerprint(agent: str) -> Optional[str]:
|
|
|
173
193
|
agent: Short agent name.
|
|
174
194
|
|
|
175
195
|
Returns:
|
|
176
|
-
40
|
|
196
|
+
40 (v4) or 64 (v6) hex fingerprint string, or None.
|
|
177
197
|
"""
|
|
178
198
|
# 1. CapAuth profile.json (source of truth for real PGP profiles)
|
|
179
199
|
capauth_dir = _agent_capauth_dir(agent)
|
|
@@ -182,7 +202,7 @@ def _load_fingerprint(agent: str) -> Optional[str]:
|
|
|
182
202
|
try:
|
|
183
203
|
data = json.loads(profile_path.read_text(encoding="utf-8"))
|
|
184
204
|
fp = data.get("key_info", {}).get("fingerprint")
|
|
185
|
-
if isinstance(fp, str) and len(fp)
|
|
205
|
+
if isinstance(fp, str) and len(fp) in (40, 64):
|
|
186
206
|
return fp
|
|
187
207
|
except (json.JSONDecodeError, OSError) as exc:
|
|
188
208
|
logger.debug("profile.json parse failed for %s: %s", agent, exc)
|
|
@@ -193,7 +213,7 @@ def _load_fingerprint(agent: str) -> Optional[str]:
|
|
|
193
213
|
|
|
194
214
|
profile = load_profile(base_dir=capauth_dir)
|
|
195
215
|
fp = profile.key_info.fingerprint
|
|
196
|
-
if isinstance(fp, str) and len(fp)
|
|
216
|
+
if isinstance(fp, str) and len(fp) in (40, 64):
|
|
197
217
|
return fp
|
|
198
218
|
except ImportError:
|
|
199
219
|
pass
|
|
@@ -207,7 +227,7 @@ def _load_fingerprint(agent: str) -> Optional[str]:
|
|
|
207
227
|
data = json.loads(identity_path.read_text(encoding="utf-8"))
|
|
208
228
|
fp = data.get("fingerprint")
|
|
209
229
|
# Only return if it looks like a real fingerprint (not placeholder)
|
|
210
|
-
if isinstance(fp, str) and len(fp)
|
|
230
|
+
if isinstance(fp, str) and len(fp) in (40, 64):
|
|
211
231
|
# Reject placeholder fingerprints (all caps hex, but generated
|
|
212
232
|
# from SHA256 of "skcapstone:<name>")
|
|
213
233
|
if data.get("capauth_managed", False):
|
|
@@ -113,7 +113,7 @@ def preferred_username_fallback(fingerprint: str) -> str:
|
|
|
113
113
|
"""Generate a stable display handle from a fingerprint when no name is asserted.
|
|
114
114
|
|
|
115
115
|
Args:
|
|
116
|
-
fingerprint: Full 40
|
|
116
|
+
fingerprint: Full PGP fingerprint: 40 (v4) or 64 (v6) hex.
|
|
117
117
|
|
|
118
118
|
Returns:
|
|
119
119
|
str: Short handle like ``capauth-8A3FC2D1``.
|