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 +62 -0
- keyctl2/_syscall.py +209 -0
- keyctl2/errors.py +16 -0
- keyctl2/flags.py +152 -0
- keyctl2/keys.py +296 -0
- keyctl2/py.typed +0 -0
- keyctl2-0.1.0.dist-info/METADATA +69 -0
- keyctl2-0.1.0.dist-info/RECORD +10 -0
- keyctl2-0.1.0.dist-info/WHEEL +4 -0
- keyctl2-0.1.0.dist-info/licenses/LICENSE +12 -0
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
|
+
[](https://github.com/Quad4-Software/keyctl2/actions/workflows/ci.yml)
|
|
32
|
+
[](https://github.com/Quad4-Software/keyctl2/actions/workflows/codeql.yml)
|
|
33
|
+
[](https://securityscorecards.dev/viewer/?uri=github.com/Quad4-Software/keyctl2)
|
|
34
|
+
[](https://pypi.org/project/keyctl2/)
|
|
35
|
+
[](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,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.
|