pamoja.security
Idiomatic device-identity facade.
Wraps the native pamoja._native.DeviceIdentity with a Python-native
surface: str or bytes payloads, properties instead of getters, and a
named constructor. It adds ergonomics only; the signing and verifying happen in
the Rust core.
1"""Idiomatic device-identity facade. 2 3Wraps the native :class:`pamoja._native.DeviceIdentity` with a Python-native 4surface: ``str`` or ``bytes`` payloads, properties instead of getters, and a 5named constructor. It adds ergonomics only; the signing and verifying happen in 6the Rust core. 7""" 8 9from __future__ import annotations 10 11from typing import Union 12 13from pamoja._native import DeviceIdentity as _NativeDeviceIdentity 14from pamoja._native import fingerprint as _native_fingerprint 15from pamoja._native import verify as _native_verify 16from pamoja._native import verify_message as _native_verify_message 17 18__all__ = ["DeviceIdentity", "Payload", "fingerprint", "verify", "verify_message"] 19 20#: A payload to sign or verify; ``str`` is encoded as UTF-8. 21Payload = Union[str, bytes, bytearray, memoryview] 22 23 24def _to_bytes(payload: Payload) -> bytes: 25 """Encode a payload to bytes, so callers may pass either text or raw data. 26 27 :param payload: The value to encode. 28 :returns: The payload as bytes. 29 """ 30 if isinstance(payload, str): 31 return payload.encode("utf-8") 32 return bytes(payload) 33 34 35class DeviceIdentity: 36 """A device's private signing identity. 37 38 A reading that drives a health or billing decision has to be provably from 39 the device that claims to have sent it, and provably unaltered on the way. 40 Sign it here, and any holder of :attr:`public_key` can check it with 41 :func:`verify`. 42 43 Example:: 44 45 device = DeviceIdentity.from_seed(seed) 46 signature = device.sign("21.5") 47 verify(device.public_key, "21.5", signature) # True 48 """ 49 50 __slots__ = ("_native",) 51 52 def __init__(self, seed: bytes) -> None: 53 """Create an identity from a provisioned 32-byte secret seed. 54 55 :param seed: The device's 32-byte secret, held on the device only. 56 :raises ValueError: If the seed is not exactly 32 bytes. 57 """ 58 self._native = _NativeDeviceIdentity(bytes(seed)) 59 60 @classmethod 61 def from_seed(cls, seed: bytes) -> "DeviceIdentity": 62 """Create an identity from a provisioned 32-byte secret seed. 63 64 :param seed: The device's 32-byte secret, held on the device only. 65 :returns: The identity that seed determines. 66 :raises ValueError: If the seed is not exactly 32 bytes. 67 """ 68 return cls(seed) 69 70 @staticmethod 71 def native(identity: "DeviceIdentity") -> _NativeDeviceIdentity: 72 """Hand the generated identity to another capability facade. 73 74 The audit and update capabilities sign with an identity this class 75 holds, and the generated bindings take the generated type. This is how 76 the two meet without a caller ever seeing it. 77 78 :param identity: The identity to unwrap. 79 :returns: The generated identity inside it. 80 """ 81 return identity._native 82 83 @property 84 def public_key(self) -> bytes: 85 """The 32-byte public key matching this identity, safe to share.""" 86 return self._native.public_key 87 88 @property 89 def fingerprint(self) -> str: 90 """A 16-character lowercase hex label for this identity.""" 91 return self._native.fingerprint 92 93 def sign(self, payload: Payload) -> bytes: 94 """Sign a payload. 95 96 :param payload: The bytes to cover; ``str`` is encoded as UTF-8. 97 :returns: The 64-byte detached signature. 98 """ 99 return self._native.sign(_to_bytes(payload)) 100 101 def sign_message(self, payload: Payload) -> bytes: 102 """Sign a payload and return one message carrying both. 103 104 The message is the signature followed by the payload, which is usually what 105 goes on a link: one blob to send, rather than a payload and a detached 106 signature to keep together and split correctly at the far end. 107 :func:`verify_message` reverses it. 108 109 :param payload: The bytes to cover; ``str`` is encoded as UTF-8. 110 :returns: The signature followed by the payload. 111 """ 112 return self._native.sign_message(_to_bytes(payload)) 113 114 def __repr__(self) -> str: 115 return f"DeviceIdentity(fingerprint={self.fingerprint!r})" 116 117 118def verify(public_key: bytes, payload: Payload, signature: bytes) -> bool: 119 """Verify that a signature covers a payload and was made by a public key. 120 121 :param public_key: The 32-byte public key of the claimed signer. 122 :param payload: The bytes the signature should cover. 123 :param signature: The 64-byte detached signature. 124 :returns: ``True`` if the signature is authentic, and ``False`` if the 125 payload was altered or was signed by a different device. 126 :raises ValueError: If an argument is not the expected length. 127 """ 128 return _native_verify(bytes(public_key), _to_bytes(payload), bytes(signature)) 129 130 131def verify_message(public_key: bytes, message: bytes) -> bytes | None: 132 """Verify a signed message and return the payload it carries. 133 134 :param public_key: The 32-byte public key of the claimed signer. 135 :param message: The signature followed by the payload, as 136 :meth:`DeviceIdentity.sign_message` wrote it. 137 :returns: The payload if the message is authentic, and ``None`` if it is too 138 short to hold a signature, was altered, or was signed by a different device. 139 :raises ValueError: If the key is not the expected length. 140 """ 141 return _native_verify_message(bytes(public_key), bytes(message)) 142 143 144def fingerprint(public_key: bytes) -> str: 145 """Return the short hex fingerprint of a public key. 146 147 :param public_key: The 32-byte public key to label. 148 :returns: A 16-character lowercase hex label. 149 :raises ValueError: If the key is not exactly 32 bytes. 150 """ 151 return _native_fingerprint(bytes(public_key))
36class DeviceIdentity: 37 """A device's private signing identity. 38 39 A reading that drives a health or billing decision has to be provably from 40 the device that claims to have sent it, and provably unaltered on the way. 41 Sign it here, and any holder of :attr:`public_key` can check it with 42 :func:`verify`. 43 44 Example:: 45 46 device = DeviceIdentity.from_seed(seed) 47 signature = device.sign("21.5") 48 verify(device.public_key, "21.5", signature) # True 49 """ 50 51 __slots__ = ("_native",) 52 53 def __init__(self, seed: bytes) -> None: 54 """Create an identity from a provisioned 32-byte secret seed. 55 56 :param seed: The device's 32-byte secret, held on the device only. 57 :raises ValueError: If the seed is not exactly 32 bytes. 58 """ 59 self._native = _NativeDeviceIdentity(bytes(seed)) 60 61 @classmethod 62 def from_seed(cls, seed: bytes) -> "DeviceIdentity": 63 """Create an identity from a provisioned 32-byte secret seed. 64 65 :param seed: The device's 32-byte secret, held on the device only. 66 :returns: The identity that seed determines. 67 :raises ValueError: If the seed is not exactly 32 bytes. 68 """ 69 return cls(seed) 70 71 @staticmethod 72 def native(identity: "DeviceIdentity") -> _NativeDeviceIdentity: 73 """Hand the generated identity to another capability facade. 74 75 The audit and update capabilities sign with an identity this class 76 holds, and the generated bindings take the generated type. This is how 77 the two meet without a caller ever seeing it. 78 79 :param identity: The identity to unwrap. 80 :returns: The generated identity inside it. 81 """ 82 return identity._native 83 84 @property 85 def public_key(self) -> bytes: 86 """The 32-byte public key matching this identity, safe to share.""" 87 return self._native.public_key 88 89 @property 90 def fingerprint(self) -> str: 91 """A 16-character lowercase hex label for this identity.""" 92 return self._native.fingerprint 93 94 def sign(self, payload: Payload) -> bytes: 95 """Sign a payload. 96 97 :param payload: The bytes to cover; ``str`` is encoded as UTF-8. 98 :returns: The 64-byte detached signature. 99 """ 100 return self._native.sign(_to_bytes(payload)) 101 102 def sign_message(self, payload: Payload) -> bytes: 103 """Sign a payload and return one message carrying both. 104 105 The message is the signature followed by the payload, which is usually what 106 goes on a link: one blob to send, rather than a payload and a detached 107 signature to keep together and split correctly at the far end. 108 :func:`verify_message` reverses it. 109 110 :param payload: The bytes to cover; ``str`` is encoded as UTF-8. 111 :returns: The signature followed by the payload. 112 """ 113 return self._native.sign_message(_to_bytes(payload)) 114 115 def __repr__(self) -> str: 116 return f"DeviceIdentity(fingerprint={self.fingerprint!r})"
A device's private signing identity.
A reading that drives a health or billing decision has to be provably from
the device that claims to have sent it, and provably unaltered on the way.
Sign it here, and any holder of public_key can check it with
verify().
Example::
device = DeviceIdentity.from_seed(seed)
signature = device.sign("21.5")
verify(device.public_key, "21.5", signature) # True
53 def __init__(self, seed: bytes) -> None: 54 """Create an identity from a provisioned 32-byte secret seed. 55 56 :param seed: The device's 32-byte secret, held on the device only. 57 :raises ValueError: If the seed is not exactly 32 bytes. 58 """ 59 self._native = _NativeDeviceIdentity(bytes(seed))
Create an identity from a provisioned 32-byte secret seed.
Parameters
- seed: The device's 32-byte secret, held on the device only.
Raises
- ValueError: If the seed is not exactly 32 bytes.
61 @classmethod 62 def from_seed(cls, seed: bytes) -> "DeviceIdentity": 63 """Create an identity from a provisioned 32-byte secret seed. 64 65 :param seed: The device's 32-byte secret, held on the device only. 66 :returns: The identity that seed determines. 67 :raises ValueError: If the seed is not exactly 32 bytes. 68 """ 69 return cls(seed)
Create an identity from a provisioned 32-byte secret seed.
Parameters
- seed: The device's 32-byte secret, held on the device only. :returns: The identity that seed determines.
Raises
- ValueError: If the seed is not exactly 32 bytes.
71 @staticmethod 72 def native(identity: "DeviceIdentity") -> _NativeDeviceIdentity: 73 """Hand the generated identity to another capability facade. 74 75 The audit and update capabilities sign with an identity this class 76 holds, and the generated bindings take the generated type. This is how 77 the two meet without a caller ever seeing it. 78 79 :param identity: The identity to unwrap. 80 :returns: The generated identity inside it. 81 """ 82 return identity._native
Hand the generated identity to another capability facade.
The audit and update capabilities sign with an identity this class holds, and the generated bindings take the generated type. This is how the two meet without a caller ever seeing it.
Parameters
- identity: The identity to unwrap. :returns: The generated identity inside it.
84 @property 85 def public_key(self) -> bytes: 86 """The 32-byte public key matching this identity, safe to share.""" 87 return self._native.public_key
The 32-byte public key matching this identity, safe to share.
89 @property 90 def fingerprint(self) -> str: 91 """A 16-character lowercase hex label for this identity.""" 92 return self._native.fingerprint
A 16-character lowercase hex label for this identity.
94 def sign(self, payload: Payload) -> bytes: 95 """Sign a payload. 96 97 :param payload: The bytes to cover; ``str`` is encoded as UTF-8. 98 :returns: The 64-byte detached signature. 99 """ 100 return self._native.sign(_to_bytes(payload))
Sign a payload.
Parameters
- payload: The bytes to cover;
stris encoded as UTF-8. :returns: The 64-byte detached signature.
102 def sign_message(self, payload: Payload) -> bytes: 103 """Sign a payload and return one message carrying both. 104 105 The message is the signature followed by the payload, which is usually what 106 goes on a link: one blob to send, rather than a payload and a detached 107 signature to keep together and split correctly at the far end. 108 :func:`verify_message` reverses it. 109 110 :param payload: The bytes to cover; ``str`` is encoded as UTF-8. 111 :returns: The signature followed by the payload. 112 """ 113 return self._native.sign_message(_to_bytes(payload))
Sign a payload and return one message carrying both.
The message is the signature followed by the payload, which is usually what
goes on a link: one blob to send, rather than a payload and a detached
signature to keep together and split correctly at the far end.
verify_message() reverses it.
Parameters
- payload: The bytes to cover;
stris encoded as UTF-8. :returns: The signature followed by the payload.
145def fingerprint(public_key: bytes) -> str: 146 """Return the short hex fingerprint of a public key. 147 148 :param public_key: The 32-byte public key to label. 149 :returns: A 16-character lowercase hex label. 150 :raises ValueError: If the key is not exactly 32 bytes. 151 """ 152 return _native_fingerprint(bytes(public_key))
Return the short hex fingerprint of a public key.
Parameters
- public_key: The 32-byte public key to label. :returns: A 16-character lowercase hex label.
Raises
- ValueError: If the key is not exactly 32 bytes.
119def verify(public_key: bytes, payload: Payload, signature: bytes) -> bool: 120 """Verify that a signature covers a payload and was made by a public key. 121 122 :param public_key: The 32-byte public key of the claimed signer. 123 :param payload: The bytes the signature should cover. 124 :param signature: The 64-byte detached signature. 125 :returns: ``True`` if the signature is authentic, and ``False`` if the 126 payload was altered or was signed by a different device. 127 :raises ValueError: If an argument is not the expected length. 128 """ 129 return _native_verify(bytes(public_key), _to_bytes(payload), bytes(signature))
Verify that a signature covers a payload and was made by a public key.
Parameters
- public_key: The 32-byte public key of the claimed signer.
- payload: The bytes the signature should cover.
- signature: The 64-byte detached signature.
:returns:
Trueif the signature is authentic, andFalseif the payload was altered or was signed by a different device.
Raises
- ValueError: If an argument is not the expected length.
132def verify_message(public_key: bytes, message: bytes) -> bytes | None: 133 """Verify a signed message and return the payload it carries. 134 135 :param public_key: The 32-byte public key of the claimed signer. 136 :param message: The signature followed by the payload, as 137 :meth:`DeviceIdentity.sign_message` wrote it. 138 :returns: The payload if the message is authentic, and ``None`` if it is too 139 short to hold a signature, was altered, or was signed by a different device. 140 :raises ValueError: If the key is not the expected length. 141 """ 142 return _native_verify_message(bytes(public_key), bytes(message))
Verify a signed message and return the payload it carries.
Parameters
- public_key: The 32-byte public key of the claimed signer.
- message: The signature followed by the payload, as
DeviceIdentity.sign_message()wrote it. :returns: The payload if the message is authentic, andNoneif it is too short to hold a signature, was altered, or was signed by a different device.
Raises
- ValueError: If the key is not the expected length.