Skip to content

Commit

Permalink
IN PROGRESS: did helpers: ecdsa p-384 key loading
Browse files Browse the repository at this point in the history
Signed-off-by: John Andersen <[email protected]>
  • Loading branch information
pdxjohnny committed Nov 16, 2023
1 parent a4bc402 commit 40f5f68
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 4 deletions.
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
[pytest]
# https://docs.pytest.org/en/7.1.x/how-to/doctest.html#using-doctest-options
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
# Alternatively, options can be enabled by an inline comment in the doc test itself:
# >>> something_that_raises() # doctest: +IGNORE_EXCEPTION_DETAIL
# Traceback (most recent call last):
# ValueError: ...
addopts = --doctest-modules
308 changes: 304 additions & 4 deletions scitt_emulator/did_helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import os
import ast
import sys
import base64
import inspect
import urllib.parse
from typing import Optional, Callable

import multiformats


def did_web_to_url(
did_web_string: str,
Expand All @@ -18,20 +22,304 @@ def did_web_to_url(
)


class DIDKeyInvalidPublicKeyLengthError(ValueError):
"""
If the byte length of rawPublicKeyBytes does not match the expected public
key length for the associated multicodecValue, an invalidPublicKeyLength
error MUST be raised.
"""


class DIDKeyDecoderNotFoundError(NotImplementedError):
"""
Raised when we don't have a function implemented to decode the given key
"""


class DIDKeyDecoderError(Exception):
"""
Raised when we failed to decode a key from a did:key DID method
"""


class DIDKeyInvalidPublicKeyError(DIDKeyDecoderError):
"""
Raised when the raw bytes of a key are invalid during decode
"""


DID_KEY_METHOD = "did:key:"


def did_key_to_jwk_dict_is_p_384_startswith_z82(did_key: str) -> dict[str, str]:
did_key = did_key.replace(DID_KEY_METHOD, "", 1)
def did_key_decode(
did_key_without_method_or_key_type_prefix: str,
) -> tuple[str, bytes]:
return


import sys, base58, multibase, multicodec
import snoop


def did_key_decode_public_key(multibase_value: str) -> dict[str, str]:
# 3.1.2.3
# Decode multibaseValue using the base58-btc multibase alphabet and set
# multicodecValue to the multicodec header for the decoded value.
multibase_value_decoded = multibase.decode(multibase_value)
# Implementers are cautioned to ensure that the multicodecValue is set to
# the result after performing varint decoding.
multicodec_value = multicodec.extract_prefix(multibase_value_decoded)
# Set the rawPublicKeyBytes to the bytes remaining after the multicodec
# header.
raw_public_key_bytes = multicodec.remove_prefix(multibase_value_decoded)
# Return multicodecValue and rawPublicKeyBytes as the decodedPublicKey.
return multicodec_value, raw_public_key_bytes


class _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
pass


MULTICODEC_VALUE_NOT_FOUND_IN_TABLE = _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE()

# Multicodec hexadecimal value, public key, byte length, Description
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY = 0xE7
MULTICODEC_HEX_X25519_PUBLIC_KEY = 0xEC
MULTICODEC_HEX_ED25519_PUBLIC_KEY = 0xED
MULTICODEC_HEX_P256_PUBLIC_KEY = 0x1200
MULTICODEC_HEX_P384_PUBLIC_KEY = 0x1201
MULTICODEC_HEX_P521_PUBLIC_KEY = 0x1202
MULTICODEC_HEX_RSA_PUBLIC_KEY = 0x1205

MULTICODEC_VALUE_TABLE = {
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY: 33, # secp256k1-pub - Secp256k1 public key (compressed)
MULTICODEC_HEX_X25519_PUBLIC_KEY: 32, # x25519-pub - Curve25519 public key
MULTICODEC_HEX_ED25519_PUBLIC_KEY: 32, # ed25519-pub - Ed25519 public key
MULTICODEC_HEX_P256_PUBLIC_KEY: 33, # p256-pub - P-256 public key (compressed)
MULTICODEC_HEX_P384_PUBLIC_KEY: 49, # p384-pub - P-384 public key (compressed)
MULTICODEC_HEX_P521_PUBLIC_KEY: None, # p521-pub - P-521 public key (compressed)
MULTICODEC_HEX_RSA_PUBLIC_KEY: None, # rsa-pub - RSA public key. DER-encoded ASN.1 type RSAPublicKey according to IETF RFC 8017 (PKCS #1)
}

import cryptography.hazmat.primitives.asymmetric.ec


@snoop
def import_ecc_public_key(raw_public_key_bytes: bytes):
# https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
# E: y^2 + x * y = x^3 + x^2 + b
# The bytes we have are x, we compute Y using the equation above, b I
# believe is defined by NIST...
# D.1.2.4 Curve P-384
# b = b3312fa7 e23ee7e4 988e056b e3f82d19 181d9c6e fe814112
# 0314088f 5013875a c656398d 8a2ed19d 2a85c8ed d3ec2aef

# Annnnnd we're back...
# This is a “Hazardous Materials” module. You should ONLY use it if you’re
# 100% absolutely sure that you know what you’re doing because this module
# is full of land mines, dragons, and dinosaurs with laser guns.

# TODO Decode x and y, ?, prophet

# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/#cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicNumbers
ecc_public_numbers = cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicNumbers.from_encoded_point(
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/#cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve(
# The name of the curve. Usually the name used for the ASN.1 OID
# such as secp256k1.
#
# $ openssl ecparam -list_curves
# secp224r1 : NIST/SECG curve over a 224 bit prime field
# secp256k1 : SECG curve over a 256 bit prime field
# secp384r1 : NIST/SECG curve over a 384 bit prime field
# secp521r1 : NIST/SECG curve over a 521 bit prime field
# prime256v1: X9.62/SECG curve over a 256 bit prime field
# brainpoolP256r1: RFC 5639 curve over a 256 bit prime field
# brainpoolP256t1: RFC 5639 curve over a 256 bit prime field
# brainpoolP320r1: RFC 5639 curve over a 320 bit prime field
# brainpoolP320t1: RFC 5639 curve over a 320 bit prime field
# brainpoolP384r1: RFC 5639 curve over a 384 bit prime field
# brainpoolP384t1: RFC 5639 curve over a 384 bit prime field
# brainpoolP512r1: RFC 5639 curve over a 512 bit prime field
# brainpoolP512t1: RFC 5639 curve over a 512 bit prime field
name="secp384r1",
# Size (in bits) of a secret scalar for the curve (as generated by
# generate_private_key()).
key_size=384,
),
raw_public_key_bytes,
)

# XXX WARNING The point represented by this object is not validated in any
# way until EllipticCurvePublicNumbers.public_key() is called and may not
# represent a valid point on the curve. You should not attempt to perform
# any computations using the values from this class until you have either
# validated it yourself or called public_key() successfully.
decoded_key = ecc_public_numbers.public_key()

return decoded_key


@snoop
def did_key_signature_method_creation(
multibase_value: hex,
raw_public_key_bytes: bytes,
) -> dict[str, str]:
# Creating a did:key value consists of creating a cryptographic key pair and
# encoding the public key using the format provided in Section § 2.
#
# NOTE The did:key Format. The creation of a DID Document is also
# performed by taking the public key value and expanding it into DID
# Document.
#
# An example is given below that expands the ed25519 did:key
# did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK into its
# associated DID Document:
#
# {
# "@context": [
# "https://www.w3.org/ns/did/v1",
# "https://w3id.org/security/suites/ed25519-2020/v1",
# "https://w3id.org/security/suites/x25519-2020/v1"
# ],
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
# "verificationMethod": [{
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
# "type": "Ed25519VerificationKey2020",
# "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
# "publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
# }],
# "authentication": [
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
# ],
# "assertionMethod": [
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
# ],
# "capabilityDelegation": [
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
# ],
# "capabilityInvocation": [
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
# ],
# "keyAgreement": [{
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p",
# "type": "X25519KeyAgreementKey2020",
# "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
# "publicKeyMultibase": "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p"
# }]
# }

# 3.1.2 https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
# Initialize verificationMethod to an empty object.
verification_method = {}

# Set multicodecValue and rawPublicKeyBytes to the result of passing
# multibaseValue and options to § 3.1.3 Decode Public Key Algorithm.
# Ensure the proper key length of rawPublicKeyBytes based on the
# multicodecValue table provided below:

# Multicodec hexadecimal value public key byte length Description
# 0xe7 33 bytes secp256k1-pub - Secp256k1 public key (compressed)
# 0xec 32 bytes x25519-pub - Curve25519 public key
# 0xed 32 bytes ed25519-pub - Ed25519 public key
# 0x1200 33 bytes p256-pub - P-256 public key (compressed)
# 0x1201 49 bytes p384-pub - P-384 public key (compressed)
# 0x1202 ?? bytes p521-pub - P-521 public key (compressed)
# 0x1205 ?? bytes rsa-pub - RSA public key. DER-encoded ASN.1 type RSAPublicKey according to IETF RFC 8017 (PKCS #1)
public_key_length_MUST_be = MULTICODEC_VALUE_TABLE.get(
multibase_value, MULTICODEC_VALUE_NOT_FOUND_IN_TABLE
)
if public_key_length_MUST_be is MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
raise DIDKeyDecoderNotFoundError(
f"multibase_value {multibase_value!r} not in MULTICODEC_VALUE_NOT_FOUND_IN_TABLE {MULTICODEC_VALUE_NOT_FOUND_IN_TABLE!r}"
)

# If the byte length of rawPublicKeyBytes does not match the expected public key length for the associated multicodecValue, an invalidPublicKeyLength error MUST be raised.
if public_key_length_MUST_be is not None and public_key_length_MUST_be != len(
raw_public_key_bytes
):
raise DIDKeyInvalidPublicKeyLengthError(
f"public_key_length_MUST_be: {public_key_length_MUST_be } != len(raw_public_key_bytes): {len(raw_public_key_bytes)}"
)

# Ensure the rawPublicKeyBytes are a proper encoding of the public key type
# as specified by the multicodecValue. This validation is often done by a
# cryptographic library when importing the public key by, for example,
# ensuring that an Elliptic Curve public key is a specific coordinate that
# exists on the elliptic curve. If an invalid public key value is detected,
# an invalidPublicKey error MUST be raised.
#
# SPEC ISSUE: Request for feedback on implementability: It is not clear if
# this particular check is implementable across all public key types. The
# group is accepting feedback on the implementability of this particular
# feature.
try:
if multibase_value in (
MULTICODEC_HEX_P256_PUBLIC_KEY,
MULTICODEC_HEX_P384_PUBLIC_KEY,
MULTICODEC_HEX_P521_PUBLIC_KEY,
):
decoded_key = import_ecc_public_key(raw_public_key_bytes)
else:
raise DIDKeyDecoderNotFoundError(
f"No importer for multibase_value {multibase_value!r}"
)
except Exception as e:
raise DIDKeyInvalidPublicKeyError(
f"invalid raw_public_key_bytes: {raw_public_key_bytes!r}"
) from e

# STRFKR: Atlantis

import jwcrypto.jwk

return jwcrypto.jwk.JWK(
kty="EC",
crv="P-384",
x=base64.urlsafe_b64encode("public_x").decode(),
y=base64.urlsafe_b64encode("public_y").decode(),
).to_dict()

# TODO

# Set the verificationMethod.id value by concatenating identifier, a hash
# character (#), and the multicodecValue. If verificationMethod.id is not a
# valid DID URL, an invalidDidUrl error MUST be raised.

# Set the publicKeyFormat value to the options.publicKeyFormat value.

# If publicKeyFormat is not known to the implementation, an
# unsupportedPublicKeyType error MUST be raised.

# If options.enableExperimentalPublicKeyTypes is set to false and
# publicKeyFormat is not Multikey, JsonWebKey2020, or
# Ed25519VerificationKey2020, an invalidPublicKeyType error MUST be raised.

# Set verificationMethod.type to the publicKeyFormat value.

# Set verificationMethod.controller to the identifier value. If
# verificationMethod.controller is not a valid DID, an invalidDid error MUST
# be raised.

# If publicKeyFormat is Multikey or Ed25519VerificationKey2020, set the
# verificationMethod.publicKeyMultibase value to multibaseValue.

# If publicKeyFormat is JsonWebKey2020, set the
# verificationMethod.publicKeyJwk value to the result of passing
# multicodecValue and rawPublicKeyBytes to § 3.1.4 Encode JWK Algorithm.

# Return verificationMethod.
return verification_method


def did_key_to_jwk_dict_is_p_384_startswith_z82(
multibase_value, raw_public_key_bytes
) -> dict[str, str]:
did_key_signature_method_creation(multibase_value, raw_public_key_bytes)
# TODO To JWK format


@snoop
def did_key_to_jwk_dict(
did_key: str,
*,
Expand All @@ -46,10 +334,12 @@ def did_key_to_jwk_dict(
Examples
- P-384: https://github.com/w3c-ccg/did-method-key/blob/f5abee840c31e92cd1ac11737e0b62103ab99d21/test-vectors/nist-curves.json#L112-L166
>>> did_key_to_jwk_dict("did:key:invalid")
Traceback (most recent call last):
DIDKeyDecoderNotFoundError: ...
>>> did_key_to_jwk_dict("did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54")
>>> did_key_to_jwk_dict("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9")
"""
if decoders_by_prefix is None:
decoders_by_prefix = {
Expand All @@ -61,8 +351,18 @@ def did_key_to_jwk_dict(
)
}

try:
multibase_value, raw_public_key_bytes = did_key_decode_public_key(
did_key.replace(DID_KEY_METHOD, "", 1)
)
except Exception as e:
raise DIDKeyDecoderNotFoundError(did_key) from e

for prefix, decoder in decoders_by_prefix.items():
if did_key.startswith(DID_KEY_METHOD + prefix):
return decoder(did_key)
try:
return decoder(multibase_value, raw_public_key_bytes)
except Exception as e:
raise DIDKeyDecoderError(did_key) from e

raise DIDKeyDecoderNotFoundError(did_key)
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"cryptography",
"cbor2",
"cwt",
"py-multicodec",
"py-multibase",
"jwcrypto",
"pycose",
"httpx",
Expand Down

0 comments on commit 40f5f68

Please sign in to comment.