Coverage for aiocoap/oscore.py: 85%
961 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-16 16:09 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-16 16:09 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""This module contains the tools to send OSCORE secured messages.
7It only deals with the algorithmic parts, the security context and protection
8and unprotection of messages. It does not touch on the integration of OSCORE in
9the larger aiocoap stack of having a context or requests; that's what
10:mod:`aiocoap.transports.osore` is for.`"""
12from __future__ import annotations
14from collections import namedtuple
15import io
16import json
17import binascii
18import os
19import os.path
20import tempfile
21import abc
22from typing import Optional
23import secrets
24import warnings
26from aiocoap.message import Message
27from aiocoap.util import cryptography_additions, deprecation_getattr
28from aiocoap.numbers import GET, POST, FETCH, CHANGED, UNAUTHORIZED, CONTENT
29from aiocoap import error
31from cryptography.hazmat.primitives.ciphers import aead
32from cryptography.hazmat.primitives.kdf.hkdf import HKDF
33from cryptography.hazmat.primitives import hashes
34import cryptography.hazmat.backends
35import cryptography.exceptions
36from cryptography.hazmat.primitives import asymmetric, serialization
37from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
39import cbor2 as cbor
41import filelock
43MAX_SEQNO = 2**40 - 1
45# Relevant values from the IANA registry "CBOR Object Signing and Encryption (COSE)"
46COSE_KID = 4
47COSE_PIV = 6
48COSE_KID_CONTEXT = 10
49# from RFC9338
50COSE_COUNTERSIGNATURE0 = 12
51# from draft-ietf-lake-edhoc-19, guessing the value
52COSE_KCCS = 13131313
54COMPRESSION_BITS_N = 0b111
55COMPRESSION_BIT_K = 0b1000
56COMPRESSION_BIT_H = 0b10000
57COMPRESSION_BIT_G = 0b100000 # Group Flag from draft-ietf-core-oscore-groupcomm-10
58COMPRESSION_BITS_RESERVED = 0b11000000
60# While the original values were simple enough to be used in literals, starting
61# with oscore-groupcomm we're using more compact values
63INFO_TYPE_KEYSTREAM_REQUEST = True
64INFO_TYPE_KEYSTREAM_RESPONSE = False
66class CodeStyle(namedtuple("_CodeStyle", ("request", "response"))):
67 @classmethod
68 def from_request(cls, request) -> CodeStyle:
69 if request == FETCH:
70 return cls.FETCH_CONTENT
71 elif request == POST:
72 return cls.POST_CHANGED
73 else:
74 raise ValueError("Invalid request code %r" % request)
76CodeStyle.FETCH_CONTENT = CodeStyle(FETCH, CONTENT)
77CodeStyle.POST_CHANGED = CodeStyle(POST, CHANGED)
80class DeterministicKey:
81 """Singleton to indicate that for this key member no public or private key
82 is available because it is the Deterministic Client (see
83 <https://www.ietf.org/archive/id/draft-amsuess-core-cachable-oscore-01.html>)
85 This is highly experimental not only from an implementation but also from a
86 specification point of view. The specification has not received adaequate
87 review that would justify using it in any non-experimental scenario.
88 """
89DETERMINISTIC_KEY = DeterministicKey()
90del DeterministicKey
92class NotAProtectedMessage(error.Error, ValueError):
93 """Raised when verification is attempted on a non-OSCORE message"""
95 def __init__(self, message, plain_message):
96 super().__init__(message)
97 self.plain_message = plain_message
99class ProtectionInvalid(error.Error, ValueError):
100 """Raised when verification of an OSCORE message fails"""
102class DecodeError(ProtectionInvalid):
103 """Raised when verification of an OSCORE message fails because CBOR or compressed data were erroneous"""
105class ReplayError(ProtectionInvalid):
106 """Raised when verification of an OSCORE message fails because the sequence numbers was already used"""
108class ReplayErrorWithEcho(ProtectionInvalid, error.RenderableError):
109 """Raised when verification of an OSCORE message fails because the
110 recipient replay window is uninitialized, but a 4.01 Echo can be
111 constructed with the data in the exception that can lead to the client
112 assisting in replay window recovery"""
113 def __init__(self, secctx, request_id, echo):
114 self.secctx = secctx
115 self.request_id = request_id
116 self.echo = echo
118 def to_message(self):
119 inner = Message(
120 code=UNAUTHORIZED,
121 echo=self.echo,
122 )
123 outer, _ = self.secctx.protect(inner, request_id=self.request_id)
124 return outer
126class ContextUnavailable(error.Error, ValueError):
127 """Raised when a context is (currently or permanently) unavailable for
128 protecting or unprotecting a message"""
130class RequestIdentifiers:
131 """A container for details that need to be passed along from the
132 (un)protection of a request to the (un)protection of the response; these
133 data ensure that the request-response binding process works by passing
134 around the request's partial IV.
136 Users of this module should never create or interact with instances, but
137 just pass them around.
138 """
139 def __init__(self, kid, partial_iv, nonce, can_reuse_nonce, request_code):
140 self.kid = kid
141 self.partial_iv = partial_iv
142 self.nonce = nonce
143 self.can_reuse_nonce = can_reuse_nonce
144 self.code_style = CodeStyle.from_request(request_code)
146 self.request_hash = None
148 def get_reusable_nonce_and_piv(self):
149 """Return the nonce and the partial IV if can_reuse_nonce is True, and
150 set can_reuse_nonce to False."""
152 if self.can_reuse_nonce:
153 self.can_reuse_nonce = False
154 return (self.nonce, self.partial_iv)
155 else:
156 return (None, None)
158def _xor_bytes(a, b):
159 assert len(a) == len(b)
160 # FIXME is this an efficient thing to do, or should we store everything
161 # that possibly needs xor'ing as long integers with an associated length?
162 return bytes(_a ^ _b for (_a, _b) in zip(a, b))
164class AeadAlgorithm(metaclass=abc.ABCMeta):
165 @abc.abstractmethod
166 def encrypt(cls, plaintext, aad, key, iv):
167 """Return ciphertext + tag for given input data"""
169 @abc.abstractmethod
170 def decrypt(cls, ciphertext_and_tag, aad, key, iv):
171 """Reverse encryption. Must raise ProtectionInvalid on any error
172 stemming from untrusted data."""
174 @staticmethod
175 def _build_encrypt0_structure(protected, external_aad):
176 assert protected == {}
177 protected_serialized = b'' # were it into an empty dict, it'd be the cbor dump
178 enc_structure = ['Encrypt0', protected_serialized, external_aad]
180 return cbor.dumps(enc_structure)
182class AES_CCM(AeadAlgorithm, metaclass=abc.ABCMeta):
183 """AES-CCM implemented using the Python cryptography library"""
185 @classmethod
186 def encrypt(cls, plaintext, aad, key, iv):
187 return aead.AESCCM(key, cls.tag_bytes).encrypt(iv, plaintext, aad)
189 @classmethod
190 def decrypt(cls, ciphertext_and_tag, aad, key, iv):
191 try:
192 return aead.AESCCM(key, cls.tag_bytes).decrypt(iv, ciphertext_and_tag, aad)
193 except cryptography.exceptions.InvalidTag:
194 raise ProtectionInvalid("Tag invalid")
196class AES_CCM_16_64_128(AES_CCM):
197 # from RFC8152 and draft-ietf-core-object-security-0[012] 3.2.1
198 value = 10
199 key_bytes = 16 # 128-bit key
200 tag_bytes = 8 # 64-bit tag
201 iv_bytes = 13 # 13-byte nonce
203class AES_CCM_16_64_256(AES_CCM):
204 # from RFC8152
205 value = 11
206 key_bytes = 32 # 256-bit key
207 tag_bytes = 8 # 64-bit tag
208 iv_bytes = 13 # 13-byte nonce
210class AES_CCM_64_64_128(AES_CCM):
211 # from RFC8152
212 value = 12
213 key_bytes = 16 # 128-bit key
214 tag_bytes = 8 # 64-bit tag
215 iv_bytes = 7 # 7-byte nonce
217class AES_CCM_64_64_256(AES_CCM):
218 # from RFC8152
219 value = 13
220 key_bytes = 32 # 256-bit key
221 tag_bytes = 8 # 64-bit tag
222 iv_bytes = 7 # 7-byte nonce
224class AES_CCM_16_128_128(AES_CCM):
225 # from RFC8152
226 value = 30
227 key_bytes = 16 # 128-bit key
228 tag_bytes = 16 # 128-bit tag
229 iv_bytes = 13 # 13-byte nonce
231class AES_CCM_16_128_256(AES_CCM):
232 # from RFC8152
233 value = 31
234 key_bytes = 32 # 256-bit key
235 tag_bytes = 16 # 128-bit tag
236 iv_bytes = 13 # 13-byte nonce
238class AES_CCM_64_128_128(AES_CCM):
239 # from RFC8152
240 value = 32
241 key_bytes = 16 # 128-bit key
242 tag_bytes = 16 # 128-bit tag
243 iv_bytes = 7 # 7-byte nonce
245class AES_CCM_64_128_256(AES_CCM):
246 # from RFC8152
247 value = 33
248 key_bytes = 32 # 256-bit key
249 tag_bytes = 16 # 128-bit tag
250 iv_bytes = 7 # 7-byte nonce
253class AES_GCM(AeadAlgorithm, metaclass=abc.ABCMeta):
254 """AES-GCM implemented using the Python cryptography library"""
256 iv_bytes = 12 # 96 bits fixed size of the nonce
258 @classmethod
259 def encrypt(cls, plaintext, aad, key, iv):
260 return aead.AESGCM(key).encrypt(iv, plaintext, aad)
262 @classmethod
263 def decrypt(cls, ciphertext_and_tag, aad, key, iv):
264 try:
265 return aead.AESGCM(key).decrypt(iv, ciphertext_and_tag, aad)
266 except cryptography.exceptions.InvalidTag:
267 raise ProtectionInvalid("Tag invalid")
269class A128GCM(AES_GCM):
270 # from RFC8152
271 value = 1
272 key_bytes = 16 # 128-bit key
273 tag_bytes = 16 # 128-bit tag
275class A192GCM(AES_GCM):
276 # from RFC8152
277 value = 2
278 key_bytes = 24 # 192-bit key
279 tag_bytes = 16 # 128-bit tag
281class A256GCM(AES_GCM):
282 # from RFC8152
283 value = 3
284 key_bytes = 32 # 256-bit key
285 tag_bytes = 16 # 128-bit tag
287class ChaCha20Poly1305(AeadAlgorithm):
288 # from RFC8152
289 value = 24
290 key_bytes = 32 # 256-bit key
291 tag_bytes = 16 # 128-bit tag
292 iv_bytes = 12 # 96-bit nonce
294 @classmethod
295 def encrypt(cls, plaintext, aad, key, iv):
296 return aead.ChaCha20Poly1305(key).encrypt(iv, plaintext, aad)
298 @classmethod
299 def decrypt(cls, ciphertext_and_tag, aad, key, iv):
300 try:
301 return aead.ChaCha20Poly1305(key).decrypt(iv, ciphertext_and_tag, aad)
302 except cryptography.exceptions.InvalidTag:
303 raise ProtectionInvalid("Tag invalid")
305class AlgorithmCountersign(metaclass=abc.ABCMeta):
306 """A fully parameterized COSE countersign algorithm
308 An instance is able to provide all the alg_signature, par_countersign and
309 par_countersign_key parameters taht go into the Group OSCORE algorithms
310 field.
311 """
312 @abc.abstractmethod
313 def sign(self, body, external_aad, private_key):
314 """Return the signature produced by the key when using
315 CounterSignature0 as describe in draft-ietf-cose-countersign-01"""
317 @abc.abstractmethod
318 def verify(self, signature, body, external_aad, public_key):
319 """Verify a signature in analogy to sign"""
321 @abc.abstractmethod
322 def generate(self):
323 """Return a usable private key"""
325 @abc.abstractmethod
326 def public_from_private(self, private_key):
327 """Given a private key, derive the publishable key"""
329 @staticmethod
330 def _build_countersign_structure(body, external_aad):
331 countersign_structure = [
332 "CounterSignature0",
333 b"",
334 b"",
335 external_aad,
336 body
337 ]
338 tobesigned = cbor.dumps(countersign_structure)
339 return tobesigned
341 @property
342 @abc.abstractproperty
343 def signature_length(self):
344 """The length of a signature using this algorithm"""
346 @property
347 @abc.abstractproperty
348 def curve_number(self):
349 """Registered curve number used with this algorithm.
351 Only used for verification of credentials' details"""
353class AlgorithmStaticStatic(metaclass=abc.ABCMeta):
354 @abc.abstractmethod
355 def staticstatic(self, private_key, public_key):
356 """Derive a shared static-static secret from a private and a public key"""
358class Ed25519(AlgorithmCountersign):
359 def sign(self, body, aad, private_key):
360 private_key = asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
361 return private_key.sign(self._build_countersign_structure(body, aad))
363 def verify(self, signature, body, aad, public_key):
364 public_key = asymmetric.ed25519.Ed25519PublicKey.from_public_bytes(public_key)
365 try:
366 public_key.verify(signature, self._build_countersign_structure(body, aad))
367 except cryptography.exceptions.InvalidSignature:
368 raise ProtectionInvalid("Signature mismatch")
370 def generate(self):
371 key = asymmetric.ed25519.Ed25519PrivateKey.generate()
372 # FIXME: We could avoid handing the easy-to-misuse bytes around if the
373 # current algorithm interfaces did not insist on passing the
374 # exchangable representations -- and generally that should be more
375 # efficient.
376 return key.private_bytes(
377 encoding=serialization.Encoding.Raw,
378 format=serialization.PrivateFormat.Raw,
379 encryption_algorithm=serialization.NoEncryption(),
380 )
382 def public_from_private(self, private_key):
383 private_key = asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
384 public_key = private_key.public_key()
385 return public_key.public_bytes(
386 encoding=serialization.Encoding.Raw,
387 format=serialization.PublicFormat.Raw,
388 )
390 value = -8
391 curve_number = 6
393 signature_length = 64
395class EcdhSsHkdf256(AlgorithmStaticStatic):
396 # FIXME: This class uses the Edwards keys as private and public keys, and
397 # not the converted ones. This will be problematic if pairwise-only
398 # contexts are to be set up.
400 value = -27 # FIXME: or -28? (see shepherd review)
402 # FIXME these two will be different when using the Montgomery keys directly
404 # This one will only be used when establishing and distributing pairwise-only keys
405 generate = Ed25519.generate
406 public_from_private = Ed25519.public_from_private
408 def staticstatic(self, private_key, public_key):
409 private_key = asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
410 private_key = cryptography_additions.sk_to_curve25519(private_key)
412 public_key = asymmetric.ed25519.Ed25519PublicKey.from_public_bytes(public_key)
413 public_key = cryptography_additions.pk_to_curve25519(public_key)
415 return private_key.exchange(public_key)
417class ECDSA_SHA256_P256(AlgorithmCountersign, AlgorithmStaticStatic):
418 # Trying a new construction approach -- should work just as well given
419 # we're just passing Python objects around
420 def from_public_parts(self, x: bytes, y: bytes):
421 """Create a public key from its COSE values"""
422 return asymmetric.ec.EllipticCurvePublicNumbers(
423 int.from_bytes(x, 'big'),
424 int.from_bytes(y, 'big'),
425 asymmetric.ec.SECP256R1()
426 ).public_key()
428 def from_private_parts(self, x: bytes, y: bytes, d: bytes):
429 public_numbers = self.from_public_parts(x, y).public_numbers()
430 private_numbers = asymmetric.ec.EllipticCurvePrivateNumbers(
431 int.from_bytes(d, 'big'),
432 public_numbers)
433 return private_numbers.private_key()
435 def sign(self, body, aad, private_key):
436 der_signature = private_key.sign(self._build_countersign_structure(body, aad), asymmetric.ec.ECDSA(hashes.SHA256()))
437 (r, s) = decode_dss_signature(der_signature)
439 return r.to_bytes(32, "big") + s.to_bytes(32, "big")
441 def verify(self, signature, body, aad, public_key):
442 r = signature[:32]
443 s = signature[32:]
444 r = int.from_bytes(r, "big")
445 s = int.from_bytes(s, "big")
446 der_signature = encode_dss_signature(r, s)
447 try:
448 public_key.verify(der_signature, self._build_countersign_structure(body, aad), asymmetric.ec.ECDSA(hashes.SHA256()))
449 except cryptography.exceptions.InvalidSignature:
450 raise ProtectionInvalid("Signature mismatch")
452 def generate(self):
453 return asymmetric.ec.generate_private_key(asymmetric.ec.SECP256R1())
455 def public_from_private(self, private_key):
456 return private_key.public_key()
458 def staticstatic(self, private_key, public_key):
459 return private_key.exchange(asymmetric.ec.ECDH(), public_key)
461 value = -7 # FIXME: when used as a static-static algorithm, does this become -27? see shepherd review.
462 curve_number = 1
464 signature_length = 64
466algorithms = {
467 'AES-CCM-16-64-128': AES_CCM_16_64_128(),
468 'AES-CCM-16-64-256': AES_CCM_16_64_256(),
469 'AES-CCM-64-64-128': AES_CCM_64_64_128(),
470 'AES-CCM-64-64-256': AES_CCM_64_64_256(),
471 'AES-CCM-16-128-128': AES_CCM_16_128_128(),
472 'AES-CCM-16-128-256': AES_CCM_16_128_256(),
473 'AES-CCM-64-128-128': AES_CCM_64_128_128(),
474 'AES-CCM-64-128-256': AES_CCM_64_128_256(),
475 'ChaCha20/Poly1305': ChaCha20Poly1305(),
476 'A128GCM': A128GCM(),
477 'A192GCM': A192GCM(),
478 'A256GCM': A256GCM(),
479 }
481# algorithms with full parameter set
482algorithms_countersign = {
483 # maybe needs a different name...
484 'EdDSA on Ed25519': Ed25519(),
485 'ECDSA w/ SHA-256 on P-256': ECDSA_SHA256_P256(),
486 }
488algorithms_staticstatic = {
489 'ECDH-SS + HKDF-256': EcdhSsHkdf256(),
490 }
492DEFAULT_ALGORITHM = 'AES-CCM-16-64-128'
494_hash_backend = cryptography.hazmat.backends.default_backend()
495hashfunctions = {
496 'sha256': hashes.SHA256(),
497 'sha384': hashes.SHA384(),
498 'sha512': hashes.SHA512(),
499 }
501DEFAULT_HASHFUNCTION = 'sha256'
503DEFAULT_WINDOWSIZE = 32
505class BaseSecurityContext:
506 # The protection and unprotection functions will use the Group OSCORE AADs
507 # rather than the regular OSCORE AADs. (Ie. alg_signature_enc etc are added to
508 # the algorithms, and request_kid_context, OSCORE_option, sender_auth_cred and
509 # gm_cred are added).
510 #
511 # This is not necessarily identical to is_signing (as pairwise contexts use
512 # this but don't sign), and is distinct from the added OSCORE option in the
513 # AAD (as that's only applicable for the external AAD as extracted for
514 # signing and signature verification purposes).
515 external_aad_is_group = False
517 # Authentication information carried with this security context; managed
518 # externally by whatever creates the security context.
519 authenticated_claims = []
521 # AEAD algorithm. This may only be None in group contexts that do not use pairwise mode.
522 alg_aead: Optional[AeadAlgorithm]
524 @property
525 def algorithm(self):
526 warnings.warn("Property was renamed to 'alg_aead'", DeprecationWarning, stacklevel=2)
527 return self.alg_aead
528 @algorithm.setter
529 def algorithm(self, value):
530 warnings.warn("Property was renamed to 'alg_aead'", DeprecationWarning, stacklevel=2)
531 self.alg_aead = value
533 def _construct_nonce(self, partial_iv_short, piv_generator_id):
534 pad_piv = b"\0" * (5 - len(partial_iv_short))
536 s = bytes([len(piv_generator_id)])
537 pad_id = b'\0' * (self.alg_aead.iv_bytes - 6 - len(piv_generator_id))
539 components = s + \
540 pad_id + \
541 piv_generator_id + \
542 pad_piv + \
543 partial_iv_short
545 nonce = _xor_bytes(self.common_iv, components)
547 return nonce
549 def _extract_external_aad(self, message, request_id, local_is_sender: bool) -> bytes:
550 """Build the serialized external AAD from information in the message
551 and the request_id.
553 Information about whether the local context is the sender of the
554 message is only relevant to group contexts, where it influences whose
555 authentication credentials are placed in the AAD.
556 """
557 # If any option were actually Class I, it would be something like
558 #
559 # the_options = pick some of(message)
560 # class_i_options = Message(the_options).opt.encode()
562 oscore_version = 1
563 class_i_options = b""
564 if request_id.request_hash is not None:
565 class_i_options = Message(request_hash=request_id.request_hash).opt.encode()
567 algorithms = [self.alg_aead.value]
568 if self.external_aad_is_group:
569 algorithms.append(self.alg_signature_enc.value)
570 algorithms.append(self.alg_signature.value)
571 algorithms.append(self.alg_pairwise_key_agreement.value)
573 external_aad = [
574 oscore_version,
575 algorithms,
576 request_id.kid,
577 request_id.partial_iv,
578 class_i_options,
579 ]
581 if self.external_aad_is_group:
582 # FIXME: We may need to carry this over in the request_id when
583 # observation span group rekeyings
584 external_aad.append(self.id_context)
586 assert message.opt.object_security is not None
587 external_aad.append(message.opt.object_security)
589 if local_is_sender:
590 external_aad.append(self.sender_auth_cred)
591 else:
592 external_aad.append(self.recipient_auth_cred)
593 external_aad.append(self.group_manager_cred)
595 external_aad = cbor.dumps(external_aad)
597 return external_aad
599# FIXME pull interface components from SecurityContext up here
600class CanProtect(BaseSecurityContext, metaclass=abc.ABCMeta):
601 # The protection function will add a signature acccording to the context's
602 # alg_signature attribute if this is true
603 is_signing = False
605 # Send the KID when protecting responses
606 #
607 # Once group pairwise mode is implemented, this will need to become a
608 # parameter to protect(), which is stored at the point where the incoming
609 # context is turned into an outgoing context. (Currently, such a mechanism
610 # isn't there yet, and oscore_wrapper protects responses with the very same
611 # context they came in on).
612 responses_send_kid = False
614 @staticmethod
615 def _compress(protected, unprotected, ciphertext):
616 """Pack the untagged COSE_Encrypt0 object described by the *args
617 into two bytestrings suitable for the Object-Security option and the
618 message body"""
620 if protected:
621 raise RuntimeError("Protection produced a message that has uncompressable fields.")
623 piv = unprotected.pop(COSE_PIV, b"")
624 if len(piv) > COMPRESSION_BITS_N:
625 raise ValueError("Can't encode overly long partial IV")
627 firstbyte = len(piv)
628 if COSE_KID in unprotected:
629 firstbyte |= COMPRESSION_BIT_K
630 kid_data = unprotected.pop(COSE_KID)
631 else:
632 kid_data = b""
634 if COSE_KID_CONTEXT in unprotected:
635 firstbyte |= COMPRESSION_BIT_H
636 kid_context = unprotected.pop(COSE_KID_CONTEXT)
637 s = len(kid_context)
638 if s > 255:
639 raise ValueError("KID Context too long")
640 s_kid_context = bytes((s,)) + kid_context
641 else:
642 s_kid_context = b""
644 if COSE_COUNTERSIGNATURE0 in unprotected:
645 firstbyte |= COMPRESSION_BIT_G
647 # In theory at least. In practice, that's an empty value to later
648 # be squished in when the compressed option value is available for
649 # signing.
650 ciphertext += unprotected.pop(COSE_COUNTERSIGNATURE0)
652 if unprotected:
653 raise RuntimeError("Protection produced a message that has uncompressable fields.")
655 if firstbyte:
656 option = bytes([firstbyte]) + piv + s_kid_context + kid_data
657 else:
658 option = b""
660 return (option, ciphertext)
662 def protect(self, message, request_id=None, *, kid_context=True):
663 """Given a plain CoAP message, create a protected message that contains
664 message's options in the inner or outer CoAP message as described in
665 OSCOAP.
667 If the message is a response to a previous message, the additional data
668 from unprotecting the request are passed in as request_id. When
669 request data is present, its partial IV is reused if possible. The
670 security context's ID context is encoded in the resulting message
671 unless kid_context is explicitly set to a False; other values for the
672 kid_context can be passed in as byte string in the same parameter.
673 """
675 assert (request_id is None) == message.code.is_request()
677 outer_message, plaintext = self._split_message(message, request_id)
679 protected = {}
680 nonce = None
681 unprotected = {}
682 if request_id is not None:
683 nonce, partial_iv_short = request_id.get_reusable_nonce_and_piv()
684 if nonce is not None:
685 partial_iv_generated_by = request_id.kid
687 if nonce is None:
688 nonce, partial_iv_short = self._build_new_nonce()
689 partial_iv_generated_by = self.sender_id
691 unprotected[COSE_PIV] = partial_iv_short
693 if message.code.is_request():
694 unprotected[COSE_KID] = self.sender_id
696 request_id = RequestIdentifiers(self.sender_id, partial_iv_short, nonce, can_reuse_nonce=None, request_code=outer_message.code)
698 if kid_context is True:
699 if self.id_context is not None:
700 unprotected[COSE_KID_CONTEXT] = self.id_context
701 elif kid_context is not False:
702 unprotected[COSE_KID_CONTEXT] = kid_context
703 else:
704 if self.responses_send_kid:
705 unprotected[COSE_KID] = self.sender_id
707 # Putting in a dummy value as the signature calculation will already need some of the compression result
708 if self.is_signing:
709 unprotected[COSE_COUNTERSIGNATURE0] = b""
710 # FIXME: Running this twice quite needlessly (just to get the object_security option for sending)
711 option_data, _ = self._compress(protected, unprotected, b"")
713 outer_message.opt.object_security = option_data
715 external_aad = self._extract_external_aad(outer_message, request_id, local_is_sender=True)
717 aad = self.alg_aead._build_encrypt0_structure(protected, external_aad)
719 key = self._get_sender_key(outer_message, external_aad, plaintext, request_id)
721 ciphertext = self.alg_aead.encrypt(plaintext, aad, key, nonce)
723 _, payload = self._compress(protected, unprotected, ciphertext)
725 if self.is_signing:
726 signature = self.alg_signature.sign(payload, external_aad, self.private_key)
727 keystream = self._kdf_for_keystreams(
728 partial_iv_generated_by,
729 partial_iv_short,
730 self.group_encryption_key,
731 self.sender_id,
732 INFO_TYPE_KEYSTREAM_REQUEST if message.code.is_request() else INFO_TYPE_KEYSTREAM_RESPONSE,
733 )
734 encrypted_signature = _xor_bytes(signature, keystream)
735 payload += encrypted_signature
736 outer_message.payload = payload
738 # FIXME go through options section
740 # the request_id in the second argument should be discarded by the
741 # caller when protecting a response -- is that reason enough for an
742 # `if` and returning None?
743 return outer_message, request_id
745 def _get_sender_key(self, outer_message, aad, plaintext, request_id):
746 """Customization hook of the protect function
748 While most security contexts have a fixed sender key, deterministic
749 requests need to shake up a few things. They need to modify the outer
750 message, as well as the request_id as it will later be used to
751 unprotect the response."""
752 return self.sender_key
754 def _split_message(self, message, request_id):
755 """Given a protected message, return the outer message that contains
756 all Class I and Class U options (but without payload or Object-Security
757 option), and the encoded inner message that contains all Class E
758 options and the payload.
760 This leaves the messages' remotes unset."""
762 if message.code.is_request():
763 outer_host = message.opt.uri_host
764 proxy_uri = message.opt.proxy_uri
766 inner_message = message.copy(
767 uri_host=None,
768 uri_port=None,
769 proxy_uri=None,
770 proxy_scheme=None,
771 )
772 inner_message.remote = None
774 if proxy_uri is not None:
775 # Use set_request_uri to split up the proxy URI into its
776 # components; extract, preserve and clear them.
777 inner_message.set_request_uri(proxy_uri, set_uri_host=False)
778 if inner_message.opt.proxy_uri is not None:
779 raise ValueError("Can not split Proxy-URI into options")
780 outer_uri = inner_message.remote.uri_base
781 inner_message.remote = None
782 inner_message.opt.proxy_scheme = None
784 if message.opt.observe is None:
785 outer_code = POST
786 else:
787 outer_code = FETCH
788 else:
789 outer_host = None
790 proxy_uri = None
792 inner_message = message.copy()
794 outer_code = request_id.code_style.response
796 # no max-age because these are always successsful responses
797 outer_message = Message(code=outer_code,
798 uri_host=outer_host,
799 observe=None if message.code.is_response() else message.opt.observe,
800 )
801 if proxy_uri is not None:
802 outer_message.set_request_uri(outer_uri)
804 plaintext = bytes([inner_message.code]) + inner_message.opt.encode()
805 if inner_message.payload:
806 plaintext += bytes([0xFF])
807 plaintext += inner_message.payload
809 return outer_message, plaintext
811 def _build_new_nonce(self):
812 """This implements generation of a new nonce, assembled as per Figure 5
813 of draft-ietf-core-object-security-06. Returns the shortened partial IV
814 as well."""
815 seqno = self.new_sequence_number()
817 partial_iv = seqno.to_bytes(5, 'big')
819 return (self._construct_nonce(partial_iv, self.sender_id), partial_iv.lstrip(b'\0') or b'\0')
821 # sequence number handling
823 def new_sequence_number(self):
824 """Return a new sequence number; the implementation is responsible for
825 never returning the same value twice in a given security context.
827 May raise ContextUnavailable."""
828 retval = self.sender_sequence_number
829 if retval >= MAX_SEQNO:
830 raise ContextUnavailable("Sequence number too large, context is exhausted.")
831 self.sender_sequence_number += 1
832 self.post_seqnoincrease()
833 return retval
835 # implementation defined
837 @abc.abstractmethod
838 def post_seqnoincrease(self):
839 """Ensure that sender_sequence_number is stored"""
840 raise
842 def context_from_response(self, unprotected_bag) -> CanUnprotect:
843 """When receiving a response to a request protected with this security
844 context, pick the security context with which to unprotect the response
845 given the unprotected information from the Object-Security option.
847 This allow picking the right security context in a group response, and
848 helps getting a new short-lived context for B.2 mode. The default
849 behaivor is returning self.
850 """
851 return self # FIXME justify by moving into a mixin for CanProtectAndUnprotect
853class CanUnprotect(BaseSecurityContext):
854 def unprotect(self, protected_message, request_id=None):
855 assert (request_id is not None) == protected_message.code.is_response()
856 is_response = protected_message.code.is_response()
858 # Set to a raisable exception on replay check failures; it will be
859 # raised, but the package may still be processed in the course of Echo handling.
860 replay_error = None
862 protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0(protected_message)
864 if protected:
865 raise ProtectionInvalid("The protected field is not empty")
867 # FIXME check for duplicate keys in protected
869 if unprotected.pop(COSE_KID_CONTEXT, self.id_context) != self.id_context:
870 # FIXME is this necessary?
871 raise ProtectionInvalid("Sender ID context does not match")
873 if unprotected.pop(COSE_KID, self.recipient_id) != self.recipient_id:
874 # for most cases, this is caught by the session ID dispatch, but in
875 # responses (where explicit sender IDs are atypical), this is a
876 # valid check
877 raise ProtectionInvalid("Sender ID does not match")
879 if COSE_PIV not in unprotected:
880 if not is_response:
881 raise ProtectionInvalid("No sequence number provided in request")
883 nonce = request_id.nonce
884 seqno = None # sentinel for not striking out anyting
885 partial_iv_short = request_id.partial_iv
886 partial_iv_generated_by = request_id.kid
887 else:
888 partial_iv_short = unprotected.pop(COSE_PIV)
889 partial_iv_generated_by = self.recipient_id
891 nonce = self._construct_nonce(partial_iv_short, self.recipient_id)
893 seqno = int.from_bytes(partial_iv_short, 'big')
895 if not is_response:
896 if not self.recipient_replay_window.is_initialized():
897 replay_error = ReplayError("Sequence number check unavailable")
898 elif not self.recipient_replay_window.is_valid(seqno):
899 replay_error = ReplayError("Sequence number was re-used")
901 if replay_error is not None and self.echo_recovery is None:
902 # Don't even try decoding if there is no reason to
903 raise replay_error
905 request_id = RequestIdentifiers(self.recipient_id, partial_iv_short, nonce, can_reuse_nonce=replay_error is None, request_code=protected_message.code)
907 if unprotected.pop(COSE_COUNTERSIGNATURE0, None) is not None:
908 try:
909 alg_signature = self.alg_signature
910 except NameError:
911 raise DecodeError("Group messages can not be decoded with this non-group context")
913 siglen = alg_signature.signature_length
914 if len(ciphertext) < siglen:
915 raise DecodeError("Message too short for signature")
916 encrypted_signature = ciphertext[-siglen:]
918 keystream = self._kdf_for_keystreams(
919 partial_iv_generated_by,
920 partial_iv_short,
921 self.group_encryption_key,
922 self.recipient_id,
923 INFO_TYPE_KEYSTREAM_REQUEST if protected_message.code.is_request() else INFO_TYPE_KEYSTREAM_RESPONSE,
924 )
925 signature = _xor_bytes(encrypted_signature, keystream)
927 ciphertext = ciphertext[:-siglen]
928 else:
929 signature = None
931 if unprotected:
932 raise DecodeError("Unsupported unprotected option")
934 if len(ciphertext) < self.alg_aead.tag_bytes + 1: # +1 assures access to plaintext[0] (the code)
935 raise ProtectionInvalid("Ciphertext too short")
937 external_aad = self._extract_external_aad(protected_message, request_id, local_is_sender=False)
938 enc_structure = ['Encrypt0', protected_serialized, external_aad]
939 aad = cbor.dumps(enc_structure)
941 key = self._get_recipient_key(protected_message)
943 plaintext = self.alg_aead.decrypt(ciphertext, aad, key, nonce)
945 self._post_decrypt_checks(external_aad, plaintext, protected_message, request_id)
947 if not is_response and seqno is not None and replay_error is None:
948 self.recipient_replay_window.strike_out(seqno)
950 if signature is not None:
951 # Only doing the expensive signature validation once the cheaper decyrption passed
952 alg_signature.verify(signature, ciphertext, external_aad, self.recipient_public_key)
954 # FIXME add options from unprotected
956 unprotected_message = Message(code=plaintext[0])
957 unprotected_message.payload = unprotected_message.opt.decode(plaintext[1:])
959 try_initialize = not self.recipient_replay_window.is_initialized() and \
960 self.echo_recovery is not None
961 if try_initialize:
962 if protected_message.code.is_request():
963 # Either accept into replay window and clear replay error, or raise
964 # something that can turn into a 4.01,Echo response
965 if unprotected_message.opt.echo == self.echo_recovery:
966 self.recipient_replay_window.initialize_from_freshlyseen(seqno)
967 replay_error = None
968 else:
969 raise ReplayErrorWithEcho(secctx=self, request_id=request_id, echo=self.echo_recovery)
970 else:
971 # We can initialize the replay window from a response as well.
972 # The response is guaranteed fresh as it was AEAD-decoded to
973 # match a request sent by this process.
974 #
975 # This is rare, as it only works when the server uses an own
976 # sequence number, eg. when sending a notification or when
977 # acting again on a retransmitted safe request whose response
978 # it did not cache.
979 #
980 # Nothing bad happens if we can't make progress -- we just
981 # don't initialize the replay window that wouldn't have been
982 # checked for a response anyway.
983 if seqno is not None:
984 self.recipient_replay_window.initialize_from_freshlyseen(seqno)
986 if replay_error is not None:
987 raise replay_error
989 if unprotected_message.code.is_request():
990 if protected_message.opt.observe != 0:
991 unprotected_message.opt.observe = None
992 else:
993 if protected_message.opt.observe is not None:
994 # -1 ensures that they sort correctly in later reordering
995 # detection. Note that neither -1 nor high (>3 byte) sequence
996 # numbers can be serialized in the Observe option, but they are
997 # in this implementation accepted for passing around.
998 unprotected_message.opt.observe = -1 if seqno is None else seqno
1000 return unprotected_message, request_id
1002 def _get_recipient_key(self, protected_message):
1003 """Customization hook of the unprotect function
1005 While most security contexts have a fixed recipient key, deterministic
1006 requests build it on demand."""
1007 return self.recipient_key
1009 def _post_decrypt_checks(self, aad, plaintext, protected_message, request_id):
1010 """Customization hook of the unprotect function after decryption
1012 While most security contexts are good with the default checks,
1013 deterministic requests need to perform additional checks while AAD and
1014 plaintext information is still available, and modify the request_id for
1015 the later protection step of the response."""
1017 @staticmethod
1018 def _uncompress(option_data, payload):
1019 if option_data == b"":
1020 firstbyte = 0
1021 else:
1022 firstbyte = option_data[0]
1023 tail = option_data[1:]
1025 unprotected = {}
1027 if firstbyte & COMPRESSION_BITS_RESERVED:
1028 raise DecodeError("Protected data uses reserved fields")
1030 pivsz = firstbyte & COMPRESSION_BITS_N
1031 if pivsz:
1032 if len(tail) < pivsz:
1033 raise DecodeError("Partial IV announced but not present")
1034 unprotected[COSE_PIV] = tail[:pivsz]
1035 tail = tail[pivsz:]
1037 if firstbyte & COMPRESSION_BIT_H:
1038 # kid context hint
1039 s = tail[0]
1040 if len(tail) - 1 < s:
1041 raise DecodeError("Context hint announced but not present")
1042 tail = tail[1:]
1043 unprotected[COSE_KID_CONTEXT] = tail[:s]
1044 tail = tail[s:]
1046 if firstbyte & COMPRESSION_BIT_K:
1047 kid = tail
1048 unprotected[COSE_KID] = kid
1050 if firstbyte & COMPRESSION_BIT_G:
1051 # Not really; As this is (also) used early on (before the KID
1052 # context is even known, because it's just getting extracted), this
1053 # is returning an incomplete value here and leaves it to the later
1054 # processing to strip the right number of bytes from the ciphertext
1055 unprotected[COSE_COUNTERSIGNATURE0] = b""
1057 return b"", {}, unprotected, payload
1059 @classmethod
1060 def _extract_encrypted0(cls, message):
1061 if message.opt.object_security is None:
1062 raise NotAProtectedMessage("No Object-Security option present", message)
1064 protected_serialized, protected, unprotected, ciphertext = cls._uncompress(message.opt.object_security, message.payload)
1065 return protected_serialized, protected, unprotected, ciphertext
1067 # implementation defined
1069 def context_for_response(self) -> CanProtect:
1070 """After processing a request with this context, with which security
1071 context should an outgoing response be protected? By default, it's the
1072 same context."""
1073 # FIXME: Is there any way in which the handler may want to influence
1074 # the decision taken here? Or would, then, the handler just call a more
1075 # elaborate but similar function when setting the response's remote
1076 # already?
1077 return self # FIXME justify by moving into a mixin for CanProtectAndUnprotect
1079class SecurityContextUtils(BaseSecurityContext):
1080 def _kdf(self, salt, ikm, role_id, out_type):
1081 """The HKDF as used to derive sender and recipient key and IV in
1082 RFC8613 Section 3.2.1, and analogously the Group Encryption Key of oscore-groupcomm.
1083 """
1084 if out_type == 'Key':
1085 out_bytes = self.alg_aead.key_bytes
1086 elif out_type == 'IV':
1087 out_bytes = self.alg_aead.iv_bytes
1088 elif out_type == "Group Encryption Key":
1089 out_bytes = self.alg_signature_enc.key_bytes
1090 else:
1091 raise ValueError("Output type not recognized")
1093 info = [
1094 role_id,
1095 self.id_context,
1096 self.alg_aead.value,
1097 out_type,
1098 out_bytes
1099 ]
1100 return self._kdf_lowlevel(salt, ikm, info, out_bytes)
1102 def _kdf_for_keystreams(self, piv_generated_by, salt, ikm, role_id, out_type):
1103 """The HKDF as used to derive the keystreams of oscore-groupcomm."""
1105 out_bytes = self.alg_signature.signature_length
1107 assert out_type in (INFO_TYPE_KEYSTREAM_REQUEST, INFO_TYPE_KEYSTREAM_RESPONSE), "Output type not recognized"
1109 info = [
1110 piv_generated_by,
1111 self.id_context,
1112 out_type,
1113 out_bytes
1114 ]
1115 return self._kdf_lowlevel(salt, ikm, info, out_bytes)
1117 def _kdf_lowlevel(self, salt: bytes, ikm: bytes, info: list, l: int) -> bytes:
1118 """The HKDF function as used in RFC8613 and oscore-groupcomm (notated
1119 there as ``something = HKDF(...)``
1121 Note that `info` typically contains `L` at some point.
1123 When `info` takes the conventional structure of pid, id_context,
1124 ald_aead, type, L], it may make sense to extend the `_kdf` function to
1125 support that case, or `_kdf_for_keystreams` for a different structure, as
1126 they are the more high-level tools."""
1127 hkdf = HKDF(
1128 algorithm=self.hashfun,
1129 length=l,
1130 salt=salt,
1131 info=cbor.dumps(info),
1132 backend=_hash_backend,
1133 )
1134 expanded = hkdf.derive(ikm)
1135 return expanded
1137 def derive_keys(self, master_salt, master_secret):
1138 """Populate sender_key, recipient_key and common_iv from the algorithm,
1139 hash function and id_context already configured beforehand, and from
1140 the passed salt and secret."""
1142 self.sender_key = self._kdf(master_salt, master_secret, self.sender_id, 'Key')
1143 self.recipient_key = self._kdf(master_salt, master_secret, self.recipient_id, 'Key')
1145 self.common_iv = self._kdf(master_salt, master_secret, b"", 'IV')
1147 # really more of the Credentials interface
1149 def get_oscore_context_for(self, unprotected):
1150 """Return a sutiable context (most easily self) for an incoming request
1151 if its unprotected data (COSE_KID, COSE_KID_CONTEXT) fit its
1152 description. If it doesn't match, it returns None.
1154 The default implementation just strictly checks for whether kid and any
1155 kid context match (not matching if a local KID context is set but none
1156 is given in the request); modes like Group OSCORE can spin up aspect
1157 objects here.
1158 """
1159 if unprotected.get(COSE_KID, None) == self.recipient_id and unprotected.get(COSE_KID_CONTEXT, None) == self.id_context:
1160 return self
1162class ReplayWindow:
1163 """A regular replay window of a fixed size.
1165 It is implemented as an index and a bitfield (represented by an integer)
1166 whose least significant bit represents the seqyence number of the index,
1167 and a 1 indicates that a number was seen. No shenanigans around implicit
1168 leading ones (think floating point normalization) happen.
1170 >>> w = ReplayWindow(32, lambda: None)
1171 >>> w.initialize_empty()
1172 >>> w.strike_out(5)
1173 >>> w.is_valid(3)
1174 True
1175 >>> w.is_valid(5)
1176 False
1177 >>> w.strike_out(0)
1178 >>> w.strike_out(1)
1179 >>> w.strike_out(2)
1180 >>> w.is_valid(1)
1181 False
1183 Jumping ahead by the window size invalidates older numbers:
1185 >>> w.is_valid(4)
1186 True
1187 >>> w.strike_out(35)
1188 >>> w.is_valid(4)
1189 True
1190 >>> w.strike_out(36)
1191 >>> w.is_valid(4)
1192 False
1194 Usage safety
1195 ------------
1197 For every key, the replay window can only be initielized empty once. On
1198 later uses, it needs to be persisted by storing the output of
1199 self.persist() somewhere and loaded from that persisted data.
1201 It is acceptable to store persistance data in the strike_out_callback, but
1202 that must then ensure that the data is written (flushed to a file or
1203 committed to a database), but that is usually inefficient.
1205 Stability
1206 ---------
1208 This class is not considered for stabilization yet and an implementation
1209 detail of the SecurityContext implementation(s).
1210 """
1212 _index = None
1213 """Sequence number represented by the least significant bit of _bitfield"""
1214 _bitfield = None
1215 """Integer interpreted as a bitfield, self._size wide. A digit 1 at any bit
1216 indicates that the bit's index (its power of 2) plus self._index was
1217 already seen."""
1219 def __init__(self, size, strike_out_callback):
1220 self._size = size
1221 self.strike_out_callback = strike_out_callback
1223 def is_initialized(self):
1224 return self._index is not None
1226 def initialize_empty(self):
1227 self._index = 0
1228 self._bitfield = 0
1230 def initialize_from_persisted(self, persisted):
1231 self._index = persisted['index']
1232 self._bitfield = persisted['bitfield']
1234 def initialize_from_freshlyseen(self, seen):
1235 """Initialize the replay window with a particular value that is just
1236 being observed in a fresh (ie. generated by the peer later than any
1237 messages processed before state was lost here) message. This marks the
1238 seen sequence number and all preceding it as invalid, and and all later
1239 ones as valid."""
1240 self._index = seen
1241 self._bitfield = 1
1243 def is_valid(self, number):
1244 if number < self._index:
1245 return False
1246 if number >= self._index + self._size:
1247 return True
1248 return (self._bitfield >> (number - self._index)) & 1 == 0
1250 def strike_out(self, number):
1251 if not self.is_valid(number):
1252 raise ValueError("Sequence number is not valid any more and "
1253 "thus can't be removed from the window")
1254 overshoot = number - (self._index + self._size - 1)
1255 if overshoot > 0:
1256 self._index += overshoot
1257 self._bitfield >>= overshoot
1258 assert self.is_valid(number)
1259 self._bitfield |= 1 << (number - self._index)
1261 self.strike_out_callback()
1263 def persist(self):
1264 """Return a dict containing internal state which can be passed to init
1265 to recreated the replay window."""
1267 return {'index': self._index, 'bitfield': self._bitfield}
1269class FilesystemSecurityContext(CanProtect, CanUnprotect, SecurityContextUtils):
1270 """Security context stored in a directory as distinct files containing
1271 containing
1273 * Master secret, master salt, sender and recipient ID,
1274 optionally algorithm, the KDF hash function, and replay window size
1275 (settings.json and secrets.json, where the latter is typically readable
1276 only for the user)
1277 * sequence numbers and replay windows (sequence.json, the only file the
1278 process needs write access to)
1280 The static parameters can all either be placed in settings.json or
1281 secrets.json, but must not be present in both; the presence of either file
1282 is sufficient.
1284 .. warning::
1286 Security contexts must never be copied around and used after another
1287 copy was used. They should only ever be moved, and if they are copied
1288 (eg. as a part of a system backup), restored contexts must not be used
1289 again; they need to be replaced with freshly created ones.
1291 An additional file named `lock` is created to prevent the accidental use of
1292 a context by to concurrent programs.
1294 Note that the sequence number file is updated in an atomic fashion which
1295 requires file creation privileges in the directory. If privilege separation
1296 between settings/key changes and sequence number changes is desired, one
1297 way to achieve that on Linux is giving the aiocoap process's user group
1298 write permissions on the directory and setting the sticky bit on the
1299 directory, thus forbidding the user to remove the settings/secret files not
1300 owned by him.
1302 Writes due to sent sequence numbers are reduced by applying a variation on
1303 the mechanism of RFC8613 Appendix B.1.1 (incrementing the persisted sender
1304 seqence number in steps of `k`). That value is automatically grown from
1305 sequence_number_chunksize_start up to sequence_number_chunksize_limit.
1306 At runtime, the receive window is not stored but kept indeterminate. In
1307 case of an abnormal shutdown, the server uses the mechanism described in
1308 Appendix B.1.2 to recover.
1309 """
1311 class LoadError(ValueError):
1312 """Exception raised with a descriptive message when trying to load a
1313 faulty security context"""
1315 def __init__(
1316 self,
1317 basedir,
1318 sequence_number_chunksize_start=10,
1319 sequence_number_chunksize_limit=10000,
1320 ):
1321 self.basedir = basedir
1323 self.lockfile = filelock.FileLock(os.path.join(basedir, 'lock'))
1324 # 0.001: Just fail if it can't be acquired
1325 # See https://github.com/benediktschmitt/py-filelock/issues/57
1326 try:
1327 self.lockfile.acquire(timeout=0.001)
1328 # see https://github.com/PyCQA/pycodestyle/issues/703
1329 except: # noqa: E722
1330 # No lock, no loading, no need to fail in __del__
1331 self.lockfile = None
1332 raise
1334 # Always enabled as committing to a file for every received request
1335 # would be a terrible burden.
1336 self.echo_recovery = secrets.token_bytes(8)
1338 try:
1339 self._load()
1340 except KeyError as k:
1341 raise self.LoadError("Configuration key missing: %s" % (k.args[0],))
1343 self.sequence_number_chunksize_start = sequence_number_chunksize_start
1344 self.sequence_number_chunksize_limit = sequence_number_chunksize_limit
1345 self.sequence_number_chunksize = sequence_number_chunksize_start
1347 self.sequence_number_persisted = self.sender_sequence_number
1349 def _load(self):
1350 # doesn't check for KeyError on every occasion, relies on __init__ to
1351 # catch that
1353 data = {}
1354 for readfile in ("secret.json", "settings.json"):
1355 try:
1356 with open(os.path.join(self.basedir, readfile)) as f:
1357 filedata = json.load(f)
1358 except FileNotFoundError:
1359 continue
1361 for (key, value) in filedata.items():
1362 if key.endswith('_hex'):
1363 key = key[:-4]
1364 value = binascii.unhexlify(value)
1365 elif key.endswith('_ascii'):
1366 key = key[:-6]
1367 value = value.encode('ascii')
1369 if key in data:
1370 raise self.LoadError("Datum %r present in multiple input files at %r." % (key, self.basedir))
1372 data[key] = value
1374 self.alg_aead = algorithms[data.get('algorithm', DEFAULT_ALGORITHM)]
1375 self.hashfun = hashfunctions[data.get('kdf-hashfun', DEFAULT_HASHFUNCTION)]
1377 windowsize = data.get('window', DEFAULT_WINDOWSIZE)
1378 if not isinstance(windowsize, int):
1379 raise self.LoadError("Non-integer replay window")
1381 self.sender_id = data['sender-id']
1382 self.recipient_id = data['recipient-id']
1384 if max(len(self.sender_id), len(self.recipient_id)) > self.alg_aead.iv_bytes - 6:
1385 raise self.LoadError("Sender or Recipient ID too long (maximum length %s for this algorithm)" % (self.alg_aead.iv_bytes - 6))
1387 master_secret = data['secret']
1388 master_salt = data.get('salt', b'')
1389 self.id_context = data.get('id-context', None)
1391 self.derive_keys(master_salt, master_secret)
1393 self.recipient_replay_window = ReplayWindow(windowsize, self._replay_window_changed)
1394 try:
1395 with open(os.path.join(self.basedir, 'sequence.json')) as f:
1396 sequence = json.load(f)
1397 except FileNotFoundError:
1398 self.sender_sequence_number = 0
1399 self.recipient_replay_window.initialize_empty()
1400 self.replay_window_persisted = True
1401 else:
1402 self.sender_sequence_number = int(sequence['next-to-send'])
1403 received = sequence['received']
1404 if received == "unknown":
1405 # The replay window will stay uninitialized, which triggers
1406 # Echo recovery
1407 self.replay_window_persisted = False
1408 else:
1409 try:
1410 self.recipient_replay_window.initialize_from_persisted(received)
1411 except (ValueError, TypeError, KeyError):
1412 # Not being particularly careful about what could go wrong: If
1413 # someone tampers with the replay data, we're already in *big*
1414 # trouble, of which I fail to see how it would become worse
1415 # than a crash inside the application around "failure to
1416 # right-shift a string" or that like; at worst it'd result in
1417 # nonce reuse which tampering with the replay window file
1418 # already does.
1419 raise self.LoadError("Persisted replay window state was not understood")
1420 self.replay_window_persisted = True
1422 # This is called internally whenever a new sequence number is taken or
1423 # crossed out from the window, and blocks a lot; B.1 mode mitigates that.
1424 #
1425 # Making it async and block in a threadpool would mitigate the blocking of
1426 # other messages, but the more visible effect of this will be that no
1427 # matter if sync or async, a reply will need to wait for a file sync
1428 # operation to conclude.
1429 def _store(self):
1430 tmphand, tmpnam = tempfile.mkstemp(dir=self.basedir,
1431 prefix='.sequence-', suffix='.json', text=True)
1433 data = {"next-to-send": self.sequence_number_persisted}
1434 if not self.replay_window_persisted:
1435 data['received'] = 'unknown'
1436 else:
1437 data['received'] = self.recipient_replay_window.persist()
1439 # Using io.open (instead os.fdopen) and binary / write with encode
1440 # rather than dumps as that works even while the interpreter is
1441 # shutting down.
1442 #
1443 # This can be relaxed when there is a defined shutdown sequence for
1444 # security contexts that's triggered from the general context shutdown
1445 # -- but right now, there isn't.
1446 with io.open(tmphand, 'wb') as tmpfile:
1447 tmpfile.write(json.dumps(data).encode('utf8'))
1448 tmpfile.flush()
1449 os.fsync(tmpfile.fileno())
1451 os.replace(tmpnam, os.path.join(self.basedir, 'sequence.json'))
1453 def _replay_window_changed(self):
1454 if self.replay_window_persisted:
1455 # Just remove the sequence numbers once from the file
1456 self.replay_window_persisted = False
1457 self._store()
1459 def post_seqnoincrease(self):
1460 if self.sender_sequence_number > self.sequence_number_persisted:
1461 self.sequence_number_persisted += self.sequence_number_chunksize
1463 self.sequence_number_chunksize = min(self.sequence_number_chunksize * 2, self.sequence_number_chunksize_limit)
1464 # FIXME: this blocks -- see https://github.com/chrysn/aiocoap/issues/178
1465 self._store()
1467 # The = case would only happen if someone deliberately sets all
1468 # numbers to 1 to force persisting on every step
1469 assert self.sender_sequence_number <= self.sequence_number_persisted
1471 def _destroy(self):
1472 """Release the lock file, and ensure tha he object has become
1473 unusable.
1475 If there is unpersisted state from B.1 operation, the actually used
1476 number and replay window gets written back to the file to allow
1477 resumption without wasting digits or round-trips.
1478 """
1479 # FIXME: Arrange for a more controlled shutdown through the credentials
1481 self.replay_window_persisted = True
1482 self.sequence_number_persisted = self.sender_sequence_number
1483 self._store()
1485 del self.sender_key
1486 del self.recipient_key
1488 os.unlink(self.lockfile.lock_file)
1489 self.lockfile.release()
1491 self.lockfile = None
1493 def __del__(self):
1494 if self.lockfile is not None:
1495 self._destroy()
1497class GroupContext(BaseSecurityContext):
1498 is_signing = True
1499 external_aad_is_group = True
1500 responses_send_kid = True
1502 # This is None iff the group does not support group mode
1503 alg_signature_enc: Optional[AeadAlgorithm]
1504 # This is None iff the group does not support group mode
1505 alg_signature: Optional[AlgorithmCountersign]
1506 # This is None iff the group does not support pairwise
1507 #
1508 # This is also of type AlgorithmCountersign because the staticstatic
1509 # function is sitting on the same type.
1510 alg_pairwise_key_agreement: Optional[AlgorithmCountersign]
1512 @abc.abstractproperty
1513 def private_key(self):
1514 """Private key used to sign outgoing messages.
1516 Contexts not designed to send messages may raise a RuntimeError here;
1517 that necessity may later go away if some more accurate class modelling
1518 is found."""
1520 @abc.abstractproperty
1521 def recipient_public_key(self):
1522 """Public key used to verify incoming messages.
1524 Contexts not designed to receive messages (because they'd have aspects
1525 for that) may raise a RuntimeError here; that necessity may later go
1526 away if some more accurate class modelling is found."""
1528class SimpleGroupContext(GroupContext, CanProtect, CanUnprotect, SecurityContextUtils):
1529 """A context for an OSCORE group
1531 This is a non-persistable version of a group context that does not support
1532 any group manager or rekeying; it is set up statically at startup.
1534 It is intended for experimentation and demos, but aims to be correct enough
1535 to be usable securely.
1536 """
1538 # set during initialization
1539 private_key = None
1540 sender_auth_cred = None
1542 def __init__(self, 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=COSE_KCCS):
1543 self.sender_id = sender_id
1544 self.id_context = group_id
1545 self.private_key = private_key
1546 self.alg_aead = alg_aead
1547 self.hashfun = hashfun
1548 self.alg_signature = alg_signature
1549 self.alg_signature_enc = alg_signature_enc
1550 self.alg_pairwise_key_agreement = alg_pairwise_key_agreement
1551 self.sender_auth_cred = sender_auth_cred
1552 self.group_manager_cred = group_manager_cred
1553 self.cred_fmt = cred_fmt
1555 self.peers = peers.keys()
1556 self.recipient_public_keys = {k: self._parse_credential(v) for (k, v) in peers.items()}
1557 self.recipient_auth_creds = peers
1558 self.recipient_replay_windows = {}
1559 for k in self.peers:
1560 # no need to persist, the whole group is ephemeral
1561 w = ReplayWindow(32, lambda: None)
1562 w.initialize_empty()
1563 self.recipient_replay_windows[k] = w
1565 self.derive_keys(master_salt, master_secret)
1566 self.sender_sequence_number = 0
1568 sender_public_key = self._parse_credential(sender_auth_cred)
1569 if self.alg_signature.public_from_private(self.private_key) != sender_public_key:
1570 raise ValueError("The key in the provided sender credential does not match the private key")
1572 def _parse_credential(self, credential: bytes):
1573 """Extract the public key (in the public_key format the respective
1574 AlgorithmCountersign needs) from credentials. This raises a ValueError
1575 if the credentials do not match the group's cred_fmt, or if the
1576 parameters do not match those configured in the group.
1578 This currently discards any information that is present in the
1579 credential that exceeds the key. (In a future version, this could
1580 return both the key and extracted other data, where that other data
1581 would be stored with the peer this is parsed from).
1582 """
1584 if self.cred_fmt != COSE_KCCS:
1585 raise ValueError("Credential parsing is currently only implemented for CCSs")
1587 try:
1588 parsed = cbor.loads(credential)
1589 except cbor.CBORDecodeError as e:
1590 raise ValueError("CCS not in CBOR format") from e
1592 CWT_CLAIM_CNF = 8
1593 CWT_CNF_COSE_KEY = 1
1594 if (
1595 not isinstance(parsed, dict)
1596 or CWT_CLAIM_CNF not in parsed
1597 or not isinstance(parsed[CWT_CLAIM_CNF], dict)
1598 or CWT_CNF_COSE_KEY not in parsed[CWT_CLAIM_CNF]
1599 ):
1600 raise ValueError("CCS must contain a COSE Key in a CNF")
1602 COSE_KEY_COMMON_KTY = 1
1603 COSE_KTY_OKP = 1
1604 COSE_KTY_EC2 = 2
1605 COSE_KEY_COMMON_ALG = 3
1606 COSE_KEY_OKP_CRV = -1
1607 COSE_KEY_OKP_X = -2
1608 COSE_KEY_EC2_X = -2
1609 COSE_KEY_EC2_Y = -3
1610 # eg. {1: 1, 3: -8, -1: 6, -2: h'77 / ... / 88'}
1611 cose_key = parsed[CWT_CLAIM_CNF][CWT_CNF_COSE_KEY]
1612 if not isinstance(cose_key, dict):
1613 raise ValueError("COSE Key in CCS must be a map")
1615 if (
1616 cose_key.get(COSE_KEY_COMMON_KTY) == COSE_KTY_OKP
1617 and cose_key.get(COSE_KEY_COMMON_ALG) == Ed25519.value
1618 and cose_key.get(COSE_KEY_OKP_CRV) == Ed25519.curve_number
1619 and COSE_KEY_OKP_X in cose_key
1620 ):
1621 return cose_key[COSE_KEY_OKP_X]
1622 elif (
1623 cose_key.get(COSE_KEY_COMMON_KTY) == COSE_KTY_EC2
1624 and cose_key.get(COSE_KEY_COMMON_ALG) == ECDSA_SHA256_P256.value
1625 and COSE_KEY_EC2_X in cose_key
1626 and COSE_KEY_EC2_Y in cose_key
1627 ):
1628 return ECDSA_SHA256_P256().from_public_parts(
1629 x=cose_key[COSE_KEY_EC2_X],
1630 y=cose_key[COSE_KEY_EC2_Y],
1631 )
1632 else:
1633 raise ValueError("Key type not recognized from CCS key %r" % cose_key)
1635 return cbor.loads(credential)[8][1][-2]
1637 def __repr__(self):
1638 return "<%s with group %r sender_id %r and %d peers>" % (
1639 type(self).__name__,
1640 self.id_context.hex(),
1641 self.sender_id.hex(),
1642 len(self.peers),
1643 )
1645 @property
1646 def recipient_public_key(self):
1647 raise RuntimeError("Group context without key indication was used for verification")
1649 def derive_keys(self, master_salt, master_secret):
1650 # FIXME unify with parent?
1652 self.sender_key = self._kdf(master_salt, master_secret, self.sender_id, 'Key')
1653 self.recipient_keys = {recipient_id: self._kdf(master_salt, master_secret, recipient_id, 'Key') for recipient_id in self.peers}
1655 self.common_iv = self._kdf(master_salt, master_secret, b"", 'IV')
1657 # but this one is new
1659 self.group_encryption_key = self._kdf(master_salt, master_secret, b"", "Group Encryption Key")
1661 def post_seqnoincrease(self):
1662 """No-op because it's ephemeral"""
1664 def context_from_response(self, unprotected_bag) -> CanUnprotect:
1665 # sender ID *needs to be* here -- if this were a pairwise request, it
1666 # would not run through here
1667 try:
1668 sender_kid = unprotected_bag[COSE_KID]
1669 except KeyError:
1670 raise DecodeError("Group server failed to send own sender KID")
1672 if COSE_COUNTERSIGNATURE0 in unprotected_bag:
1673 return _GroupContextAspect(self, sender_kid)
1674 else:
1675 return _PairwiseContextAspect(self, sender_kid)
1677 def get_oscore_context_for(self, unprotected):
1678 if unprotected.get(COSE_KID_CONTEXT, None) != self.id_context:
1679 return None
1681 kid = unprotected.get(COSE_KID, None)
1682 if kid in self.peers:
1683 if COSE_COUNTERSIGNATURE0 in unprotected:
1684 return _GroupContextAspect(self, kid)
1685 elif self.recipient_public_keys[kid] is DETERMINISTIC_KEY:
1686 return _DeterministicUnprotectProtoAspect(self, kid)
1687 else:
1688 return _PairwiseContextAspect(self, kid)
1690 # yet to stabilize...
1692 def pairwise_for(self, recipient_id):
1693 return _PairwiseContextAspect(self, recipient_id)
1695 def for_sending_deterministic_requests(self, deterministic_id, target_server: Optional[bytes]):
1696 return _DeterministicProtectProtoAspect(self, deterministic_id, target_server)
1698class _GroupContextAspect(GroupContext, CanUnprotect, SecurityContextUtils):
1699 """The concrete context this host has with a particular peer
1701 As all actual data is stored in the underlying groupcontext, this acts as
1702 an accessor to that object (which picks the right recipient key).
1704 This accessor is for receiving messages in group mode from a particular
1705 peer; it does not send (and turns into a pairwise context through
1706 context_for_response before it comes to that).
1707 """
1709 def __init__(self, groupcontext, recipient_id):
1710 self.groupcontext = groupcontext
1711 self.recipient_id = recipient_id
1713 def __repr__(self):
1714 return "<%s inside %r with the peer %r>" % (
1715 type(self).__name__,
1716 self.groupcontext,
1717 self.recipient_id.hex(),
1718 )
1720 id_context = property(lambda self: self.groupcontext.id_context)
1721 alg_aead = property(lambda self: self.groupcontext.alg_aead)
1722 alg_signature = property(lambda self: self.groupcontext.alg_signature)
1723 alg_signature_enc = property(lambda self: self.groupcontext.alg_signature_enc)
1724 alg_pairwise_key_agreement = property(lambda self: self.groupcontext.alg_pairwise_key_agreement)
1725 group_manager_cred = property(lambda self: self.groupcontext.group_manager_cred)
1726 common_iv = property(lambda self: self.groupcontext.common_iv)
1728 hashfun = property(lambda self: self.groupcontext.hashfun)
1729 group_encryption_key = property(lambda self: self.groupcontext.group_encryption_key)
1731 recipient_key = property(lambda self: self.groupcontext.recipient_keys[self.recipient_id])
1732 recipient_public_key = property(lambda self: self.groupcontext.recipient_public_keys[self.recipient_id])
1733 recipient_auth_cred = property(lambda self: self.groupcontext.recipient_auth_creds[self.recipient_id])
1734 recipient_replay_window = property(lambda self: self.groupcontext.recipient_replay_windows[self.recipient_id])
1736 def context_for_response(self):
1737 return self.groupcontext.pairwise_for(self.recipient_id)
1739 @property
1740 def sender_auth_cred(self):
1741 raise RuntimeError("Could relay the sender auth credential from the group context, but it shouldn't matter here")
1743class _PairwiseContextAspect(GroupContext, CanProtect, CanUnprotect, SecurityContextUtils):
1744 is_signing = False
1746 def __init__(self, groupcontext, recipient_id):
1747 self.groupcontext = groupcontext
1748 self.recipient_id = recipient_id
1750 shared_secret = self.alg_pairwise_key_agreement.staticstatic(
1751 self.groupcontext.private_key,
1752 self.groupcontext.recipient_public_keys[recipient_id]
1753 )
1755 self.sender_key = self._kdf(
1756 self.groupcontext.sender_key,
1757 (
1758 self.groupcontext.sender_auth_cred
1759 + self.groupcontext.recipient_auth_creds[recipient_id]
1760 + shared_secret
1761 ),
1762 self.groupcontext.sender_id,
1763 'Key',
1764 )
1765 self.recipient_key = self._kdf(
1766 self.groupcontext.recipient_keys[recipient_id],
1767 (
1768 self.groupcontext.recipient_auth_creds[recipient_id]
1769 + self.groupcontext.sender_auth_cred
1770 + shared_secret
1771 ),
1772 self.recipient_id,
1773 'Key',
1774 )
1776 def __repr__(self):
1777 return "<%s based on %r with the peer %r>" % (
1778 type(self).__name__,
1779 self.groupcontext,
1780 self.recipient_id.hex(),
1781 )
1783 # FIXME: actually, only to be sent in requests
1784 id_context = property(lambda self: self.groupcontext.id_context)
1785 alg_aead = property(lambda self: self.groupcontext.alg_aead)
1786 hashfun = property(lambda self: self.groupcontext.hashfun)
1787 alg_signature = property(lambda self: self.groupcontext.alg_signature)
1788 alg_signature_enc = property(lambda self: self.groupcontext.alg_signature_enc)
1789 alg_pairwise_key_agreement = property(lambda self: self.groupcontext.alg_pairwise_key_agreement)
1790 group_manager_cred = property(lambda self: self.groupcontext.group_manager_cred)
1791 common_iv = property(lambda self: self.groupcontext.common_iv)
1792 sender_id = property(lambda self: self.groupcontext.sender_id)
1794 recipient_auth_cred = property(lambda self: self.groupcontext.recipient_auth_creds[self.recipient_id])
1795 sender_auth_cred = property(lambda self: self.groupcontext.sender_auth_cred)
1797 recipient_replay_window = property(lambda self: self.groupcontext.recipient_replay_windows[self.recipient_id])
1799 # Set at initialization
1800 recipient_key = None
1801 sender_key = None
1803 @property
1804 def sender_sequence_number(self):
1805 return self.groupcontext.sender_sequence_number
1806 @sender_sequence_number.setter
1807 def sender_sequence_number(self, new):
1808 self.groupcontext.sender_sequence_number = new
1810 def post_seqnoincrease(self):
1811 self.groupcontext.post_seqnoincrease()
1813 # same here -- not needed because not signing
1814 private_key = property(post_seqnoincrease)
1815 recipient_public_key = property(post_seqnoincrease)
1817 def context_from_response(self, unprotected_bag) -> CanUnprotect:
1818 if unprotected_bag.get(COSE_KID, self.recipient_id) != self.recipient_id:
1819 raise DecodeError("Response coming from a different server than requested, not attempting to decrypt")
1821 if COSE_COUNTERSIGNATURE0 in unprotected_bag:
1822 # It'd be an odd thing to do, but it's source verified, so the
1823 # server hopefully has reasons to make this readable to other group
1824 # members.
1825 return _GroupContextAspect(self.groupcontext, self.recipient_id)
1826 else:
1827 return self
1829class _DeterministicProtectProtoAspect(CanProtect, SecurityContextUtils):
1830 """This implements the sending side of Deterministic Requests.
1832 While simialr to a _PairwiseContextAspect, it only derives the key at
1833 protection time, as the plain text is hashed into the key."""
1835 deterministic_hashfun = hashes.SHA256()
1837 def __init__(self, groupcontext, sender_id, target_server: Optional[bytes]):
1838 self.groupcontext = groupcontext
1839 self.sender_id = sender_id
1840 self.target_server = target_server
1842 def __repr__(self):
1843 return "<%s based on %r with the sender ID %r%s>" % (
1844 type(self).__name__,
1845 self.groupcontext,
1846 self.sender_id.hex(),
1847 "limited to responses from %s" % self.target_server if self.target_server is not None else ""
1848 )
1850 def new_sequence_number(self):
1851 return 0
1853 def post_seqnoincrease(self):
1854 pass
1856 def context_from_response(self, unprotected_bag):
1857 if self.target_server is None:
1858 if COSE_KID not in unprotected_bag:
1859 raise DecodeError("Server did not send a KID and no particular one was addressed")
1860 else:
1861 if unprotected_bag.get(COSE_KID, self.target_server) != self.target_server:
1862 raise DecodeError("Response coming from a different server than requested, not attempting to decrypt")
1864 if COSE_COUNTERSIGNATURE0 not in unprotected_bag:
1865 # Could just as well pass and later barf when the group context doesn't find a signature
1866 raise DecodeError("Response to deterministic request came from unsecure pairwise context")
1868 return _GroupContextAspect(self.groupcontext, unprotected_bag.get(COSE_KID, self.target_server))
1870 def _get_sender_key(self, outer_message, aad, plaintext, request_id):
1871 if outer_message.code.is_response():
1872 raise RuntimeError("Deterministic contexts shouldn't protect responses")
1874 basekey = self.groupcontext.recipient_keys[self.sender_id]
1876 h = hashes.Hash(self.deterministic_hashfun)
1877 h.update(basekey)
1878 h.update(aad)
1879 h.update(plaintext)
1880 request_hash = h.finalize()
1882 outer_message.opt.request_hash = request_hash
1883 outer_message.code = FETCH
1885 # By this time, the AADs have all been calculated already; setting this
1886 # for the benefit of the response parsing later
1887 request_id.request_hash = request_hash
1888 # FIXME I don't think this ever comes to bear but want to be sure
1889 # before removing this line (this should only be client-side)
1890 request_id.can_reuse_nonce = False
1891 # FIXME: we're still sending a h'00' PIV. Not wrong, just a wasted byte.
1893 return self._kdf(basekey, request_hash, self.sender_id, 'Key')
1895 external_aad_is_group = True
1897 # details needed for various operations, especially eAAD generation
1898 alg_aead = property(lambda self: self.groupcontext.alg_aead)
1899 hashfun = property(lambda self: self.groupcontext.hashfun)
1900 common_iv = property(lambda self: self.groupcontext.common_iv)
1901 id_context = property(lambda self: self.groupcontext.id_context)
1902 alg_signature = property(lambda self: self.groupcontext.alg_signature)
1904class _DeterministicUnprotectProtoAspect(CanUnprotect, SecurityContextUtils):
1905 """This implements the sending side of Deterministic Requests.
1907 While simialr to a _PairwiseContextAspect, it only derives the key at
1908 unprotection time, based on information given as Request-Hash."""
1910 # Unless None, this is the value by which the running process recognizes
1911 # that the second phase of a B.1.2 replay window recovery Echo option comes
1912 # from the current process, and thus its sequence number is fresh
1913 echo_recovery = None
1915 deterministic_hashfun = hashes.SHA256()
1917 class ZeroIsAlwaysValid:
1918 """Special-purpose replay window that accepts 0 indefinitely"""
1920 def is_initialized(self):
1921 return True
1923 def is_valid(self, number):
1924 # No particular reason to be lax here
1925 return number == 0
1927 def strike_out(self, number):
1928 # FIXME: I'd rather indicate here that it's a potential replay, have the
1929 # request_id.can_reuse_nonce = False
1930 # set here rather than in _post_decrypt_checks, and thus also get
1931 # the check for whether it's a safe method
1932 pass
1934 def persist(self):
1935 pass
1937 def __init__(self, groupcontext, recipient_id):
1938 self.groupcontext = groupcontext
1939 self.recipient_id = recipient_id
1941 self.recipient_replay_window = self.ZeroIsAlwaysValid()
1943 def __repr__(self):
1944 return "<%s based on %r with the recipient ID %r>" % (
1945 type(self).__name__,
1946 self.groupcontext,
1947 self.recipient_id.hex(),
1948 )
1950 def context_for_response(self):
1951 return self.groupcontext
1953 def _get_recipient_key(self, protected_message):
1954 return self._kdf(self.groupcontext.recipient_keys[self.recipient_id], protected_message.opt.request_hash, self.recipient_id, 'Key')
1956 def _post_decrypt_checks(self, aad, plaintext, protected_message, request_id):
1957 if plaintext[0] not in (GET, FETCH): # FIXME: "is safe"
1958 # FIXME: accept but return inner Unauthorized. (Raising Unauthorized
1959 # here would just create an unprotected Unauthorized, which is not
1960 # what's spec'd for here)
1961 raise ProtectionInvalid("Request was not safe")
1963 basekey = self.groupcontext.recipient_keys[self.recipient_id]
1965 h = hashes.Hash(self.deterministic_hashfun)
1966 h.update(basekey)
1967 h.update(aad)
1968 h.update(plaintext)
1969 request_hash = h.finalize()
1971 if request_hash != protected_message.opt.request_hash:
1972 raise ProtectionInvalid("Client's hash of the plaintext diverges from the actual request hash")
1974 # This is intended for the protection of the response, and the
1975 # later use in signature in the unprotect function is not happening
1976 # here anyway, neither is the later use for Echo requests
1977 request_id.request_hash = request_hash
1978 request_id.can_reuse_nonce = False
1980 external_aad_is_group = True
1982 # details needed for various operations, especially eAAD generation
1983 alg_aead = property(lambda self: self.groupcontext.alg_aead)
1984 hashfun = property(lambda self: self.groupcontext.hashfun)
1985 common_iv = property(lambda self: self.groupcontext.common_iv)
1986 id_context = property(lambda self: self.groupcontext.id_context)
1987 alg_signature = property(lambda self: self.groupcontext.alg_signature)
1989def verify_start(message):
1990 """Extract the unprotected COSE options from a
1991 message for the verifier to then pick a security context to actually verify
1992 the message. (Future versions may also report fields from both unprotected
1993 and protected, if the protected bag is ever used with OSCORE.).
1995 Call this only requests; for responses, you'll have to know the security
1996 context anyway, and there is usually no information to be gained."""
1998 _, _, unprotected, _ = CanUnprotect._extract_encrypted0(message)
2000 return unprotected
2004_getattr__ = deprecation_getattr({
2005 'COSE_COUNTERSINGATURE0': 'COSE_COUNTERSIGNATURE0',
2006 'Algorithm': 'AeadAlgorithm',
2007 }, globals())