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)
A key-agreement secret, and the public key to hand to a peer.
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.
A message that has been sealed, with the header that travels beside it.
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.
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.
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.
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.
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.
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.