129 lines
4.2 KiB
Python
129 lines
4.2 KiB
Python
"""Pure IoTivity manufacturer-certificate OwnerPSK derivation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
import hashlib
|
|
import hmac
|
|
from types import MappingProxyType
|
|
from typing import Final
|
|
|
|
|
|
CONFIRMED_MFG_CERTIFICATE_OXM_LABEL: Final = b"x.org.iotivity.conmfgcert"
|
|
STANDARD_MFG_CERTIFICATE_OXM_LABEL: Final = b"oic.sec.doxm.mfgcert"
|
|
|
|
# OpenSSL cipher names mapped to the key-block lengths used by IoTivity's
|
|
# CAGenerateOwnerPSK implementation.
|
|
MFG_CERTIFICATE_KEY_BLOCK_LENGTHS: Final[Mapping[str, int]] = MappingProxyType(
|
|
{
|
|
"ECDHE-ECDSA-AES128-SHA256": 96,
|
|
"ECDHE-ECDSA-AES128-CCM": 40,
|
|
"ECDHE-ECDSA-AES128-CCM8": 40,
|
|
"ECDHE-ECDSA-AES128-GCM-SHA256": 120,
|
|
"AES256-SHA256": 128,
|
|
"ECDHE-ECDSA-AES256-SHA384": 160,
|
|
"ECDHE-ECDSA-AES256-GCM-SHA384": 184,
|
|
"AES128-GCM-SHA256": 120,
|
|
}
|
|
)
|
|
|
|
_TLS_MASTER_SECRET_BYTES: Final = 48
|
|
_TLS_RANDOM_BYTES: Final = 32
|
|
_OCF_UUID_BYTES: Final = 16
|
|
_OWNER_PSK_BYTES: Final = 16
|
|
|
|
|
|
def _require_bytes(name: str, value: bytes, length: int) -> bytes:
|
|
if not isinstance(value, bytes):
|
|
raise TypeError(f"{name} must be bytes")
|
|
if len(value) != length:
|
|
raise ValueError(f"{name} must be exactly {length} bytes")
|
|
return value
|
|
|
|
|
|
def _require_uuid(name: str, value: bytes) -> bytes:
|
|
value = _require_bytes(name, value, _OCF_UUID_BYTES)
|
|
if not any(value):
|
|
raise ValueError(f"{name} must not be the nil UUID")
|
|
return value
|
|
|
|
|
|
def _tls12_p_hash_sha256(
|
|
key: bytes,
|
|
label: bytes,
|
|
random1: bytes,
|
|
random2: bytes,
|
|
length: int,
|
|
) -> bytes:
|
|
seed = label + random1 + random2
|
|
a_value = hmac.new(key, seed, hashlib.sha256).digest()
|
|
output = bytearray()
|
|
while len(output) < length:
|
|
output.extend(hmac.new(key, a_value + seed, hashlib.sha256).digest())
|
|
a_value = hmac.new(key, a_value, hashlib.sha256).digest()
|
|
return bytes(output[:length])
|
|
|
|
|
|
def derive_mfg_certificate_owner_psk(
|
|
*,
|
|
master_secret: bytes,
|
|
client_random: bytes,
|
|
server_random: bytes,
|
|
owner_uuid: bytes,
|
|
device_uuid: bytes,
|
|
cipher_name: str,
|
|
oxm_label: bytes,
|
|
) -> bytes:
|
|
"""Derive a 128-bit OwnerPSK from caller-supplied DTLS state.
|
|
|
|
This implements IoTivity's two-stage TLS 1.2 SHA-256 P_hash operation.
|
|
It performs no session access, network I/O, ownership writes, or storage.
|
|
The caller must supply state from an authenticated manufacturer-certificate
|
|
session and explicitly select the OXM label used by that transaction.
|
|
IoTivity's other 96-byte ECDH_ANON, ECDHE_PSK, and ECDHE_RSA mappings are
|
|
intentionally outside this helper's manufacturer-certificate allowlist.
|
|
"""
|
|
|
|
if not isinstance(cipher_name, str):
|
|
raise TypeError("cipher_name must be a string")
|
|
key_block_bytes = MFG_CERTIFICATE_KEY_BLOCK_LENGTHS.get(cipher_name)
|
|
if key_block_bytes is None:
|
|
raise ValueError("unexpected manufacturer-certificate DTLS cipher")
|
|
|
|
master_secret = _require_bytes(
|
|
"master_secret", master_secret, _TLS_MASTER_SECRET_BYTES
|
|
)
|
|
client_random = _require_bytes(
|
|
"client_random", client_random, _TLS_RANDOM_BYTES
|
|
)
|
|
server_random = _require_bytes(
|
|
"server_random", server_random, _TLS_RANDOM_BYTES
|
|
)
|
|
owner_uuid = _require_uuid("owner_uuid", owner_uuid)
|
|
device_uuid = _require_uuid("device_uuid", device_uuid)
|
|
if not isinstance(oxm_label, bytes):
|
|
raise TypeError("oxm_label must be bytes")
|
|
if oxm_label not in {
|
|
CONFIRMED_MFG_CERTIFICATE_OXM_LABEL,
|
|
STANDARD_MFG_CERTIFICATE_OXM_LABEL,
|
|
}:
|
|
raise ValueError("unexpected manufacturer-certificate OXM label")
|
|
|
|
key_block = _tls12_p_hash_sha256(
|
|
master_secret,
|
|
b"key expansion",
|
|
server_random,
|
|
client_random,
|
|
key_block_bytes,
|
|
)
|
|
# IoTivity's OTM callers pass the owner UUID first and the target device
|
|
# UUID second. The lower adapter's historical rsrc/prov parameter names
|
|
# describe those arguments inconsistently, so preserve the caller order.
|
|
return _tls12_p_hash_sha256(
|
|
key_block,
|
|
oxm_label,
|
|
owner_uuid,
|
|
device_uuid,
|
|
_OWNER_PSK_BYTES,
|
|
)
|