pamoja.session

Idiomatic secured-session facade.

Two devices that already know each other's public keys can agree on a session key without ever sending it, and then exchange messages that are confidential, cannot be altered undetected, and cannot be replayed. That is the whole of what a small device usually needs from transport security, at a fraction of what a TLS stack costs it.

  1"""Idiomatic secured-session facade.
  2
  3Two devices that already know each other's public keys can agree on a session key
  4without ever sending it, and then exchange messages that are confidential, cannot
  5be altered undetected, and cannot be replayed. That is the whole of what a small
  6device usually needs from transport security, at a fraction of what a TLS stack
  7costs it.
  8"""
  9
 10from __future__ import annotations
 11
 12import enum
 13
 14from pamoja._native import AgreementKey, SealedMessage
 15from pamoja._native import Session as _Session
 16from pamoja._native import hkdf_sha256_expand as _hkdf_sha256
 17from pamoja._native import hmac_sha256_digest as _hmac_sha256
 18
 19__all__ = [
 20    "AgreementKey",
 21    "Role",
 22    "SealedMessage",
 23    "Session",
 24    "hkdf_sha256",
 25    "hmac_sha256",
 26]
 27
 28
 29class Role(str, enum.Enum):
 30    """Which side of a session a device is on.
 31
 32    The two devices must choose opposite roles: the role decides the order the
 33    public keys are mixed in and which direction each side tags its messages
 34    with, so a session where both sides claim the same role opens nothing.
 35    """
 36
 37    #: The device that opens the session.
 38    INITIATOR = "Initiator"
 39    #: The device that answers.
 40    RESPONDER = "Responder"
 41
 42
 43class Session:
 44    """A confidential, tamper-evident, replay-protected channel with one peer."""
 45
 46    def __init__(
 47        self,
 48        local: AgreementKey,
 49        peer_public_key: bytes,
 50        salt: bytes,
 51        role: Role,
 52    ) -> None:
 53        """Establish a session with a peer.
 54
 55        :param local: This device's key-agreement secret.
 56        :param peer_public_key: The peer's 32-byte public key, already
 57            authenticated by pinning or by a signature.
 58        :param salt: A fresh per-session salt both sides share, exchanged in the
 59            clear. Reusing one with the same pair of keys reuses the session key,
 60            so it must change each session.
 61        :param role: Whether this device opens the session or answers.
 62        """
 63        self._inner = _Session(local, peer_public_key, salt, role.value)
 64
 65    def seal(self, plaintext: bytes, aad: bytes = b"") -> SealedMessage:
 66        """Seal a message for the peer.
 67
 68        :param plaintext: The message to protect.
 69        :param aad: Data authenticated but not encrypted, so it stays readable on
 70            the wire yet cannot be altered: a device identifier or a routing
 71            header belongs here.
 72        :returns: The ciphertext, with the counter and tag to send beside it.
 73        """
 74        return self._inner.seal(plaintext, aad)
 75
 76    def open(self, sealed: SealedMessage, aad: bytes = b"") -> bytes:
 77        """Open a message from the peer.
 78
 79        :param sealed: The ciphertext with the counter and tag that arrived with
 80            it.
 81        :param aad: The same associated data the sender authenticated.
 82        :returns: The plaintext.
 83        :raises PamojaError: When the counter repeats or is older than the replay
 84            window still tracks, and when the tag does not authenticate. Nothing
 85            readable is ever returned from a message that failed either check.
 86        """
 87        return self._inner.open(sealed, aad)
 88
 89
 90def hmac_sha256(key: bytes, message: bytes) -> bytes:
 91    """Compute a keyed hash over a message.
 92
 93    This is the primitive a host uses to authenticate a pairing exchange or a
 94    single command, where a whole session would be more than the job needs.
 95
 96    :param key: The secret key.
 97    :param message: The message to authenticate.
 98    :returns: The 32-byte digest.
 99    """
100    return _hmac_sha256(key, message)
101
102
103def hkdf_sha256(salt: bytes, ikm: bytes, info: bytes, length: int) -> bytes:
104    """Expand input keying material into ``length`` bytes bound to ``info``.
105
106    :param salt: The salt, which may be empty.
107    :param ikm: The input keying material.
108    :param info: Context binding the output to its purpose.
109    :param length: How many bytes to derive.
110    :returns: The derived bytes.
111    """
112    return _hkdf_sha256(salt, ikm, info, length)
class AgreementKey:

A key-agreement secret, and the public key to hand to a peer.

public_key

The public key to hand to a peer.

class Role(builtins.str, enum.Enum):
30class Role(str, enum.Enum):
31    """Which side of a session a device is on.
32
33    The two devices must choose opposite roles: the role decides the order the
34    public keys are mixed in and which direction each side tags its messages
35    with, so a session where both sides claim the same role opens nothing.
36    """
37
38    #: The device that opens the session.
39    INITIATOR = "Initiator"
40    #: The device that answers.
41    RESPONDER = "Responder"

Which side of a session a device is on.

The two devices must choose opposite roles: the role decides the order the public keys are mixed in and which direction each side tags its messages with, so a session where both sides claim the same role opens nothing.

INITIATOR = <Role.INITIATOR: 'Initiator'>
RESPONDER = <Role.RESPONDER: 'Responder'>
class SealedMessage:

A message that has been sealed, with the header that travels beside it.

tag

The tag over the ciphertext and its associated data.

ciphertext

The encrypted message.

counter

The counter naming this message within the session.

class Session:
44class Session:
45    """A confidential, tamper-evident, replay-protected channel with one peer."""
46
47    def __init__(
48        self,
49        local: AgreementKey,
50        peer_public_key: bytes,
51        salt: bytes,
52        role: Role,
53    ) -> None:
54        """Establish a session with a peer.
55
56        :param local: This device's key-agreement secret.
57        :param peer_public_key: The peer's 32-byte public key, already
58            authenticated by pinning or by a signature.
59        :param salt: A fresh per-session salt both sides share, exchanged in the
60            clear. Reusing one with the same pair of keys reuses the session key,
61            so it must change each session.
62        :param role: Whether this device opens the session or answers.
63        """
64        self._inner = _Session(local, peer_public_key, salt, role.value)
65
66    def seal(self, plaintext: bytes, aad: bytes = b"") -> SealedMessage:
67        """Seal a message for the peer.
68
69        :param plaintext: The message to protect.
70        :param aad: Data authenticated but not encrypted, so it stays readable on
71            the wire yet cannot be altered: a device identifier or a routing
72            header belongs here.
73        :returns: The ciphertext, with the counter and tag to send beside it.
74        """
75        return self._inner.seal(plaintext, aad)
76
77    def open(self, sealed: SealedMessage, aad: bytes = b"") -> bytes:
78        """Open a message from the peer.
79
80        :param sealed: The ciphertext with the counter and tag that arrived with
81            it.
82        :param aad: The same associated data the sender authenticated.
83        :returns: The plaintext.
84        :raises PamojaError: When the counter repeats or is older than the replay
85            window still tracks, and when the tag does not authenticate. Nothing
86            readable is ever returned from a message that failed either check.
87        """
88        return self._inner.open(sealed, aad)

A confidential, tamper-evident, replay-protected channel with one peer.

Session( local: AgreementKey, peer_public_key: bytes, salt: bytes, role: Role)
47    def __init__(
48        self,
49        local: AgreementKey,
50        peer_public_key: bytes,
51        salt: bytes,
52        role: Role,
53    ) -> None:
54        """Establish a session with a peer.
55
56        :param local: This device's key-agreement secret.
57        :param peer_public_key: The peer's 32-byte public key, already
58            authenticated by pinning or by a signature.
59        :param salt: A fresh per-session salt both sides share, exchanged in the
60            clear. Reusing one with the same pair of keys reuses the session key,
61            so it must change each session.
62        :param role: Whether this device opens the session or answers.
63        """
64        self._inner = _Session(local, peer_public_key, salt, role.value)

Establish a session with a peer.

Parameters
  • local: This device's key-agreement secret.
  • peer_public_key: The peer's 32-byte public key, already authenticated by pinning or by a signature.
  • salt: A fresh per-session salt both sides share, exchanged in the clear. Reusing one with the same pair of keys reuses the session key, so it must change each session.
  • role: Whether this device opens the session or answers.
def seal(self, plaintext: bytes, aad: bytes = b'') -> SealedMessage:
66    def seal(self, plaintext: bytes, aad: bytes = b"") -> SealedMessage:
67        """Seal a message for the peer.
68
69        :param plaintext: The message to protect.
70        :param aad: Data authenticated but not encrypted, so it stays readable on
71            the wire yet cannot be altered: a device identifier or a routing
72            header belongs here.
73        :returns: The ciphertext, with the counter and tag to send beside it.
74        """
75        return self._inner.seal(plaintext, aad)

Seal a message for the peer.

Parameters
  • plaintext: The message to protect.
  • aad: Data authenticated but not encrypted, so it stays readable on the wire yet cannot be altered: a device identifier or a routing header belongs here. :returns: The ciphertext, with the counter and tag to send beside it.
def open(self, sealed: SealedMessage, aad: bytes = b'') -> bytes:
77    def open(self, sealed: SealedMessage, aad: bytes = b"") -> bytes:
78        """Open a message from the peer.
79
80        :param sealed: The ciphertext with the counter and tag that arrived with
81            it.
82        :param aad: The same associated data the sender authenticated.
83        :returns: The plaintext.
84        :raises PamojaError: When the counter repeats or is older than the replay
85            window still tracks, and when the tag does not authenticate. Nothing
86            readable is ever returned from a message that failed either check.
87        """
88        return self._inner.open(sealed, aad)

Open a message from the peer.

Parameters
  • sealed: The ciphertext with the counter and tag that arrived with it.
  • aad: The same associated data the sender authenticated. :returns: The plaintext.
Raises
  • PamojaError: When the counter repeats or is older than the replay window still tracks, and when the tag does not authenticate. Nothing readable is ever returned from a message that failed either check.
def hkdf_sha256(salt: bytes, ikm: bytes, info: bytes, length: int) -> bytes:
104def hkdf_sha256(salt: bytes, ikm: bytes, info: bytes, length: int) -> bytes:
105    """Expand input keying material into ``length`` bytes bound to ``info``.
106
107    :param salt: The salt, which may be empty.
108    :param ikm: The input keying material.
109    :param info: Context binding the output to its purpose.
110    :param length: How many bytes to derive.
111    :returns: The derived bytes.
112    """
113    return _hkdf_sha256(salt, ikm, info, length)

Expand input keying material into length bytes bound to info.

Parameters
  • salt: The salt, which may be empty.
  • ikm: The input keying material.
  • info: Context binding the output to its purpose.
  • length: How many bytes to derive. :returns: The derived bytes.
def hmac_sha256(key: bytes, message: bytes) -> bytes:
 91def hmac_sha256(key: bytes, message: bytes) -> bytes:
 92    """Compute a keyed hash over a message.
 93
 94    This is the primitive a host uses to authenticate a pairing exchange or a
 95    single command, where a whole session would be more than the job needs.
 96
 97    :param key: The secret key.
 98    :param message: The message to authenticate.
 99    :returns: The 32-byte digest.
100    """
101    return _hmac_sha256(key, message)

Compute a keyed hash over a message.

This is the primitive a host uses to authenticate a pairing exchange or a single command, where a whole session would be more than the job needs.

Parameters
  • key: The secret key.
  • message: The message to authenticate. :returns: The 32-byte digest.