tibet-keychain 0.1.0__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.
- tibet_keychain-0.1.0/LICENSE +1 -0
- tibet_keychain-0.1.0/PKG-INFO +196 -0
- tibet_keychain-0.1.0/README.md +161 -0
- tibet_keychain-0.1.0/pyproject.toml +61 -0
- tibet_keychain-0.1.0/setup.cfg +4 -0
- tibet_keychain-0.1.0/src/tibet_keychain/__init__.py +43 -0
- tibet_keychain-0.1.0/src/tibet_keychain/cli.py +88 -0
- tibet_keychain-0.1.0/src/tibet_keychain/types.py +120 -0
- tibet_keychain-0.1.0/src/tibet_keychain.egg-info/PKG-INFO +196 -0
- tibet_keychain-0.1.0/src/tibet_keychain.egg-info/SOURCES.txt +12 -0
- tibet_keychain-0.1.0/src/tibet_keychain.egg-info/dependency_links.txt +1 -0
- tibet_keychain-0.1.0/src/tibet_keychain.egg-info/entry_points.txt +3 -0
- tibet_keychain-0.1.0/src/tibet_keychain.egg-info/requires.txt +11 -0
- tibet_keychain-0.1.0/src/tibet_keychain.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT License — Copyright (c) 2026 Humotica, Jasper van de Meent, Root AI, Codex
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tibet-keychain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Causal-aware secret custody — where secrets live, how they moved, who touched them. Part of the TIBET vault family (tibet-vault = WHEN, tibet-keychain = WHERE/HOW, tibet-sam = WHY, tibet-gateway = where ACT is performed).
|
|
5
|
+
Author-email: Jasper van de Meent <info@humotica.com>, Root AI <root_idd@humotica.nl>, Codex <codex@humotica.nl>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://humotica.com
|
|
8
|
+
Project-URL: Repository, https://github.com/jaspertvdm/tibet-keychain
|
|
9
|
+
Keywords: tibet,keychain,secret-custody,vault,credential,causal,audit,rotation,exposure,cbom
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security :: Cryptography
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: cryptography>=42.0
|
|
27
|
+
Provides-Extra: sealed
|
|
28
|
+
Requires-Dist: tibet-drop>=0.3.0; extra == "sealed"
|
|
29
|
+
Provides-Extra: cbom
|
|
30
|
+
Requires-Dist: tibet-cbom>=0.1.1; extra == "cbom"
|
|
31
|
+
Provides-Extra: full
|
|
32
|
+
Requires-Dist: tibet-drop>=0.3.0; extra == "full"
|
|
33
|
+
Requires-Dist: tibet-cbom>=0.1.1; extra == "full"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# tibet-keychain
|
|
37
|
+
|
|
38
|
+
> **Causal-aware secret custody — where secrets live, how they moved, who touched them.**
|
|
39
|
+
|
|
40
|
+
`tibet-keychain` is part of the **TIBET vault family** — four primitives
|
|
41
|
+
that answer four different questions about secrets, keys, tokens, and
|
|
42
|
+
credentials:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
═══════════════════════════════════════════════════════════════
|
|
46
|
+
THE VAULT FAMILY
|
|
47
|
+
═══════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
tibet-vault WHEN temporal trigger
|
|
50
|
+
"release on date / dead-man-switch"
|
|
51
|
+
|
|
52
|
+
tibet-keychain WHERE/HOW custody + timeline ← this package
|
|
53
|
+
"where this secret lives, how it moved"
|
|
54
|
+
|
|
55
|
+
tibet-sam WHY intent + scope authorization
|
|
56
|
+
"why this one specific act is allowed"
|
|
57
|
+
|
|
58
|
+
tibet-gateway WHERE-EXEC execution boundary
|
|
59
|
+
"where the act is safely performed"
|
|
60
|
+
═══════════════════════════════════════════════════════════════
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Why a separate keychain primitive?
|
|
64
|
+
|
|
65
|
+
Traditional secret stores answer:
|
|
66
|
+
|
|
67
|
+
- where is the key
|
|
68
|
+
- can this caller read it
|
|
69
|
+
|
|
70
|
+
`tibet-keychain` answers:
|
|
71
|
+
|
|
72
|
+
- where is the key (still)
|
|
73
|
+
- **how did it get here** (= causal history)
|
|
74
|
+
- **who touched it** (= custody timeline)
|
|
75
|
+
- **was it exposed** (= rotation triggers)
|
|
76
|
+
- **how does the chain walk back** (= CBOM-compatible)
|
|
77
|
+
|
|
78
|
+
That makes `tibet-keychain` the natural foundation for any system
|
|
79
|
+
that has to prove not only "I have the key" but "I know exactly
|
|
80
|
+
how this key entered my custody, who touched it on the way, and
|
|
81
|
+
under which authority each transition happened".
|
|
82
|
+
|
|
83
|
+
## Core idea
|
|
84
|
+
|
|
85
|
+
Each secret is stored as a sealed `.tza` continuity object with:
|
|
86
|
+
|
|
87
|
+
- encrypted secret payload (= the actual material)
|
|
88
|
+
- vault-metadata (issuer / scope / created_at / expires_at)
|
|
89
|
+
- custody-transition (owner ↔ custodian ↔ active-operator)
|
|
90
|
+
- exposure_state + rotation_required flag
|
|
91
|
+
- timeline of all `secret-*` events
|
|
92
|
+
|
|
93
|
+
The secret material itself is encrypted and tightly scoped. The
|
|
94
|
+
surrounding continuity metadata remains auditable.
|
|
95
|
+
|
|
96
|
+
## Secret types
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
api_key oauth_token signing_key
|
|
100
|
+
service_account_cred pypi_token crates_token
|
|
101
|
+
github_pat ssh_key root_password
|
|
102
|
+
smart_contract_signer tls_cert webhook_signer
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Timeline events
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
secret-created secret-imported secret-sealed
|
|
109
|
+
secret-unsealed secret-proxied secret-delegated
|
|
110
|
+
secret-exposed secret-rotated secret-revoked
|
|
111
|
+
secret-archived
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each event carries:
|
|
115
|
+
|
|
116
|
+
- actor identity (= who)
|
|
117
|
+
- action_id + parent_action_id (= chain link)
|
|
118
|
+
- timestamp
|
|
119
|
+
- actor class (= human / machine / external / mcp-server / gateway / system)
|
|
120
|
+
- relevant policy decisions (= scope / authority-shift)
|
|
121
|
+
|
|
122
|
+
## Coupling with the rest of the family
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
tibet-keychain
|
|
126
|
+
(custody, timeline)
|
|
127
|
+
│
|
|
128
|
+
↓ secret-proxied event
|
|
129
|
+
│
|
|
130
|
+
┌───────────────────────┴───────────────────────┐
|
|
131
|
+
▼ ▼
|
|
132
|
+
tibet-sam tibet-gateway
|
|
133
|
+
(why this act is OK) (where it happens)
|
|
134
|
+
↓ ↑
|
|
135
|
+
intent + scope break-seal,
|
|
136
|
+
constraint sealed execute,
|
|
137
|
+
in one-shot .tza destroy-session
|
|
138
|
+
↓ ↑
|
|
139
|
+
└─────────── handed to ──────────────────────────┘
|
|
140
|
+
↓
|
|
141
|
+
tibet-continuityd
|
|
142
|
+
audit JSONL
|
|
143
|
+
↓
|
|
144
|
+
tibet-cbom
|
|
145
|
+
timeline walk
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Why this beats traditional secret stores
|
|
149
|
+
|
|
150
|
+
| Property | HashiCorp Vault | tibet-keychain |
|
|
151
|
+
|--------------------------------|-----------------|----------------|
|
|
152
|
+
| Encrypted at rest | ✓ | ✓ |
|
|
153
|
+
| ACL / policy controlled | ✓ | ✓ |
|
|
154
|
+
| Causal history of secret | ✗ | ✓ |
|
|
155
|
+
| Custody-transition chain | ✗ | ✓ |
|
|
156
|
+
| Exposure events recorded | partial | ✓ |
|
|
157
|
+
| Audit walkable cross-bundle | ✗ | ✓ |
|
|
158
|
+
| Identity-bound rotation chain | ✗ | ✓ |
|
|
159
|
+
| Regulator-auditable timeline | partial | ✓ |
|
|
160
|
+
|
|
161
|
+
## Use cases
|
|
162
|
+
|
|
163
|
+
**Infrastructure**: SSH keys, database credentials, root access —
|
|
164
|
+
all stored with custody chain, rotated through signed transitions.
|
|
165
|
+
|
|
166
|
+
**Agent workflows**: PyPI tokens, crates.io tokens, GitHub PATs —
|
|
167
|
+
agents never see them; they request a SAM capsule that the gateway
|
|
168
|
+
executes against, the keychain logs the request.
|
|
169
|
+
|
|
170
|
+
**Smart contracts**: signing keys with explicit "who can request a
|
|
171
|
+
signing capsule for which contract address" policy, every signing
|
|
172
|
+
event in the timeline.
|
|
173
|
+
|
|
174
|
+
**Compliance evidence**: regulator asks "who could have signed this?
|
|
175
|
+
who actually did?" — the keychain timeline answers both.
|
|
176
|
+
|
|
177
|
+
## Install
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install tibet-keychain[full]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
This pulls `tibet-drop` (= sealed envelope substrate) and `tibet-cbom`
|
|
184
|
+
(= timeline renderer) as soft dependencies.
|
|
185
|
+
|
|
186
|
+
## Status
|
|
187
|
+
|
|
188
|
+
**v0.1.0** — package skeleton + spec. Implementation track for full
|
|
189
|
+
secret storage + sealed-bundle integration follows; designed to land
|
|
190
|
+
in time for IETF spec referencing and Marco van Hurne / W3C demos.
|
|
191
|
+
|
|
192
|
+
Spec source: `/srv/jtel-stack/hersenspinsels/tibet-vault-key-custody-and-sealed-secret-timeline-2026-05-12.md`
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT — Humotica + Root AI + Codex (2026)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# tibet-keychain
|
|
2
|
+
|
|
3
|
+
> **Causal-aware secret custody — where secrets live, how they moved, who touched them.**
|
|
4
|
+
|
|
5
|
+
`tibet-keychain` is part of the **TIBET vault family** — four primitives
|
|
6
|
+
that answer four different questions about secrets, keys, tokens, and
|
|
7
|
+
credentials:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
═══════════════════════════════════════════════════════════════
|
|
11
|
+
THE VAULT FAMILY
|
|
12
|
+
═══════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
tibet-vault WHEN temporal trigger
|
|
15
|
+
"release on date / dead-man-switch"
|
|
16
|
+
|
|
17
|
+
tibet-keychain WHERE/HOW custody + timeline ← this package
|
|
18
|
+
"where this secret lives, how it moved"
|
|
19
|
+
|
|
20
|
+
tibet-sam WHY intent + scope authorization
|
|
21
|
+
"why this one specific act is allowed"
|
|
22
|
+
|
|
23
|
+
tibet-gateway WHERE-EXEC execution boundary
|
|
24
|
+
"where the act is safely performed"
|
|
25
|
+
═══════════════════════════════════════════════════════════════
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why a separate keychain primitive?
|
|
29
|
+
|
|
30
|
+
Traditional secret stores answer:
|
|
31
|
+
|
|
32
|
+
- where is the key
|
|
33
|
+
- can this caller read it
|
|
34
|
+
|
|
35
|
+
`tibet-keychain` answers:
|
|
36
|
+
|
|
37
|
+
- where is the key (still)
|
|
38
|
+
- **how did it get here** (= causal history)
|
|
39
|
+
- **who touched it** (= custody timeline)
|
|
40
|
+
- **was it exposed** (= rotation triggers)
|
|
41
|
+
- **how does the chain walk back** (= CBOM-compatible)
|
|
42
|
+
|
|
43
|
+
That makes `tibet-keychain` the natural foundation for any system
|
|
44
|
+
that has to prove not only "I have the key" but "I know exactly
|
|
45
|
+
how this key entered my custody, who touched it on the way, and
|
|
46
|
+
under which authority each transition happened".
|
|
47
|
+
|
|
48
|
+
## Core idea
|
|
49
|
+
|
|
50
|
+
Each secret is stored as a sealed `.tza` continuity object with:
|
|
51
|
+
|
|
52
|
+
- encrypted secret payload (= the actual material)
|
|
53
|
+
- vault-metadata (issuer / scope / created_at / expires_at)
|
|
54
|
+
- custody-transition (owner ↔ custodian ↔ active-operator)
|
|
55
|
+
- exposure_state + rotation_required flag
|
|
56
|
+
- timeline of all `secret-*` events
|
|
57
|
+
|
|
58
|
+
The secret material itself is encrypted and tightly scoped. The
|
|
59
|
+
surrounding continuity metadata remains auditable.
|
|
60
|
+
|
|
61
|
+
## Secret types
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
api_key oauth_token signing_key
|
|
65
|
+
service_account_cred pypi_token crates_token
|
|
66
|
+
github_pat ssh_key root_password
|
|
67
|
+
smart_contract_signer tls_cert webhook_signer
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Timeline events
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
secret-created secret-imported secret-sealed
|
|
74
|
+
secret-unsealed secret-proxied secret-delegated
|
|
75
|
+
secret-exposed secret-rotated secret-revoked
|
|
76
|
+
secret-archived
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Each event carries:
|
|
80
|
+
|
|
81
|
+
- actor identity (= who)
|
|
82
|
+
- action_id + parent_action_id (= chain link)
|
|
83
|
+
- timestamp
|
|
84
|
+
- actor class (= human / machine / external / mcp-server / gateway / system)
|
|
85
|
+
- relevant policy decisions (= scope / authority-shift)
|
|
86
|
+
|
|
87
|
+
## Coupling with the rest of the family
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
tibet-keychain
|
|
91
|
+
(custody, timeline)
|
|
92
|
+
│
|
|
93
|
+
↓ secret-proxied event
|
|
94
|
+
│
|
|
95
|
+
┌───────────────────────┴───────────────────────┐
|
|
96
|
+
▼ ▼
|
|
97
|
+
tibet-sam tibet-gateway
|
|
98
|
+
(why this act is OK) (where it happens)
|
|
99
|
+
↓ ↑
|
|
100
|
+
intent + scope break-seal,
|
|
101
|
+
constraint sealed execute,
|
|
102
|
+
in one-shot .tza destroy-session
|
|
103
|
+
↓ ↑
|
|
104
|
+
└─────────── handed to ──────────────────────────┘
|
|
105
|
+
↓
|
|
106
|
+
tibet-continuityd
|
|
107
|
+
audit JSONL
|
|
108
|
+
↓
|
|
109
|
+
tibet-cbom
|
|
110
|
+
timeline walk
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Why this beats traditional secret stores
|
|
114
|
+
|
|
115
|
+
| Property | HashiCorp Vault | tibet-keychain |
|
|
116
|
+
|--------------------------------|-----------------|----------------|
|
|
117
|
+
| Encrypted at rest | ✓ | ✓ |
|
|
118
|
+
| ACL / policy controlled | ✓ | ✓ |
|
|
119
|
+
| Causal history of secret | ✗ | ✓ |
|
|
120
|
+
| Custody-transition chain | ✗ | ✓ |
|
|
121
|
+
| Exposure events recorded | partial | ✓ |
|
|
122
|
+
| Audit walkable cross-bundle | ✗ | ✓ |
|
|
123
|
+
| Identity-bound rotation chain | ✗ | ✓ |
|
|
124
|
+
| Regulator-auditable timeline | partial | ✓ |
|
|
125
|
+
|
|
126
|
+
## Use cases
|
|
127
|
+
|
|
128
|
+
**Infrastructure**: SSH keys, database credentials, root access —
|
|
129
|
+
all stored with custody chain, rotated through signed transitions.
|
|
130
|
+
|
|
131
|
+
**Agent workflows**: PyPI tokens, crates.io tokens, GitHub PATs —
|
|
132
|
+
agents never see them; they request a SAM capsule that the gateway
|
|
133
|
+
executes against, the keychain logs the request.
|
|
134
|
+
|
|
135
|
+
**Smart contracts**: signing keys with explicit "who can request a
|
|
136
|
+
signing capsule for which contract address" policy, every signing
|
|
137
|
+
event in the timeline.
|
|
138
|
+
|
|
139
|
+
**Compliance evidence**: regulator asks "who could have signed this?
|
|
140
|
+
who actually did?" — the keychain timeline answers both.
|
|
141
|
+
|
|
142
|
+
## Install
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install tibet-keychain[full]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This pulls `tibet-drop` (= sealed envelope substrate) and `tibet-cbom`
|
|
149
|
+
(= timeline renderer) as soft dependencies.
|
|
150
|
+
|
|
151
|
+
## Status
|
|
152
|
+
|
|
153
|
+
**v0.1.0** — package skeleton + spec. Implementation track for full
|
|
154
|
+
secret storage + sealed-bundle integration follows; designed to land
|
|
155
|
+
in time for IETF spec referencing and Marco van Hurne / W3C demos.
|
|
156
|
+
|
|
157
|
+
Spec source: `/srv/jtel-stack/hersenspinsels/tibet-vault-key-custody-and-sealed-secret-timeline-2026-05-12.md`
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT — Humotica + Root AI + Codex (2026)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tibet-keychain"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Causal-aware secret custody — where secrets live, how they moved, who touched them. Part of the TIBET vault family (tibet-vault = WHEN, tibet-keychain = WHERE/HOW, tibet-sam = WHY, tibet-gateway = where ACT is performed)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Jasper van de Meent", email = "info@humotica.com"},
|
|
14
|
+
{name = "Root AI", email = "root_idd@humotica.nl"},
|
|
15
|
+
{name = "Codex", email = "codex@humotica.nl"},
|
|
16
|
+
]
|
|
17
|
+
keywords = [
|
|
18
|
+
"tibet", "keychain", "secret-custody", "vault", "credential",
|
|
19
|
+
"causal", "audit", "rotation", "exposure", "cbom",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Intended Audience :: Information Technology",
|
|
25
|
+
"Intended Audience :: System Administrators",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Security :: Cryptography",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"cryptography>=42.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
# Bridge to tibet-drop for sealed envelope storage
|
|
42
|
+
sealed = [
|
|
43
|
+
"tibet-drop>=0.3.0",
|
|
44
|
+
]
|
|
45
|
+
# Bridge to tibet-cbom for timeline rendering
|
|
46
|
+
cbom = [
|
|
47
|
+
"tibet-cbom>=0.1.1",
|
|
48
|
+
]
|
|
49
|
+
# Full stack
|
|
50
|
+
full = [
|
|
51
|
+
"tibet-drop>=0.3.0",
|
|
52
|
+
"tibet-cbom>=0.1.1",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
tibet-keychain = "tibet_keychain.cli:main"
|
|
57
|
+
tkc = "tibet_keychain.cli:main"
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://humotica.com"
|
|
61
|
+
Repository = "https://github.com/jaspertvdm/tibet-keychain"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tibet-keychain — Causal-aware secret custody.
|
|
3
|
+
|
|
4
|
+
Part of the TIBET vault family:
|
|
5
|
+
tibet-vault WHEN temporal trigger
|
|
6
|
+
tibet-keychain WHERE/HOW custody + timeline ← this package
|
|
7
|
+
tibet-sam WHY intent + scope
|
|
8
|
+
tibet-gateway WHERE-EXEC execution boundary
|
|
9
|
+
|
|
10
|
+
Naming decision Jasper van de Meent 12 May 2026:
|
|
11
|
+
"tibet-vault = when
|
|
12
|
+
tibet-keychain = where/how secret lives
|
|
13
|
+
tibet-sam = why this one act is allowed
|
|
14
|
+
tibet-gateway = where the act is performed safely"
|
|
15
|
+
|
|
16
|
+
Spec source:
|
|
17
|
+
/srv/jtel-stack/hersenspinsels/tibet-vault-key-custody-and-sealed-secret-timeline-2026-05-12.md
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .types import (
|
|
22
|
+
SecretType,
|
|
23
|
+
SecretTimelineEvent,
|
|
24
|
+
ActorClass,
|
|
25
|
+
ExposureState,
|
|
26
|
+
SecretRecord,
|
|
27
|
+
CustodyTransition,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
__author__ = "Jasper van de Meent, Root AI, Codex"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"__version__",
|
|
37
|
+
"SecretType",
|
|
38
|
+
"SecretTimelineEvent",
|
|
39
|
+
"ActorClass",
|
|
40
|
+
"ExposureState",
|
|
41
|
+
"SecretRecord",
|
|
42
|
+
"CustodyTransition",
|
|
43
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tibet-keychain CLI — list / show / events / explain.
|
|
3
|
+
|
|
4
|
+
This v0.1.0 CLI is type-only: it inspects records and prints
|
|
5
|
+
human-readable views. Storage backend, sealed-bundle integration,
|
|
6
|
+
and rotation flow follow in v0.2+ once design lands with tibet-sam
|
|
7
|
+
and tibet-gateway.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from . import __version__, SecretType, SecretTimelineEvent, ActorClass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cmd_types(args):
|
|
19
|
+
"""List the vocabulary."""
|
|
20
|
+
print(f"tibet-keychain {__version__}")
|
|
21
|
+
print()
|
|
22
|
+
print(f"Secret types ({len(list(SecretType))}):")
|
|
23
|
+
for t in SecretType:
|
|
24
|
+
print(f" {t.value}")
|
|
25
|
+
print()
|
|
26
|
+
print(f"Timeline events ({len(list(SecretTimelineEvent))}):")
|
|
27
|
+
for e in SecretTimelineEvent:
|
|
28
|
+
print(f" {e.value}")
|
|
29
|
+
print()
|
|
30
|
+
print(f"Actor classes ({len(list(ActorClass))}):")
|
|
31
|
+
for a in ActorClass:
|
|
32
|
+
print(f" {a.value}")
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cmd_family(args):
|
|
37
|
+
"""Print the vault family overview."""
|
|
38
|
+
print("""
|
|
39
|
+
══════════════════════════════════════════════════════════════════
|
|
40
|
+
THE TIBET VAULT FAMILY
|
|
41
|
+
══════════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
tibet-vault WHEN temporal trigger
|
|
44
|
+
"release on date / dead-man-switch"
|
|
45
|
+
|
|
46
|
+
tibet-keychain WHERE/HOW custody + timeline ← you are here
|
|
47
|
+
"where this secret lives, how it moved"
|
|
48
|
+
|
|
49
|
+
tibet-sam WHY intent + scope authorization
|
|
50
|
+
"why this one specific act is allowed"
|
|
51
|
+
|
|
52
|
+
tibet-gateway WHERE-EXEC execution boundary
|
|
53
|
+
"where the act is safely performed"
|
|
54
|
+
|
|
55
|
+
══════════════════════════════════════════════════════════════════
|
|
56
|
+
""")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv=None):
|
|
61
|
+
parser = argparse.ArgumentParser(
|
|
62
|
+
prog="tibet-keychain",
|
|
63
|
+
description=(
|
|
64
|
+
"Causal-aware secret custody. Part of the TIBET vault "
|
|
65
|
+
"family (vault / keychain / sam / gateway)."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"-V", "--version", action="version",
|
|
70
|
+
version=f"tibet-keychain {__version__}",
|
|
71
|
+
)
|
|
72
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
73
|
+
|
|
74
|
+
p_types = sub.add_parser("types", help="List vocabulary (secret types, events, actor classes)")
|
|
75
|
+
p_types.set_defaults(func=_cmd_types)
|
|
76
|
+
|
|
77
|
+
p_family = sub.add_parser("family", help="Show the TIBET vault family overview")
|
|
78
|
+
p_family.set_defaults(func=_cmd_family)
|
|
79
|
+
|
|
80
|
+
args = parser.parse_args(argv)
|
|
81
|
+
if not args.cmd:
|
|
82
|
+
parser.print_help()
|
|
83
|
+
return 0
|
|
84
|
+
return args.func(args)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
sys.exit(main())
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core types for tibet-keychain.
|
|
3
|
+
|
|
4
|
+
Defines the vocabulary used across the package: secret types, timeline
|
|
5
|
+
event types, actor classes, exposure states, and the two main records
|
|
6
|
+
(SecretRecord and CustodyTransition).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SecretType(str, Enum):
|
|
16
|
+
"""Recognised secret-material classes.
|
|
17
|
+
|
|
18
|
+
The TIBET vault family does not invent new credential formats —
|
|
19
|
+
it provides a custody substrate around existing ones.
|
|
20
|
+
"""
|
|
21
|
+
API_KEY = "api_key"
|
|
22
|
+
OAUTH_TOKEN = "oauth_token"
|
|
23
|
+
SIGNING_KEY = "signing_key"
|
|
24
|
+
SERVICE_ACCOUNT_CREDENTIAL = "service_account_credential"
|
|
25
|
+
PYPI_TOKEN = "pypi_token"
|
|
26
|
+
CRATES_TOKEN = "crates_token"
|
|
27
|
+
GITHUB_PAT = "github_pat"
|
|
28
|
+
SSH_KEY = "ssh_key"
|
|
29
|
+
ROOT_PASSWORD = "root_password"
|
|
30
|
+
SMART_CONTRACT_SIGNER = "smart_contract_signer"
|
|
31
|
+
TLS_CERT = "tls_cert"
|
|
32
|
+
WEBHOOK_SIGNER = "webhook_signer"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SecretTimelineEvent(str, Enum):
|
|
36
|
+
"""Every meaningful state change a secret can go through.
|
|
37
|
+
|
|
38
|
+
The keychain's value is precisely that every one of these events
|
|
39
|
+
is recorded with actor / authority / parent_action_id, making the
|
|
40
|
+
custody chain walkable from any point in time.
|
|
41
|
+
"""
|
|
42
|
+
CREATED = "secret-created"
|
|
43
|
+
IMPORTED = "secret-imported"
|
|
44
|
+
SEALED = "secret-sealed"
|
|
45
|
+
UNSEALED = "secret-unsealed"
|
|
46
|
+
PROXIED = "secret-proxied"
|
|
47
|
+
DELEGATED = "secret-delegated"
|
|
48
|
+
EXPOSED = "secret-exposed"
|
|
49
|
+
ROTATED = "secret-rotated"
|
|
50
|
+
REVOKED = "secret-revoked"
|
|
51
|
+
ARCHIVED = "secret-archived"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ActorClass(str, Enum):
|
|
55
|
+
"""Who touched the secret. Not every actor is equal.
|
|
56
|
+
|
|
57
|
+
A timeline that records HUMAN unsealing has different policy
|
|
58
|
+
implications than one that records GATEWAY proxying.
|
|
59
|
+
"""
|
|
60
|
+
HUMAN = "human"
|
|
61
|
+
MACHINE = "machine"
|
|
62
|
+
EXTERNAL = "external"
|
|
63
|
+
MCP_SERVER = "mcp-server"
|
|
64
|
+
GATEWAY = "gateway"
|
|
65
|
+
SYSTEM = "system"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ExposureState(str, Enum):
|
|
69
|
+
"""Current exposure assessment of the secret material."""
|
|
70
|
+
SEALED = "sealed"
|
|
71
|
+
IN_USE = "in-use"
|
|
72
|
+
CHAT_DISCLOSED = "chat-disclosed"
|
|
73
|
+
GIT_LEAKED = "git-leaked"
|
|
74
|
+
LOG_LEAKED = "log-leaked"
|
|
75
|
+
SUSPECTED = "suspected"
|
|
76
|
+
ROTATED = "rotated"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class SecretRecord:
|
|
81
|
+
"""The metadata view of a secret in the keychain.
|
|
82
|
+
|
|
83
|
+
The actual secret material lives inside a sealed `.tza` payload
|
|
84
|
+
block; this record is the auditable metadata projection.
|
|
85
|
+
"""
|
|
86
|
+
secret_id: str
|
|
87
|
+
secret_type: SecretType
|
|
88
|
+
issuer: str # who minted the underlying credential
|
|
89
|
+
scope: str # what it is allowed to authorise
|
|
90
|
+
created_at: str # RFC3339
|
|
91
|
+
expires_at: Optional[str] = None
|
|
92
|
+
owner_id: Optional[str] = None # JIS-DID of nominal owner
|
|
93
|
+
custodian_id: Optional[str] = None # who currently holds it
|
|
94
|
+
active_operator_id: Optional[str] = None # who is allowed to use it
|
|
95
|
+
last_access_action_id: Optional[str] = None
|
|
96
|
+
last_rotation_action_id: Optional[str] = None
|
|
97
|
+
exposure_state: ExposureState = ExposureState.SEALED
|
|
98
|
+
rotation_required: bool = False
|
|
99
|
+
notes: list[str] = field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class CustodyTransition:
|
|
104
|
+
"""A single transition in a secret's custody timeline.
|
|
105
|
+
|
|
106
|
+
Mirrors the shape of tibet-cbom ownership-transition events so
|
|
107
|
+
that keychain transitions can be walked by `tcbom timeline`
|
|
108
|
+
when an audit-file is supplied.
|
|
109
|
+
"""
|
|
110
|
+
action_id: str
|
|
111
|
+
parent_action_id: Optional[str]
|
|
112
|
+
secret_id: str
|
|
113
|
+
event: SecretTimelineEvent
|
|
114
|
+
actor_id: str
|
|
115
|
+
actor_class: ActorClass
|
|
116
|
+
timestamp: str # RFC3339
|
|
117
|
+
reason: str = ""
|
|
118
|
+
authority_mode: str = "system" # agent / admin / triage / shared / system
|
|
119
|
+
target_assignee: Optional[str] = None
|
|
120
|
+
policy_lane: Optional[str] = None
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tibet-keychain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Causal-aware secret custody — where secrets live, how they moved, who touched them. Part of the TIBET vault family (tibet-vault = WHEN, tibet-keychain = WHERE/HOW, tibet-sam = WHY, tibet-gateway = where ACT is performed).
|
|
5
|
+
Author-email: Jasper van de Meent <info@humotica.com>, Root AI <root_idd@humotica.nl>, Codex <codex@humotica.nl>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://humotica.com
|
|
8
|
+
Project-URL: Repository, https://github.com/jaspertvdm/tibet-keychain
|
|
9
|
+
Keywords: tibet,keychain,secret-custody,vault,credential,causal,audit,rotation,exposure,cbom
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security :: Cryptography
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: cryptography>=42.0
|
|
27
|
+
Provides-Extra: sealed
|
|
28
|
+
Requires-Dist: tibet-drop>=0.3.0; extra == "sealed"
|
|
29
|
+
Provides-Extra: cbom
|
|
30
|
+
Requires-Dist: tibet-cbom>=0.1.1; extra == "cbom"
|
|
31
|
+
Provides-Extra: full
|
|
32
|
+
Requires-Dist: tibet-drop>=0.3.0; extra == "full"
|
|
33
|
+
Requires-Dist: tibet-cbom>=0.1.1; extra == "full"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# tibet-keychain
|
|
37
|
+
|
|
38
|
+
> **Causal-aware secret custody — where secrets live, how they moved, who touched them.**
|
|
39
|
+
|
|
40
|
+
`tibet-keychain` is part of the **TIBET vault family** — four primitives
|
|
41
|
+
that answer four different questions about secrets, keys, tokens, and
|
|
42
|
+
credentials:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
═══════════════════════════════════════════════════════════════
|
|
46
|
+
THE VAULT FAMILY
|
|
47
|
+
═══════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
tibet-vault WHEN temporal trigger
|
|
50
|
+
"release on date / dead-man-switch"
|
|
51
|
+
|
|
52
|
+
tibet-keychain WHERE/HOW custody + timeline ← this package
|
|
53
|
+
"where this secret lives, how it moved"
|
|
54
|
+
|
|
55
|
+
tibet-sam WHY intent + scope authorization
|
|
56
|
+
"why this one specific act is allowed"
|
|
57
|
+
|
|
58
|
+
tibet-gateway WHERE-EXEC execution boundary
|
|
59
|
+
"where the act is safely performed"
|
|
60
|
+
═══════════════════════════════════════════════════════════════
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Why a separate keychain primitive?
|
|
64
|
+
|
|
65
|
+
Traditional secret stores answer:
|
|
66
|
+
|
|
67
|
+
- where is the key
|
|
68
|
+
- can this caller read it
|
|
69
|
+
|
|
70
|
+
`tibet-keychain` answers:
|
|
71
|
+
|
|
72
|
+
- where is the key (still)
|
|
73
|
+
- **how did it get here** (= causal history)
|
|
74
|
+
- **who touched it** (= custody timeline)
|
|
75
|
+
- **was it exposed** (= rotation triggers)
|
|
76
|
+
- **how does the chain walk back** (= CBOM-compatible)
|
|
77
|
+
|
|
78
|
+
That makes `tibet-keychain` the natural foundation for any system
|
|
79
|
+
that has to prove not only "I have the key" but "I know exactly
|
|
80
|
+
how this key entered my custody, who touched it on the way, and
|
|
81
|
+
under which authority each transition happened".
|
|
82
|
+
|
|
83
|
+
## Core idea
|
|
84
|
+
|
|
85
|
+
Each secret is stored as a sealed `.tza` continuity object with:
|
|
86
|
+
|
|
87
|
+
- encrypted secret payload (= the actual material)
|
|
88
|
+
- vault-metadata (issuer / scope / created_at / expires_at)
|
|
89
|
+
- custody-transition (owner ↔ custodian ↔ active-operator)
|
|
90
|
+
- exposure_state + rotation_required flag
|
|
91
|
+
- timeline of all `secret-*` events
|
|
92
|
+
|
|
93
|
+
The secret material itself is encrypted and tightly scoped. The
|
|
94
|
+
surrounding continuity metadata remains auditable.
|
|
95
|
+
|
|
96
|
+
## Secret types
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
api_key oauth_token signing_key
|
|
100
|
+
service_account_cred pypi_token crates_token
|
|
101
|
+
github_pat ssh_key root_password
|
|
102
|
+
smart_contract_signer tls_cert webhook_signer
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Timeline events
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
secret-created secret-imported secret-sealed
|
|
109
|
+
secret-unsealed secret-proxied secret-delegated
|
|
110
|
+
secret-exposed secret-rotated secret-revoked
|
|
111
|
+
secret-archived
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each event carries:
|
|
115
|
+
|
|
116
|
+
- actor identity (= who)
|
|
117
|
+
- action_id + parent_action_id (= chain link)
|
|
118
|
+
- timestamp
|
|
119
|
+
- actor class (= human / machine / external / mcp-server / gateway / system)
|
|
120
|
+
- relevant policy decisions (= scope / authority-shift)
|
|
121
|
+
|
|
122
|
+
## Coupling with the rest of the family
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
tibet-keychain
|
|
126
|
+
(custody, timeline)
|
|
127
|
+
│
|
|
128
|
+
↓ secret-proxied event
|
|
129
|
+
│
|
|
130
|
+
┌───────────────────────┴───────────────────────┐
|
|
131
|
+
▼ ▼
|
|
132
|
+
tibet-sam tibet-gateway
|
|
133
|
+
(why this act is OK) (where it happens)
|
|
134
|
+
↓ ↑
|
|
135
|
+
intent + scope break-seal,
|
|
136
|
+
constraint sealed execute,
|
|
137
|
+
in one-shot .tza destroy-session
|
|
138
|
+
↓ ↑
|
|
139
|
+
└─────────── handed to ──────────────────────────┘
|
|
140
|
+
↓
|
|
141
|
+
tibet-continuityd
|
|
142
|
+
audit JSONL
|
|
143
|
+
↓
|
|
144
|
+
tibet-cbom
|
|
145
|
+
timeline walk
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Why this beats traditional secret stores
|
|
149
|
+
|
|
150
|
+
| Property | HashiCorp Vault | tibet-keychain |
|
|
151
|
+
|--------------------------------|-----------------|----------------|
|
|
152
|
+
| Encrypted at rest | ✓ | ✓ |
|
|
153
|
+
| ACL / policy controlled | ✓ | ✓ |
|
|
154
|
+
| Causal history of secret | ✗ | ✓ |
|
|
155
|
+
| Custody-transition chain | ✗ | ✓ |
|
|
156
|
+
| Exposure events recorded | partial | ✓ |
|
|
157
|
+
| Audit walkable cross-bundle | ✗ | ✓ |
|
|
158
|
+
| Identity-bound rotation chain | ✗ | ✓ |
|
|
159
|
+
| Regulator-auditable timeline | partial | ✓ |
|
|
160
|
+
|
|
161
|
+
## Use cases
|
|
162
|
+
|
|
163
|
+
**Infrastructure**: SSH keys, database credentials, root access —
|
|
164
|
+
all stored with custody chain, rotated through signed transitions.
|
|
165
|
+
|
|
166
|
+
**Agent workflows**: PyPI tokens, crates.io tokens, GitHub PATs —
|
|
167
|
+
agents never see them; they request a SAM capsule that the gateway
|
|
168
|
+
executes against, the keychain logs the request.
|
|
169
|
+
|
|
170
|
+
**Smart contracts**: signing keys with explicit "who can request a
|
|
171
|
+
signing capsule for which contract address" policy, every signing
|
|
172
|
+
event in the timeline.
|
|
173
|
+
|
|
174
|
+
**Compliance evidence**: regulator asks "who could have signed this?
|
|
175
|
+
who actually did?" — the keychain timeline answers both.
|
|
176
|
+
|
|
177
|
+
## Install
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install tibet-keychain[full]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
This pulls `tibet-drop` (= sealed envelope substrate) and `tibet-cbom`
|
|
184
|
+
(= timeline renderer) as soft dependencies.
|
|
185
|
+
|
|
186
|
+
## Status
|
|
187
|
+
|
|
188
|
+
**v0.1.0** — package skeleton + spec. Implementation track for full
|
|
189
|
+
secret storage + sealed-bundle integration follows; designed to land
|
|
190
|
+
in time for IETF spec referencing and Marco van Hurne / W3C demos.
|
|
191
|
+
|
|
192
|
+
Spec source: `/srv/jtel-stack/hersenspinsels/tibet-vault-key-custody-and-sealed-secret-timeline-2026-05-12.md`
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT — Humotica + Root AI + Codex (2026)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/tibet_keychain/__init__.py
|
|
5
|
+
src/tibet_keychain/cli.py
|
|
6
|
+
src/tibet_keychain/types.py
|
|
7
|
+
src/tibet_keychain.egg-info/PKG-INFO
|
|
8
|
+
src/tibet_keychain.egg-info/SOURCES.txt
|
|
9
|
+
src/tibet_keychain.egg-info/dependency_links.txt
|
|
10
|
+
src/tibet_keychain.egg-info/entry_points.txt
|
|
11
|
+
src/tibet_keychain.egg-info/requires.txt
|
|
12
|
+
src/tibet_keychain.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tibet_keychain
|