keyctl2 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
keyctl2/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ """Python bindings for the Linux kernel key retention service.
3
+
4
+ Create and search keys and keyrings through add_key(2), request_key(2)
5
+ and keyctl(2) via ctypes; there are no runtime dependencies.
6
+
7
+ Kernel references: keyctl(2), add_key(2), request_key(2), keyrings(7).
8
+ """
9
+
10
+ from .errors import KeyctlError, UnsupportedError
11
+ from .flags import (
12
+ KeyctlOp,
13
+ KeyPerm,
14
+ KeySpec,
15
+ KeyType,
16
+ MoveFlag,
17
+ ReqKeyDefault,
18
+ )
19
+ from .keys import (
20
+ Key,
21
+ KeyDescription,
22
+ add_key,
23
+ capabilities,
24
+ clear,
25
+ get_keyring_id,
26
+ get_persistent,
27
+ join_session_keyring,
28
+ keyring,
29
+ request_key,
30
+ restrict,
31
+ search,
32
+ session_to_parent,
33
+ set_reqkey_keyring,
34
+ )
35
+
36
+ __version__ = "0.1.0"
37
+
38
+ __all__ = [
39
+ "Key",
40
+ "KeyDescription",
41
+ "KeyPerm",
42
+ "KeySpec",
43
+ "KeyType",
44
+ "KeyctlError",
45
+ "KeyctlOp",
46
+ "MoveFlag",
47
+ "ReqKeyDefault",
48
+ "UnsupportedError",
49
+ "__version__",
50
+ "add_key",
51
+ "capabilities",
52
+ "clear",
53
+ "get_keyring_id",
54
+ "get_persistent",
55
+ "join_session_keyring",
56
+ "keyring",
57
+ "request_key",
58
+ "restrict",
59
+ "search",
60
+ "session_to_parent",
61
+ "set_reqkey_keyring",
62
+ ]
keyctl2/_syscall.py ADDED
@@ -0,0 +1,209 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ """Raw ctypes bindings for add_key(2), request_key(2) and keyctl(2).
3
+
4
+ glibc has no wrappers for these syscalls on most architectures, so all
5
+ three are invoked through libc's syscall(2). The numbers differ per
6
+ architecture; the table covers the common ones and everything else gets
7
+ UnsupportedError.
8
+ """
9
+
10
+ import ctypes
11
+ import ctypes.util
12
+ import errno
13
+ import os
14
+ import platform
15
+ import sys
16
+ from typing import NoReturn
17
+
18
+ from .errors import KeyctlError, UnsupportedError
19
+ from .flags import KeyctlOp
20
+
21
+ _libc: ctypes.CDLL | None = None
22
+ _numbers: tuple[int, int, int] | None = None
23
+
24
+
25
+ def _get_libc() -> ctypes.CDLL:
26
+ global _libc
27
+ if _libc is None:
28
+ if sys.platform != "linux":
29
+ raise UnsupportedError("keyctl2 is only available on Linux")
30
+ name = ctypes.util.find_library("c")
31
+ _libc = ctypes.CDLL(name or None, use_errno=True)
32
+ _libc.syscall.restype = ctypes.c_long
33
+ return _libc
34
+
35
+
36
+ def _syscall_numbers() -> tuple[int, int, int]:
37
+ """Return the (add_key, request_key, keyctl) numbers for this arch."""
38
+ global _numbers
39
+ if _numbers is not None:
40
+ return _numbers
41
+ machine = platform.machine().lower()
42
+ if machine in ("x86_64", "amd64"):
43
+ _numbers = (248, 249, 250)
44
+ elif machine in ("aarch64", "arm64", "riscv64", "loongarch64"):
45
+ # asm-generic syscall table
46
+ _numbers = (217, 218, 219)
47
+ elif machine in ("i386", "i486", "i586", "i686", "x86"):
48
+ _numbers = (286, 287, 288)
49
+ else:
50
+ raise UnsupportedError(
51
+ f"no add_key/request_key/keyctl numbers for architecture {machine}"
52
+ )
53
+ return _numbers
54
+
55
+
56
+ def _raise_errno(err: int) -> NoReturn:
57
+ if err in (errno.ENOSYS, errno.EOPNOTSUPP):
58
+ raise UnsupportedError(err, os.strerror(err))
59
+ raise KeyctlError(err, os.strerror(err))
60
+
61
+
62
+ def _call(nr: int, *args: object) -> int:
63
+ ret = int(_get_libc().syscall(nr, *args))
64
+ if ret == -1:
65
+ _raise_errno(ctypes.get_errno())
66
+ return ret
67
+
68
+
69
+ def _keyctl(op: int, *args: object) -> int:
70
+ return _call(_syscall_numbers()[2], op, *args)
71
+
72
+
73
+ def _u32(value: int) -> ctypes.c_uint32:
74
+ # unsigned 32-bit argument, so -1 style sentinels marshal correctly
75
+ return ctypes.c_uint32(value & 0xFFFFFFFF)
76
+
77
+
78
+ def add_key(
79
+ type_: bytes,
80
+ description: bytes | None,
81
+ payload: bytes | None,
82
+ ringid: int,
83
+ ) -> int:
84
+ """add_key(2): create or update a key in ringid, return its serial."""
85
+ plen = 0 if payload is None else len(payload)
86
+ return _call(_syscall_numbers()[0], type_, description, payload, plen, ringid)
87
+
88
+
89
+ def request_key(
90
+ type_: bytes,
91
+ description: bytes,
92
+ callout_info: bytes | None,
93
+ ringid: int,
94
+ ) -> int:
95
+ """request_key(2): find or conjure a key, return its serial."""
96
+ return _call(_syscall_numbers()[1], type_, description, callout_info, ringid)
97
+
98
+
99
+ def get_keyring_id(ringid: int, create: bool) -> int:
100
+ """KEYCTL_GET_KEYRING_ID: resolve a key or special ID to a serial."""
101
+ return _keyctl(KeyctlOp.GET_KEYRING_ID, ringid, int(create))
102
+
103
+
104
+ def join_session_keyring(name: bytes | None) -> int:
105
+ """KEYCTL_JOIN_SESSION_KEYRING: return the joined ring's serial."""
106
+ return _keyctl(KeyctlOp.JOIN_SESSION_KEYRING, name)
107
+
108
+
109
+ def update(serial: int, payload: bytes | None) -> None:
110
+ """KEYCTL_UPDATE: replace a key's payload."""
111
+ plen = 0 if payload is None else len(payload)
112
+ _keyctl(KeyctlOp.UPDATE, serial, payload, plen)
113
+
114
+
115
+ def revoke(serial: int) -> None:
116
+ """KEYCTL_REVOKE: revoke a key."""
117
+ _keyctl(KeyctlOp.REVOKE, serial)
118
+
119
+
120
+ def chown(serial: int, uid: int, gid: int) -> None:
121
+ """KEYCTL_CHOWN: set ownership; -1 leaves a field unchanged."""
122
+ _keyctl(KeyctlOp.CHOWN, serial, _u32(uid), _u32(gid))
123
+
124
+
125
+ def setperm(serial: int, perm: int) -> None:
126
+ """KEYCTL_SETPERM: set a key's permission mask."""
127
+ _keyctl(KeyctlOp.SETPERM, serial, _u32(perm))
128
+
129
+
130
+ def describe(serial: int) -> bytes:
131
+ """KEYCTL_DESCRIBE: return the type;uid;gid;perm;description string."""
132
+ size = _keyctl(KeyctlOp.DESCRIBE, serial, None, 0)
133
+ buf = ctypes.create_string_buffer(size)
134
+ _keyctl(KeyctlOp.DESCRIBE, serial, buf, size)
135
+ return bytes(buf.value)
136
+
137
+
138
+ def clear(ringid: int) -> None:
139
+ """KEYCTL_CLEAR: remove all links from a keyring."""
140
+ _keyctl(KeyctlOp.CLEAR, ringid)
141
+
142
+
143
+ def link(serial: int, ringid: int) -> None:
144
+ """KEYCTL_LINK: link a key into a keyring."""
145
+ _keyctl(KeyctlOp.LINK, serial, ringid)
146
+
147
+
148
+ def unlink(serial: int, ringid: int) -> None:
149
+ """KEYCTL_UNLINK: remove a key's link from a keyring."""
150
+ _keyctl(KeyctlOp.UNLINK, serial, ringid)
151
+
152
+
153
+ def search(ringid: int, type_: bytes, description: bytes, destringid: int) -> int:
154
+ """KEYCTL_SEARCH: find a key in a ring, return its serial."""
155
+ return _keyctl(KeyctlOp.SEARCH, ringid, type_, description, destringid)
156
+
157
+
158
+ def read(serial: int) -> bytes:
159
+ """KEYCTL_READ: return a key's payload or a keyring's serial list."""
160
+ size = _keyctl(KeyctlOp.READ, serial, None, 0)
161
+ if size == 0:
162
+ return b""
163
+ buf = ctypes.create_string_buffer(size)
164
+ got = _keyctl(KeyctlOp.READ, serial, buf, size)
165
+ return buf.raw[:got]
166
+
167
+
168
+ def set_reqkey_keyring(default: int) -> int:
169
+ """KEYCTL_SET_REQKEY_KEYRING: set the request_key(2) default ring."""
170
+ return _keyctl(KeyctlOp.SET_REQKEY_KEYRING, default)
171
+
172
+
173
+ def set_timeout(serial: int, timeout: int) -> None:
174
+ """KEYCTL_SET_TIMEOUT: expire a key after timeout seconds."""
175
+ _keyctl(KeyctlOp.SET_TIMEOUT, serial, _u32(timeout))
176
+
177
+
178
+ def session_to_parent() -> None:
179
+ """KEYCTL_SESSION_TO_PARENT: install our session keyring on the parent."""
180
+ _keyctl(KeyctlOp.SESSION_TO_PARENT)
181
+
182
+
183
+ def invalidate(serial: int) -> None:
184
+ """KEYCTL_INVALIDATE: mark a key invalid."""
185
+ _keyctl(KeyctlOp.INVALIDATE, serial)
186
+
187
+
188
+ def get_persistent(uid: int, ringid: int) -> int:
189
+ """KEYCTL_GET_PERSISTENT: return a user's persistent keyring serial."""
190
+ return _keyctl(KeyctlOp.GET_PERSISTENT, _u32(uid), ringid)
191
+
192
+
193
+ def restrict_keyring(
194
+ ringid: int, type_: bytes | None, restriction: bytes | None
195
+ ) -> None:
196
+ """KEYCTL_RESTRICT_KEYRING: limit which keys may link to a ring."""
197
+ _keyctl(KeyctlOp.RESTRICT_KEYRING, ringid, type_, restriction)
198
+
199
+
200
+ def move(serial: int, from_ringid: int, to_ringid: int, flags: int) -> None:
201
+ """KEYCTL_MOVE: move a key's link between keyrings."""
202
+ _keyctl(KeyctlOp.MOVE, serial, from_ringid, to_ringid, _u32(flags))
203
+
204
+
205
+ def capabilities() -> bytes:
206
+ """KEYCTL_CAPABILITIES: return the subsystem capability bits."""
207
+ buf = ctypes.create_string_buffer(8)
208
+ got = _keyctl(KeyctlOp.CAPABILITIES, buf, len(buf))
209
+ return buf.raw[:got]
keyctl2/errors.py ADDED
@@ -0,0 +1,16 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ """Exception types raised by keyctl2."""
3
+
4
+
5
+ class KeyctlError(OSError):
6
+ """A key management syscall failed.
7
+
8
+ The errno attribute carries the kernel error. Common values are
9
+ ENOKEY for a missing or non-searchable key, EACCES for a revoked,
10
+ expired or inaccessible key, EDQUOT for an exceeded key quota and
11
+ EKEYEXPIRED or EKEYREVOKED for keys in those states.
12
+ """
13
+
14
+
15
+ class UnsupportedError(KeyctlError):
16
+ """The running kernel or architecture does not support the operation."""
keyctl2/flags.py ADDED
@@ -0,0 +1,152 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ """Constants for the kernel key retention service.
3
+
4
+ Values are from linux/keyctl.h and keyutils.h. See keyctl(2), keyrings(7)
5
+ and the kernel documentation under Documentation/security/keys/.
6
+ """
7
+
8
+ from enum import Enum, IntEnum, IntFlag
9
+
10
+ __all__ = [
11
+ "KeyPerm",
12
+ "KeySpec",
13
+ "KeyType",
14
+ "KeyctlOp",
15
+ "MoveFlag",
16
+ "ReqKeyDefault",
17
+ ]
18
+
19
+
20
+ class KeySpec(IntEnum):
21
+ """Special keyring IDs, resolved by the kernel per caller.
22
+
23
+ The negative values are passed to the kernel verbatim; they are not
24
+ real key serials.
25
+ """
26
+
27
+ THREAD_KEYRING = -1
28
+ PROCESS_KEYRING = -2
29
+ SESSION_KEYRING = -3
30
+ USER_KEYRING = -4
31
+ USER_SESSION_KEYRING = -5
32
+ GROUP_KEYRING = -6
33
+ REQKEY_AUTH_KEY = -7
34
+ REQUESTOR_KEYRING = -8
35
+
36
+ @classmethod
37
+ def from_name(cls, name: str) -> "KeySpec":
38
+ """Resolve a short name like "session" or "user" to a KeySpec."""
39
+ table = {
40
+ "thread": cls.THREAD_KEYRING,
41
+ "process": cls.PROCESS_KEYRING,
42
+ "session": cls.SESSION_KEYRING,
43
+ "user": cls.USER_KEYRING,
44
+ "user_session": cls.USER_SESSION_KEYRING,
45
+ "group": cls.GROUP_KEYRING,
46
+ "reqkey_auth": cls.REQKEY_AUTH_KEY,
47
+ "requestor": cls.REQUESTOR_KEYRING,
48
+ }
49
+ try:
50
+ return table[name]
51
+ except KeyError:
52
+ raise ValueError(f"unknown keyring name: {name!r}") from None
53
+
54
+
55
+ class KeyPerm(IntFlag):
56
+ """Key permission bits, one hex digit per class.
57
+
58
+ Each of the possessor, user, group and other classes holds the same
59
+ six bits: VIEW, READ, WRITE, SEARCH, LINK and SETATTR. The ALL masks
60
+ cover a whole class; KeyPerm.ALL covers everything.
61
+ """
62
+
63
+ NONE = 0
64
+ POS_VIEW = 0x01000000
65
+ POS_READ = 0x02000000
66
+ POS_WRITE = 0x04000000
67
+ POS_SEARCH = 0x08000000
68
+ POS_LINK = 0x10000000
69
+ POS_SETATTR = 0x20000000
70
+ POS_ALL = 0x3F000000
71
+ USR_VIEW = 0x00010000
72
+ USR_READ = 0x00020000
73
+ USR_WRITE = 0x00040000
74
+ USR_SEARCH = 0x00080000
75
+ USR_LINK = 0x00100000
76
+ USR_SETATTR = 0x00200000
77
+ USR_ALL = 0x003F0000
78
+ GRP_VIEW = 0x00000100
79
+ GRP_READ = 0x00000200
80
+ GRP_WRITE = 0x00000400
81
+ GRP_SEARCH = 0x00000800
82
+ GRP_LINK = 0x00001000
83
+ GRP_SETATTR = 0x00002000
84
+ GRP_ALL = 0x00003F00
85
+ OTH_VIEW = 0x00000001
86
+ OTH_READ = 0x00000002
87
+ OTH_WRITE = 0x00000004
88
+ OTH_SEARCH = 0x00000008
89
+ OTH_LINK = 0x00000010
90
+ OTH_SETATTR = 0x00000020
91
+ OTH_ALL = 0x0000003F
92
+ ALL = 0x3F3F3F3F
93
+
94
+
95
+ class KeyType(str, Enum):
96
+ """Common kernel key types.
97
+
98
+ Plain strings are accepted everywhere a KeyType is, so types not
99
+ listed here can still be used.
100
+ """
101
+
102
+ USER = "user"
103
+ LOGON = "logon"
104
+ KEYRING = "keyring"
105
+ BIG_KEY = "big_key"
106
+ ASYMMETRIC = "asymmetric"
107
+
108
+
109
+ class KeyctlOp(IntEnum):
110
+ """Operation codes for the keyctl(2) syscall."""
111
+
112
+ GET_KEYRING_ID = 0
113
+ JOIN_SESSION_KEYRING = 1
114
+ UPDATE = 2
115
+ REVOKE = 3
116
+ CHOWN = 4
117
+ SETPERM = 5
118
+ DESCRIBE = 6
119
+ CLEAR = 7
120
+ LINK = 8
121
+ UNLINK = 9
122
+ SEARCH = 10
123
+ READ = 11
124
+ SET_REQKEY_KEYRING = 14
125
+ SET_TIMEOUT = 15
126
+ SESSION_TO_PARENT = 18
127
+ INVALIDATE = 21
128
+ GET_PERSISTENT = 22
129
+ RESTRICT_KEYRING = 29
130
+ MOVE = 30
131
+ CAPABILITIES = 31
132
+
133
+
134
+ class MoveFlag(IntFlag):
135
+ """Flags for KEYCTL_MOVE."""
136
+
137
+ NONE = 0
138
+ EXCL = 0x00000001 # KEYCTL_MOVE_EXCL, do not displace a matching key
139
+
140
+
141
+ class ReqKeyDefault(IntEnum):
142
+ """Default destination keyring for request_key(2), KEY_REQKEY_DEFL_*."""
143
+
144
+ NO_CHANGE = -1
145
+ DEFAULT = 0
146
+ THREAD_KEYRING = 1
147
+ PROCESS_KEYRING = 2
148
+ SESSION_KEYRING = 3
149
+ USER_KEYRING = 4
150
+ USER_SESSION_KEYRING = 5
151
+ GROUP_KEYRING = 6
152
+ REQUESTOR_KEYRING = 7
keyctl2/keys.py ADDED
@@ -0,0 +1,296 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ """High-level interface to the kernel key retention service.
3
+
4
+ Serials returned by add_key() and friends are plain ints. Wrap one in a
5
+ Key to describe, read, update, link or revoke it. Keyring arguments
6
+ accept a serial, a KeySpec member or a short name like "session".
7
+
8
+ Kernel references: keyctl(2), add_key(2), request_key(2), keyrings(7).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from dataclasses import dataclass
15
+
16
+ from . import _syscall
17
+ from .flags import KeyPerm, KeySpec, KeyType, MoveFlag, ReqKeyDefault
18
+
19
+ __all__ = [
20
+ "Key",
21
+ "KeyDescription",
22
+ "add_key",
23
+ "capabilities",
24
+ "clear",
25
+ "get_keyring_id",
26
+ "get_persistent",
27
+ "join_session_keyring",
28
+ "keyring",
29
+ "request_key",
30
+ "restrict",
31
+ "search",
32
+ "session_to_parent",
33
+ "set_reqkey_keyring",
34
+ ]
35
+
36
+ _KeyringLike = int | KeySpec | str
37
+ _KeyTypeLike = str | KeyType
38
+
39
+
40
+ def _ring(ring: _KeyringLike) -> int:
41
+ if isinstance(ring, str):
42
+ return int(KeySpec.from_name(ring))
43
+ return int(ring)
44
+
45
+
46
+ def _type(type_: _KeyTypeLike) -> bytes:
47
+ if isinstance(type_, KeyType):
48
+ type_ = type_.value
49
+ return os.fsencode(type_)
50
+
51
+
52
+ def _desc(description: str) -> bytes:
53
+ data = os.fsencode(description)
54
+ if b"\x00" in data:
55
+ raise ValueError("description must not contain NUL")
56
+ return data
57
+
58
+
59
+ def add_key(
60
+ type_: _KeyTypeLike,
61
+ description: str,
62
+ payload: bytes | None = None,
63
+ keyring: _KeyringLike = KeySpec.SESSION_KEYRING,
64
+ ) -> int:
65
+ """Create a key in the given keyring and return its serial.
66
+
67
+ If a matching key already exists in the keyring its payload is
68
+ updated instead. EDQUOT means the caller's key quota is exhausted.
69
+ """
70
+ return _syscall.add_key(_type(type_), _desc(description), payload, _ring(keyring))
71
+
72
+
73
+ def request_key(
74
+ type_: _KeyTypeLike,
75
+ description: str,
76
+ keyring: _KeyringLike = KeySpec.SESSION_KEYRING,
77
+ callout_info: str | None = None,
78
+ ) -> int:
79
+ """Search the caller's keyrings for a key, return its serial.
80
+
81
+ If the key is not found and callout_info is given, the kernel asks
82
+ /sbin/request-key to instantiate it, passing callout_info along.
83
+ With callout_info=None a miss raises ENOKEY.
84
+ """
85
+ info = None if callout_info is None else os.fsencode(callout_info)
86
+ return _syscall.request_key(_type(type_), _desc(description), info, _ring(keyring))
87
+
88
+
89
+ def join_session_keyring(name: str | None = None) -> int:
90
+ """Join or create a session keyring, return its serial.
91
+
92
+ This replaces the calling process's session keyring. name=None
93
+ joins a fresh anonymous keyring.
94
+ """
95
+ return _syscall.join_session_keyring(None if name is None else os.fsencode(name))
96
+
97
+
98
+ def get_keyring_id(ring: _KeyringLike, create: bool = False) -> int:
99
+ """Resolve a key or special keyring ID to a real serial.
100
+
101
+ With create=True, special keyring IDs are created on demand.
102
+ """
103
+ return _syscall.get_keyring_id(_ring(ring), create)
104
+
105
+
106
+ def keyring(ring: _KeyringLike) -> Key:
107
+ """Wrap a keyring serial, KeySpec member or name in a Key."""
108
+ return Key(_ring(ring))
109
+
110
+
111
+ def clear(ring: _KeyringLike) -> None:
112
+ """Remove all links from a keyring."""
113
+ _syscall.clear(_ring(ring))
114
+
115
+
116
+ def search(
117
+ ring: _KeyringLike,
118
+ type_: _KeyTypeLike,
119
+ description: str,
120
+ dest: _KeyringLike = 0,
121
+ ) -> int:
122
+ """Search a keyring tree for a key, return its serial.
123
+
124
+ If dest is nonzero the found key is also linked into that keyring.
125
+ ENOKEY means no matching key was found or it was not searchable.
126
+ """
127
+ return _syscall.search(_ring(ring), _type(type_), _desc(description), _ring(dest))
128
+
129
+
130
+ def restrict(
131
+ ring: _KeyringLike,
132
+ type_: _KeyTypeLike | None = None,
133
+ restriction: str | None = None,
134
+ ) -> None:
135
+ """Restrict which keys may be linked into a keyring.
136
+
137
+ With type=None and restriction=None the ring rejects all further
138
+ links. A type may only be named if it implements a restriction
139
+ scheme, like "asymmetric" does; the restriction string then selects
140
+ the scheme, for example "builtin_trusted". Restriction is permanent.
141
+ """
142
+ _syscall.restrict_keyring(
143
+ _ring(ring),
144
+ None if type_ is None else _type(type_),
145
+ None if restriction is None else os.fsencode(restriction),
146
+ )
147
+
148
+
149
+ def session_to_parent() -> None:
150
+ """Install this process's session keyring on the parent process."""
151
+ _syscall.session_to_parent()
152
+
153
+
154
+ def get_persistent(uid: int, ring: _KeyringLike) -> int:
155
+ """Return a user's persistent keyring, linked into ring.
156
+
157
+ The persistent keyring survives logout for a kernel-configured
158
+ grace period. EOPNOTSUPP means the kernel lacks persistent ring
159
+ support.
160
+ """
161
+ return _syscall.get_persistent(uid, _ring(ring))
162
+
163
+
164
+ def set_reqkey_keyring(default: ReqKeyDefault) -> None:
165
+ """Set the default destination ring for request_key(2) calls."""
166
+ _syscall.set_reqkey_keyring(int(default))
167
+
168
+
169
+ def capabilities() -> bytes:
170
+ """Return the keyrings subsystem capability bits."""
171
+ return _syscall.capabilities()
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class KeyDescription:
176
+ """Parsed result of KEYCTL_DESCRIBE."""
177
+
178
+ type: str
179
+ uid: int
180
+ gid: int
181
+ perm: KeyPerm
182
+ description: str
183
+
184
+ @classmethod
185
+ def parse(cls, text: bytes) -> KeyDescription:
186
+ """Parse a type;uid;gid;perm;description string.
187
+
188
+ The description itself may contain semicolons, so at most four
189
+ fields are split off the front.
190
+ """
191
+ parts = text.split(b";", 4)
192
+ if len(parts) != 5:
193
+ raise ValueError(f"malformed describe string: {text!r}")
194
+ return cls(
195
+ type=parts[0].decode("ascii"),
196
+ uid=int(parts[1]),
197
+ gid=int(parts[2]),
198
+ perm=KeyPerm(int(parts[3], 16)),
199
+ description=os.fsdecode(parts[4]),
200
+ )
201
+
202
+
203
+ class Key:
204
+ """A key or keyring, referenced by serial or KeySpec member."""
205
+
206
+ __slots__ = ("_serial",)
207
+
208
+ def __init__(self, serial: int | KeySpec) -> None:
209
+ self._serial = int(serial)
210
+
211
+ @property
212
+ def serial(self) -> int:
213
+ """The key serial number, possibly a negative KeySpec value."""
214
+ return self._serial
215
+
216
+ def describe(self) -> KeyDescription:
217
+ """Fetch the key's attributes."""
218
+ return KeyDescription.parse(_syscall.describe(self._serial))
219
+
220
+ def read(self) -> bytes:
221
+ """Read the key's payload, or a keyring's serial list.
222
+
223
+ EKEYREVOKED, EKEYEXPIRED or EACCES mean the key can no longer be
224
+ read by this caller.
225
+ """
226
+ return _syscall.read(self._serial)
227
+
228
+ def update(self, payload: bytes | None) -> None:
229
+ """Replace the key's payload."""
230
+ _syscall.update(self._serial, payload)
231
+
232
+ def set_timeout(self, seconds: int) -> None:
233
+ """Make the key expire after the given number of seconds."""
234
+ if seconds < 0:
235
+ raise ValueError(f"timeout out of range: {seconds}")
236
+ _syscall.set_timeout(self._serial, seconds)
237
+
238
+ def set_perm(self, perm: KeyPerm) -> None:
239
+ """Set the key's permission mask."""
240
+ _syscall.setperm(self._serial, int(perm))
241
+
242
+ def link(self, ring: _KeyringLike) -> None:
243
+ """Link the key into a keyring."""
244
+ _syscall.link(self._serial, _ring(ring))
245
+
246
+ def unlink(self, ring: _KeyringLike) -> None:
247
+ """Remove the key's link from a keyring."""
248
+ _syscall.unlink(self._serial, _ring(ring))
249
+
250
+ def move(
251
+ self,
252
+ from_ring: _KeyringLike,
253
+ to_ring: _KeyringLike,
254
+ flags: MoveFlag = MoveFlag.NONE,
255
+ ) -> None:
256
+ """Atomically move the key's link between two keyrings.
257
+
258
+ With MoveFlag.EXCL, a matching key already in to_ring is kept
259
+ and the call fails with EEXIST instead of displacing it.
260
+ """
261
+ _syscall.move(self._serial, _ring(from_ring), _ring(to_ring), int(flags))
262
+
263
+ def revoke(self) -> None:
264
+ """Revoke the key, making it unusable."""
265
+ _syscall.revoke(self._serial)
266
+
267
+ def invalidate(self) -> None:
268
+ """Mark the key invalid, making it vanish promptly."""
269
+ _syscall.invalidate(self._serial)
270
+
271
+ def chown(self, uid: int = -1, gid: int = -1) -> None:
272
+ """Set the key's owner; -1 leaves a field unchanged."""
273
+ _syscall.chown(self._serial, uid, gid)
274
+
275
+ def search(
276
+ self,
277
+ type_: _KeyTypeLike,
278
+ description: str,
279
+ dest: _KeyringLike = 0,
280
+ ) -> int:
281
+ """Search this keyring for a key, return its serial."""
282
+ return search(self._serial, type_, description, dest)
283
+
284
+ def __int__(self) -> int:
285
+ return self._serial
286
+
287
+ def __repr__(self) -> str:
288
+ return f"{type(self).__name__}({self._serial})"
289
+
290
+ def __eq__(self, other: object) -> bool:
291
+ if isinstance(other, Key):
292
+ return self._serial == other._serial
293
+ return NotImplemented
294
+
295
+ def __hash__(self) -> int:
296
+ return hash(self._serial)
keyctl2/py.typed ADDED
File without changes
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.5
2
+ Name: keyctl2
3
+ Version: 0.1.0
4
+ Summary: Python bindings for the Linux kernel keyring
5
+ Project-URL: Homepage, https://quad4.io
6
+ Project-URL: Repository, https://github.com/Quad4-Software/keyctl2
7
+ Project-URL: Issues, https://github.com/Quad4-Software/keyctl2/issues
8
+ Project-URL: Changelog, https://github.com/Quad4-Software/keyctl2/blob/master/CHANGELOG.md
9
+ Author: Quad4
10
+ License-Expression: 0BSD
11
+ License-File: LICENSE
12
+ Keywords: keyctl,keyring,linux,secrets,security
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: System :: Operating System Kernels :: Linux
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+
29
+ # keyctl2
30
+
31
+ [![CI](https://github.com/Quad4-Software/keyctl2/actions/workflows/ci.yml/badge.svg)](https://github.com/Quad4-Software/keyctl2/actions/workflows/ci.yml)
32
+ [![CodeQL](https://github.com/Quad4-Software/keyctl2/actions/workflows/codeql.yml/badge.svg)](https://github.com/Quad4-Software/keyctl2/actions/workflows/codeql.yml)
33
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Quad4-Software/keyctl2/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Quad4-Software/keyctl2)
34
+ [![PyPI](https://img.shields.io/pypi/v/keyctl2.svg)](https://pypi.org/project/keyctl2/)
35
+ [![License: 0BSD](https://img.shields.io/badge/license-0BSD-blue)](LICENSE)
36
+
37
+ Dependency-free Python bindings for the Linux kernel keyring: add,
38
+ search, read, describe, link and unlink keys in session and process
39
+ keyrings, without shelling out to keyctl(1).
40
+
41
+ Requires Python 3.10+ and Linux. No runtime dependencies: the bindings
42
+ call add_key(2), request_key(2) and keyctl(2) through ctypes.
43
+
44
+ ## Install
45
+
46
+ pip install keyctl2
47
+
48
+ ## Example
49
+
50
+ import keyctl2
51
+
52
+ # Add a user key to the session keyring and read it back.
53
+ serial = keyctl2.add_key("user", "my-secret", b"hunter2")
54
+ key = keyctl2.Key(serial)
55
+ assert key.read() == b"hunter2"
56
+
57
+ # Search the session keyring for it later.
58
+ assert keyctl2.search("session", "user", "my-secret") == serial
59
+
60
+ # Inspect it and clean up.
61
+ print(key.describe()) # KeyDescription(type='user', ...)
62
+ key.unlink("session")
63
+
64
+ ## Development
65
+
66
+ uv sync --group dev
67
+ make check
68
+
69
+ License: 0BSD. Quad4 Software, https://quad4.io
@@ -0,0 +1,10 @@
1
+ keyctl2/__init__.py,sha256=BZQSSjXi6F1JtWY9lHVrK9PcUxwhcDYU9ZRmHn-ztj8,1192
2
+ keyctl2/_syscall.py,sha256=UyUluf3FWlQgbesF-vrHfzARdJTO4XVxaLOp-eyq7zk,6646
3
+ keyctl2/errors.py,sha256=GE9AtXAuBPJZ9uYKbI0of55A79FN6gTTNX-gRiflads,533
4
+ keyctl2/flags.py,sha256=Fb7Yk6SKttKILOoJaG9lwbzRsu2qzubscAa6ATUlAAs,3732
5
+ keyctl2/keys.py,sha256=IeIsnxXtrn2zSF0qUlhs16O4fJfM1FcwzK8qCiALVYg,8934
6
+ keyctl2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ keyctl2-0.1.0.dist-info/METADATA,sha256=h-UGB6wR5epE6BTDVOE6vJRDZFXThpqXVHLDxeszq7M,2756
8
+ keyctl2-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ keyctl2-0.1.0.dist-info/licenses/LICENSE,sha256=3Hnwsz5EXuTC9iTlGjzA171dKQtIVsgjFoF5DEMRl48,633
10
+ keyctl2-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2026 Quad4
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose
4
+ with or without fee is hereby granted.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
7
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
8
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
10
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
11
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
12
+ PERFORMANCE OF THIS SOFTWARE.