aiocoap.oscore module

This module contains the tools to send OSCORE secured messages.

It only deals with the algorithmic parts, the security context and protection and unprotection of messages. It does not touch on the integration of OSCORE in the larger aiocoap stack of having a context or requests; that’s what aiocoap.transports.osore is for.`

class aiocoap.oscore.CodeStyle(request, response)

Bases: _CodeStyle

classmethod from_request(request) CodeStyle
FETCH_CONTENT = (<Request Code 5 "FETCH">, <Successful Response Code 69 "2.05 Content">)
POST_CHANGED = (<Request Code 2 "POST">, <Successful Response Code 68 "2.04 Changed">)
exception aiocoap.oscore.NotAProtectedMessage(message, plain_message)

Bases: Error, ValueError

Raised when verification is attempted on a non-OSCORE message

exception aiocoap.oscore.ProtectionInvalid

Bases: Error, ValueError

Raised when verification of an OSCORE message fails

exception aiocoap.oscore.DecodeError

Bases: ProtectionInvalid

Raised when verification of an OSCORE message fails because CBOR or compressed data were erroneous

exception aiocoap.oscore.ReplayError

Bases: ProtectionInvalid

Raised when verification of an OSCORE message fails because the sequence numbers was already used

exception aiocoap.oscore.ReplayErrorWithEcho(secctx, request_id, echo)

Bases: ProtectionInvalid, RenderableError

Raised when verification of an OSCORE message fails because the recipient replay window is uninitialized, but a 4.01 Echo can be constructed with the data in the exception that can lead to the client assisting in replay window recovery

to_message()

Create a CoAP message that should be sent when this exception is rendered

exception aiocoap.oscore.ContextUnavailable

Bases: Error, ValueError

Raised when a context is (currently or permanently) unavailable for protecting or unprotecting a message

class aiocoap.oscore.RequestIdentifiers(kid, partial_iv, nonce, can_reuse_nonce, request_code)

Bases: object

A container for details that need to be passed along from the (un)protection of a request to the (un)protection of the response; these data ensure that the request-response binding process works by passing around the request’s partial IV.

Users of this module should never create or interact with instances, but just pass them around.

get_reusable_nonce_and_piv()

Return the nonce and the partial IV if can_reuse_nonce is True, and set can_reuse_nonce to False.

class aiocoap.oscore.AeadAlgorithm

Bases: object

abstract encrypt(plaintext, aad, key, iv)

Return ciphertext + tag for given input data

abstract decrypt(ciphertext_and_tag, aad, key, iv)

Reverse encryption. Must raise ProtectionInvalid on any error stemming from untrusted data.

class aiocoap.oscore.AES_CCM

Bases: AeadAlgorithm

AES-CCM implemented using the Python cryptography library

classmethod encrypt(plaintext, aad, key, iv)

Return ciphertext + tag for given input data

classmethod decrypt(ciphertext_and_tag, aad, key, iv)

Reverse encryption. Must raise ProtectionInvalid on any error stemming from untrusted data.

class aiocoap.oscore.AES_CCM_16_64_128

Bases: AES_CCM

value = 10
key_bytes = 16
tag_bytes = 8
iv_bytes = 13
class aiocoap.oscore.AES_CCM_16_64_256

Bases: AES_CCM

value = 11
key_bytes = 32
tag_bytes = 8
iv_bytes = 13
class aiocoap.oscore.AES_CCM_64_64_128

Bases: AES_CCM

value = 12
key_bytes = 16
tag_bytes = 8
iv_bytes = 7
class aiocoap.oscore.AES_CCM_64_64_256

Bases: AES_CCM

value = 13
key_bytes = 32
tag_bytes = 8
iv_bytes = 7
class aiocoap.oscore.AES_CCM_16_128_128

Bases: AES_CCM

value = 30
key_bytes = 16
tag_bytes = 16
iv_bytes = 13
class aiocoap.oscore.AES_CCM_16_128_256

Bases: AES_CCM

value = 31
key_bytes = 32
tag_bytes = 16
iv_bytes = 13
class aiocoap.oscore.AES_CCM_64_128_128

Bases: AES_CCM

value = 32
key_bytes = 16
tag_bytes = 16
iv_bytes = 7
class aiocoap.oscore.AES_CCM_64_128_256

Bases: AES_CCM

value = 33
key_bytes = 32
tag_bytes = 16
iv_bytes = 7
class aiocoap.oscore.AES_GCM

Bases: AeadAlgorithm

AES-GCM implemented using the Python cryptography library

iv_bytes = 12
classmethod encrypt(plaintext, aad, key, iv)

Return ciphertext + tag for given input data

classmethod decrypt(ciphertext_and_tag, aad, key, iv)

Reverse encryption. Must raise ProtectionInvalid on any error stemming from untrusted data.

class aiocoap.oscore.A128GCM

Bases: AES_GCM

value = 1
key_bytes = 16
tag_bytes = 16
class aiocoap.oscore.A192GCM

Bases: AES_GCM

value = 2
key_bytes = 24
tag_bytes = 16
class aiocoap.oscore.A256GCM

Bases: AES_GCM

value = 3
key_bytes = 32
tag_bytes = 16
class aiocoap.oscore.ChaCha20Poly1305

Bases: AeadAlgorithm

value = 24
key_bytes = 32
tag_bytes = 16
iv_bytes = 12
classmethod encrypt(plaintext, aad, key, iv)

Return ciphertext + tag for given input data

classmethod decrypt(ciphertext_and_tag, aad, key, iv)

Reverse encryption. Must raise ProtectionInvalid on any error stemming from untrusted data.

class aiocoap.oscore.AlgorithmCountersign

Bases: object

A fully parameterized COSE countersign algorithm

An instance is able to provide all the alg_signature, par_countersign and par_countersign_key parameters taht go into the Group OSCORE algorithms field.

abstract sign(body, external_aad, private_key)

Return the signature produced by the key when using CounterSignature0 as describe in draft-ietf-cose-countersign-01

abstract verify(signature, body, external_aad, public_key)

Verify a signature in analogy to sign

abstract generate()

Return a usable private key

abstract public_from_private(private_key)

Given a private key, derive the publishable key

abstract property signature_length

The length of a signature using this algorithm

abstract property curve_number

Registered curve number used with this algorithm.

Only used for verification of credentials’ details

class aiocoap.oscore.AlgorithmStaticStatic

Bases: object

abstract staticstatic(private_key, public_key)

Derive a shared static-static secret from a private and a public key

class aiocoap.oscore.Ed25519

Bases: AlgorithmCountersign

sign(body, aad, private_key)

Return the signature produced by the key when using CounterSignature0 as describe in draft-ietf-cose-countersign-01

verify(signature, body, aad, public_key)

Verify a signature in analogy to sign

generate()

Return a usable private key

public_from_private(private_key)

Given a private key, derive the publishable key

value = -8
curve_number = 6
signature_length = 64
class aiocoap.oscore.EcdhSsHkdf256

Bases: AlgorithmStaticStatic

value = -27
generate()

Return a usable private key

public_from_private(private_key)

Given a private key, derive the publishable key

staticstatic(private_key, public_key)

Derive a shared static-static secret from a private and a public key

class aiocoap.oscore.ECDSA_SHA256_P256

Bases: AlgorithmCountersign, AlgorithmStaticStatic

from_public_parts(x: bytes, y: bytes)

Create a public key from its COSE values

from_private_parts(x: bytes, y: bytes, d: bytes)
sign(body, aad, private_key)

Return the signature produced by the key when using CounterSignature0 as describe in draft-ietf-cose-countersign-01

verify(signature, body, aad, public_key)

Verify a signature in analogy to sign

generate()

Return a usable private key

public_from_private(private_key)

Given a private key, derive the publishable key

staticstatic(private_key, public_key)

Derive a shared static-static secret from a private and a public key

value = -7
curve_number = 1
signature_length = 64
class aiocoap.oscore.BaseSecurityContext

Bases: object

external_aad_is_group = False
authenticated_claims = []
alg_aead: AeadAlgorithm | None
property algorithm
class aiocoap.oscore.CanProtect

Bases: BaseSecurityContext

is_signing = False
responses_send_kid = False
protect(message, request_id=None, *, kid_context=True)

Given a plain CoAP message, create a protected message that contains message’s options in the inner or outer CoAP message as described in OSCOAP.

If the message is a response to a previous message, the additional data from unprotecting the request are passed in as request_id. When request data is present, its partial IV is reused if possible. The security context’s ID context is encoded in the resulting message unless kid_context is explicitly set to a False; other values for the kid_context can be passed in as byte string in the same parameter.

new_sequence_number()

Return a new sequence number; the implementation is responsible for never returning the same value twice in a given security context.

May raise ContextUnavailable.

abstract post_seqnoincrease()

Ensure that sender_sequence_number is stored

context_from_response(unprotected_bag) CanUnprotect

When receiving a response to a request protected with this security context, pick the security context with which to unprotect the response given the unprotected information from the Object-Security option.

This allow picking the right security context in a group response, and helps getting a new short-lived context for B.2 mode. The default behaivor is returning self.

class aiocoap.oscore.CanUnprotect

Bases: BaseSecurityContext

unprotect(protected_message, request_id=None)
context_for_response() CanProtect

After processing a request with this context, with which security context should an outgoing response be protected? By default, it’s the same context.

class aiocoap.oscore.SecurityContextUtils

Bases: BaseSecurityContext

derive_keys(master_salt, master_secret)

Populate sender_key, recipient_key and common_iv from the algorithm, hash function and id_context already configured beforehand, and from the passed salt and secret.

get_oscore_context_for(unprotected)

Return a sutiable context (most easily self) for an incoming request if its unprotected data (COSE_KID, COSE_KID_CONTEXT) fit its description. If it doesn’t match, it returns None.

The default implementation just strictly checks for whether kid and any kid context match (not matching if a local KID context is set but none is given in the request); modes like Group OSCORE can spin up aspect objects here.

class aiocoap.oscore.ReplayWindow(size, strike_out_callback)

Bases: object

A regular replay window of a fixed size.

It is implemented as an index and a bitfield (represented by an integer) whose least significant bit represents the seqyence number of the index, and a 1 indicates that a number was seen. No shenanigans around implicit leading ones (think floating point normalization) happen.

>>> w = ReplayWindow(32, lambda: None)
>>> w.initialize_empty()
>>> w.strike_out(5)
>>> w.is_valid(3)
True
>>> w.is_valid(5)
False
>>> w.strike_out(0)
>>> w.strike_out(1)
>>> w.strike_out(2)
>>> w.is_valid(1)
False

Jumping ahead by the window size invalidates older numbers:

>>> w.is_valid(4)
True
>>> w.strike_out(35)
>>> w.is_valid(4)
True
>>> w.strike_out(36)
>>> w.is_valid(4)
False

Usage safety

For every key, the replay window can only be initielized empty once. On later uses, it needs to be persisted by storing the output of self.persist() somewhere and loaded from that persisted data.

It is acceptable to store persistance data in the strike_out_callback, but that must then ensure that the data is written (flushed to a file or committed to a database), but that is usually inefficient.

Stability

This class is not considered for stabilization yet and an implementation detail of the SecurityContext implementation(s).

is_initialized()
initialize_empty()
initialize_from_persisted(persisted)
initialize_from_freshlyseen(seen)

Initialize the replay window with a particular value that is just being observed in a fresh (ie. generated by the peer later than any messages processed before state was lost here) message. This marks the seen sequence number and all preceding it as invalid, and and all later ones as valid.

is_valid(number)
strike_out(number)
persist()

Return a dict containing internal state which can be passed to init to recreated the replay window.

class aiocoap.oscore.FilesystemSecurityContext(basedir, sequence_number_chunksize_start=10, sequence_number_chunksize_limit=10000)

Bases: CanProtect, CanUnprotect, SecurityContextUtils

Security context stored in a directory as distinct files containing containing

  • Master secret, master salt, sender and recipient ID, optionally algorithm, the KDF hash function, and replay window size (settings.json and secrets.json, where the latter is typically readable only for the user)

  • sequence numbers and replay windows (sequence.json, the only file the process needs write access to)

The static parameters can all either be placed in settings.json or secrets.json, but must not be present in both; the presence of either file is sufficient.

Warning

Security contexts must never be copied around and used after another copy was used. They should only ever be moved, and if they are copied (eg. as a part of a system backup), restored contexts must not be used again; they need to be replaced with freshly created ones.

An additional file named lock is created to prevent the accidental use of a context by to concurrent programs.

Note that the sequence number file is updated in an atomic fashion which requires file creation privileges in the directory. If privilege separation between settings/key changes and sequence number changes is desired, one way to achieve that on Linux is giving the aiocoap process’s user group write permissions on the directory and setting the sticky bit on the directory, thus forbidding the user to remove the settings/secret files not owned by him.

Writes due to sent sequence numbers are reduced by applying a variation on the mechanism of RFC8613 Appendix B.1.1 (incrementing the persisted sender seqence number in steps of k). That value is automatically grown from sequence_number_chunksize_start up to sequence_number_chunksize_limit. At runtime, the receive window is not stored but kept indeterminate. In case of an abnormal shutdown, the server uses the mechanism described in Appendix B.1.2 to recover.

exception LoadError

Bases: ValueError

Exception raised with a descriptive message when trying to load a faulty security context

post_seqnoincrease()

Ensure that sender_sequence_number is stored

class aiocoap.oscore.GroupContext

Bases: BaseSecurityContext

is_signing = True
external_aad_is_group = True
responses_send_kid = True
alg_signature_enc: AeadAlgorithm | None
alg_signature: AlgorithmCountersign | None
alg_pairwise_key_agreement: AlgorithmCountersign | None
abstract property private_key

Private key used to sign outgoing messages.

Contexts not designed to send messages may raise a RuntimeError here; that necessity may later go away if some more accurate class modelling is found.

abstract property recipient_public_key

Public key used to verify incoming messages.

Contexts not designed to receive messages (because they’d have aspects for that) may raise a RuntimeError here; that necessity may later go away if some more accurate class modelling is found.

class aiocoap.oscore.SimpleGroupContext(alg_aead, hashfun, alg_signature, alg_signature_enc, alg_pairwise_key_agreement, group_id, master_secret, master_salt, sender_id, private_key, sender_auth_cred, peers, group_manager_cred=None, cred_fmt=13131313)

Bases: GroupContext, CanProtect, CanUnprotect, SecurityContextUtils

A context for an OSCORE group

This is a non-persistable version of a group context that does not support any group manager or rekeying; it is set up statically at startup.

It is intended for experimentation and demos, but aims to be correct enough to be usable securely.

private_key = None
sender_auth_cred = None
property recipient_public_key

Public key used to verify incoming messages.

Contexts not designed to receive messages (because they’d have aspects for that) may raise a RuntimeError here; that necessity may later go away if some more accurate class modelling is found.

derive_keys(master_salt, master_secret)

Populate sender_key, recipient_key and common_iv from the algorithm, hash function and id_context already configured beforehand, and from the passed salt and secret.

post_seqnoincrease()

No-op because it’s ephemeral

context_from_response(unprotected_bag) CanUnprotect

When receiving a response to a request protected with this security context, pick the security context with which to unprotect the response given the unprotected information from the Object-Security option.

This allow picking the right security context in a group response, and helps getting a new short-lived context for B.2 mode. The default behaivor is returning self.

get_oscore_context_for(unprotected)

Return a sutiable context (most easily self) for an incoming request if its unprotected data (COSE_KID, COSE_KID_CONTEXT) fit its description. If it doesn’t match, it returns None.

The default implementation just strictly checks for whether kid and any kid context match (not matching if a local KID context is set but none is given in the request); modes like Group OSCORE can spin up aspect objects here.

pairwise_for(recipient_id)
for_sending_deterministic_requests(deterministic_id, target_server: bytes | None)
aiocoap.oscore.verify_start(message)

Extract the unprotected COSE options from a message for the verifier to then pick a security context to actually verify the message. (Future versions may also report fields from both unprotected and protected, if the protected bag is ever used with OSCORE.).

Call this only requests; for responses, you’ll have to know the security context anyway, and there is usually no information to be gained.