Skip to content

Commit

Permalink
More factoring out of ecdsa
Browse files Browse the repository at this point in the history
Extract needed parts of ecdsa.der as cm_der.py.
Provide crypto.* calls to avoid calling ecdsa directly.

The goal is to remove ecdsa entirely for CircuitPython use, using
mbedtls and/or Python to substitute.
  • Loading branch information
dhalbert committed Nov 27, 2024
1 parent 3798399 commit 7c3e311
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 34 deletions.
9 changes: 3 additions & 6 deletions circuitmatter/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
import binascii
import hashlib

import ecdsa
from ecdsa import der
from ecdsa.curves import NIST256p

from . import cm_der as der
from . import crypto, pase, tlv
from .data_model import Enum8

Expand Down Expand Up @@ -79,10 +78,8 @@ def generate_certificates(vendor_id=0xFFF1, product_id=0x8000, device_type=22, p

# From: https://github.com/project-chip/matter.js/blob/main/packages/protocol/src/certificate/CertificationDeclarationManager.ts
# NIST256p is the same as secp256r1
private_key = ecdsa.keys.SigningKey.from_string(
private_key = crypto.key_from_bytes(
b"\xae\xf3\x48\x41\x16\xe9\x48\x1e\xc5\x7b\xe0\x47\x2d\xf4\x1b\xf4\x99\x06\x4e\x50\x24\xad\x86\x9e\xca\x5e\x88\x98\x02\xd4\x80\x75", # noqa: E501 Line too long
curve=ecdsa.curves.NIST256p,
hashfunc=hashlib.sha256,
)
subject_key_identifier = (
b"\x62\xfa\x82\x33\x59\xac\xfa\xa9\x96\x3e\x1c\xfa\x14\x0a\xdd\xf5\x04\xf3\x71\x60"
Expand Down Expand Up @@ -187,7 +184,7 @@ def generate_dac(vendor_id, product_id, product_name, random_source) -> tuple[by
extensions,
)

pai_key = ecdsa.keys.SigningKey.from_der(PAI_KEY_DER, hashfunc=hashlib.sha256)
pai_key = crypto.key_from_der(PAI_KEY_DER)
signature = crypto.Sign_as_der(pai_key, certificate)

dac_cert = der.encode_sequence(
Expand Down
115 changes: 115 additions & 0 deletions circuitmatter/cm_der.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Derived from https://github.com/tlsfuzzer/python-ecdsa
# SPDX-FileCopyrightText: Copyright (c) 2010 Brian Warner
# SPDX-FileCopyrightText: Copyright (c) 2024 Dan Halbert for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import binascii


def int2byte(i):
return i.to_bytes(1, "big")


def encode_constructed(tag, value):
return int2byte(0xA0 + tag) + encode_length(len(value)) + value


def encode_integer(r):
assert r >= 0 # can't support negative numbers yet
h = f"{r:x}".encode()
if len(h) % 2:
h = b"0" + h
s = binascii.unhexlify(h)
num = s[0]
if num <= 0x7F:
return b"\x02" + encode_length(len(s)) + s
else:
# DER integers are two's complement, so if the first byte is
# 0x80-0xff then we need an extra 0x00 byte to prevent it from
# looking negative.
return b"\x02" + encode_length(len(s) + 1) + b"\x00" + s


def encode_bitstring(s, *, unused):
"""
Encode a binary string as a BIT STRING using :term:`DER` encoding.
Note, because there is no native Python object that can encode an actual
bit string, this function only accepts byte strings as the `s` argument.
The byte string is the actual bit string that will be encoded, padded
on the right (least significant bits, looking from big endian perspective)
to the first full byte. If the bit string has a bit length that is multiple
of 8, then the padding should not be included. For correct DER encoding
the padding bits MUST be set to 0.
Number of bits of padding need to be provided as the `unused` parameter.
In case they are specified as None, it means the number of unused bits
is already encoded in the string as the first byte.
Empty string must be encoded with `unused` specified as 0.
:param s: bytes to encode
:type s: bytes like object
:param unused: number of bits at the end of `s` that are unused, must be
between 0 and 7 (inclusive)
:type unused: int or None
:raises ValueError: when `unused` is too large or too small
:return: `s` encoded using DER
:rtype: bytes
"""
encoded_unused = b""
len_extra = 0
if not 0 <= unused <= 7:
raise ValueError("unused must be integer between 0 and 7")
if unused:
if not s:
raise ValueError("unused is non-zero but s is empty")
last = s[-1]
if last & (2**unused - 1):
raise ValueError("unused bits must be zeros in DER")
encoded_unused = int2byte(unused)
len_extra = 1
return b"\x03" + encode_length(len(s) + len_extra) + encoded_unused + s


def encode_octet_string(s):
return b"\x04" + encode_length(len(s)) + s


def encode_oid(first, second, *pieces):
assert 0 <= first < 2 and 0 <= second <= 39 or first == 2 and 0 <= second
body = bytearray(encode_number(40 * first + second))
for p in pieces:
body += encode_number(p)
return b"\x06" + encode_length(len(body)) + body


def encode_sequence(*encoded_pieces):
total_len = sum([len(p) for p in encoded_pieces])
return b"\x30" + encode_length(total_len) + b"".join(encoded_pieces)


def encode_number(n):
b128_digits = []
while n:
b128_digits.insert(0, (n & 0x7F) | 0x80)
n >>= 7
if not b128_digits:
b128_digits.append(0)
b128_digits[-1] &= 0x7F
return b"".join([int2byte(d) for d in b128_digits])


def encode_length(l):
assert l >= 0
if l < 0x80:
return int2byte(l)
s = f"{l:x}".encode()
if len(s) % 2:
s = b"0" + s
s = binascii.unhexlify(s)
llen = len(s)
return int2byte(0x80 | llen) + s
10 changes: 10 additions & 0 deletions circuitmatter/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,13 @@ def ECDH(private_key: ecdsa.keys.SigningKey, public_key: bytes) -> bytes:
ecdh.load_private_key(private_key)
ecdh.load_received_public_key_bytes(public_key)
return ecdh.generate_sharedsecret_bytes()


def key_from_bytes(key_bytes: bytes):
return ecdsa.keys.SigningKey.from_string(
key_bytes, curve=ecdsa.curves.NIST256p, hashfunc=hashlib.sha256
)


def key_from_der(der):
return ecdsa.keys.SigningKey.from_der(der, hashfunc=hashlib.sha256)
34 changes: 7 additions & 27 deletions circuitmatter/device_types/utility/root_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
import struct
import time

import ecdsa
from ecdsa import der

from circuitmatter import cm_der as der
from circuitmatter import crypto, interaction_model, tlv
from circuitmatter.clusters.device_management.basic_information import (
BasicInformationCluster,
Expand Down Expand Up @@ -125,9 +123,7 @@ def __init__(self, group_key_manager, random_source, mdns_server, port):
def restore(self, nonvolatile):
super().restore(nonvolatile)

self.dac_key = ecdsa.keys.SigningKey.from_der(
binascii.a2b_base64(nonvolatile["dac_key"]), hashfunc=hashlib.sha256
)
self.dac_key = crypto.key_from_der(binascii.a2b_base64(nonvolatile["dac_key"]))

if "pk" not in nonvolatile:
return
Expand All @@ -147,11 +143,7 @@ def restore(self, nonvolatile):
root_cert.ec_pub_key[1:], fabric_id, b"CompressedFabric", 64
)
self.compressed_fabric_ids.append(compressed_fabric_id)
signing_key = ecdsa.keys.SigningKey.from_string(
binascii.a2b_base64(self.encoded_noc_keys[i]),
curve=ecdsa.NIST256p,
hashfunc=hashlib.sha256,
)
signing_key = crypto.key_from_bytes(binascii.a2b_base64(self.encoded_noc_keys[i]))
self.noc_keys.append(signing_key)

node_id = struct.pack(">Q", fabric.NodeID).hex().upper()
Expand Down Expand Up @@ -192,11 +184,7 @@ def attestation_request(
attestation_tbs = elements.tobytes() + session.attestation_challenge
response = NodeOperationalCredentialsCluster.AttestationResponse()
response.AttestationElements = elements
response.AttestationSignature = self.dac_key.sign_deterministic(
attestation_tbs,
hashfunc=hashlib.sha256,
sigencode=ecdsa.util.sigencode_string,
)
response.AttestationSignature = crypto.Sign_as_string(self.dac_key, attestation_tbs)
return response

def csr_request(
Expand All @@ -207,9 +195,7 @@ def csr_request(
# Signing Request

self.new_key_for_update = args.IsForUpdateNOC
self.pending_signing_key = ecdsa.keys.SigningKey.generate(
curve=ecdsa.NIST256p, hashfunc=hashlib.sha256, entropy=self.random.urandom
)
self.pending_signing_key = crypto.GenerateKeyPair(self.random.urandom)

# DER encode the request
# https://www.rfc-editor.org/rfc/rfc2986 Section 4.2
Expand Down Expand Up @@ -253,11 +239,7 @@ def csr_request(
certification_request.append(signature_algorithm)

# Signature
signature = self.pending_signing_key.sign_deterministic(
certification_request_info,
hashfunc=hashlib.sha256,
sigencode=ecdsa.util.sigencode_der_canonize,
)
signature = crypto.Sign_as_der(self.pending_signing_key, certification_request_info)
certification_request.append(der.encode_bitstring(signature, unused=0))

# Generate a new key pair.
Expand All @@ -275,9 +257,7 @@ def csr_request(
# AttestationSignature = tlv.OctetStringMember(1, 64)
response = NodeOperationalCredentialsCluster.CSRResponse()
response.NOCSRElements = elements
response.AttestationSignature = self.dac_key.sign_deterministic(
nocsr_tbs, hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_string
)
response.AttestationSignature = crypto.Sign_as_string(self.dac_key, nocsr_tbs)
return response

def add_trusted_root_certificate(
Expand Down
1 change: 0 additions & 1 deletion circuitmatter/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import time

import cryptography
import ecdsa
from cryptography.hazmat.primitives.ciphers.aead import AESCCM

from . import case, crypto, protocol, tlv
Expand Down
34 changes: 34 additions & 0 deletions examples/blink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 Dan Halbert for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""Simple LED on and off as a light."""

import board
import digitalio

import circuitmatter as cm
from circuitmatter.device_types.lighting import on_off


class LED(on_off.OnOffLight):
def __init__(self, name, led):
super().__init__(name)
# self._led = led
# self._led.direction = digitalio.Direction.OUTPUT

def on(self):
print("_led set to on")
# self._led.value = True

def off(self):
print("_led set to off")
# self._led.value = False


matter = cm.CircuitMatter()
# led = LED("led1", digitalio.DigitalInOut(board.D13))
led = LED("led1", None)
matter.add_device(led)
while True:
matter.process_packets()

0 comments on commit 7c3e311

Please sign in to comment.