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))
class DeviceIdentity:
 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
DeviceIdentity(seed: bytes)
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.
@classmethod
def from_seed(cls, seed: bytes) -> DeviceIdentity:
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.
@staticmethod
def native(identity: DeviceIdentity) -> DeviceIdentity:
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.
public_key: bytes
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.

fingerprint: str
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.

def sign(self, payload: Union[str, bytes, bytearray, memoryview]) -> bytes:
 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; str is encoded as UTF-8. :returns: The 64-byte detached signature.
def sign_message(self, payload: Union[str, bytes, bytearray, memoryview]) -> bytes:
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; str is encoded as UTF-8. :returns: The signature followed by the payload.
Payload = typing.Union[str, bytes, bytearray, memoryview]
def fingerprint(public_key: bytes) -> str:
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.
def verify( public_key: bytes, payload: Union[str, bytes, bytearray, memoryview], signature: bytes) -> bool:
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: True if the signature is authentic, and False if the payload was altered or was signed by a different device.
Raises
  • ValueError: If an argument is not the expected length.
def verify_message(public_key: bytes, message: bytes) -> bytes | None:
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, and None if 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.