Skip to content

Commit 476e259

Browse files
committed
IN PROGRESS: did helpers: ecdsa p-384 key loading
Signed-off-by: John Andersen <[email protected]>
1 parent a4bc402 commit 476e259

File tree

3 files changed

+248
-7
lines changed

3 files changed

+248
-7
lines changed

pytest.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
[pytest]
2+
# https://docs.pytest.org/en/7.1.x/how-to/doctest.html#using-doctest-options
3+
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
4+
# Alternatively, options can be enabled by an inline comment in the doc test itself:
5+
# >>> something_that_raises() # doctest: +IGNORE_EXCEPTION_DETAIL
6+
# Traceback (most recent call last):
7+
# ValueError: ...
28
addopts = --doctest-modules

scitt_emulator/did_helpers.py

Lines changed: 240 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import os
2+
import ast
23
import sys
4+
import base64
35
import inspect
46
import urllib.parse
57
from typing import Optional, Callable
68

9+
import multibase
10+
import multicodec
11+
import cryptography.hazmat.primitives.asymmetric.ec
12+
713

814
def did_web_to_url(
915
did_web_string: str,
@@ -18,18 +24,233 @@ def did_web_to_url(
1824
)
1925

2026

27+
class DIDKeyInvalidPublicKeyLengthError(ValueError):
28+
"""
29+
If the byte length of rawPublicKeyBytes does not match the expected public
30+
key length for the associated multicodecValue, an invalidPublicKeyLength
31+
error MUST be raised.
32+
"""
33+
34+
2135
class DIDKeyDecoderNotFoundError(NotImplementedError):
2236
"""
2337
Raised when we don't have a function implemented to decode the given key
2438
"""
2539

2640

41+
class DIDKeyDecoderError(Exception):
42+
"""
43+
Raised when we failed to decode a key from a did:key DID method
44+
"""
45+
46+
47+
class DIDKeyInvalidPublicKeyError(DIDKeyDecoderError):
48+
"""
49+
Raised when the raw bytes of a key are invalid during decode
50+
"""
51+
52+
2753
DID_KEY_METHOD = "did:key:"
2854

2955

30-
def did_key_to_jwk_dict_is_p_384_startswith_z82(did_key: str) -> dict[str, str]:
31-
did_key = did_key.replace(DID_KEY_METHOD, "", 1)
32-
return
56+
def did_key_decode_public_key(multibase_value: str) -> dict[str, str]:
57+
# 3.1.2.3
58+
# Decode multibaseValue using the base58-btc multibase alphabet and set
59+
# multicodecValue to the multicodec header for the decoded value.
60+
multibase_value_decoded = multibase.decode(multibase_value)
61+
# Implementers are cautioned to ensure that the multicodecValue is set to
62+
# the result after performing varint decoding.
63+
multicodec_value = multicodec.extract_prefix(multibase_value_decoded)
64+
# Set the rawPublicKeyBytes to the bytes remaining after the multicodec
65+
# header.
66+
raw_public_key_bytes = multicodec.remove_prefix(multibase_value_decoded)
67+
# Return multicodecValue and rawPublicKeyBytes as the decodedPublicKey.
68+
return multicodec_value, raw_public_key_bytes
69+
70+
71+
class _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
72+
pass
73+
74+
75+
MULTICODEC_VALUE_NOT_FOUND_IN_TABLE = _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE()
76+
77+
# Multicodec hexadecimal value, public key, byte length, Description
78+
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY = 0xE7
79+
MULTICODEC_HEX_X25519_PUBLIC_KEY = 0xEC
80+
MULTICODEC_HEX_ED25519_PUBLIC_KEY = 0xED
81+
MULTICODEC_HEX_P256_PUBLIC_KEY = 0x1200
82+
MULTICODEC_HEX_P384_PUBLIC_KEY = 0x1201
83+
MULTICODEC_HEX_P521_PUBLIC_KEY = 0x1202
84+
MULTICODEC_HEX_RSA_PUBLIC_KEY = 0x1205
85+
86+
MULTICODEC_VALUE_TABLE = {
87+
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY: 33, # secp256k1-pub - Secp256k1 public key (compressed)
88+
MULTICODEC_HEX_X25519_PUBLIC_KEY: 32, # x25519-pub - Curve25519 public key
89+
MULTICODEC_HEX_ED25519_PUBLIC_KEY: 32, # ed25519-pub - Ed25519 public key
90+
MULTICODEC_HEX_P256_PUBLIC_KEY: 33, # p256-pub - P-256 public key (compressed)
91+
MULTICODEC_HEX_P384_PUBLIC_KEY: 49, # p384-pub - P-384 public key (compressed)
92+
MULTICODEC_HEX_P521_PUBLIC_KEY: None, # p521-pub - P-521 public key (compressed)
93+
MULTICODEC_HEX_RSA_PUBLIC_KEY: None, # rsa-pub - RSA public key. DER-encoded ASN.1 type RSAPublicKey according to IETF RFC 8017 (PKCS #1)
94+
}
95+
96+
97+
def did_key_signature_method_creation(
98+
multibase_value: hex,
99+
raw_public_key_bytes: bytes,
100+
) -> dict[str, str]:
101+
# Creating a did:key value consists of creating a cryptographic key pair and
102+
# encoding the public key using the format provided in Section § 2.
103+
#
104+
# NOTE The did:key Format. The creation of a DID Document is also
105+
# performed by taking the public key value and expanding it into DID
106+
# Document.
107+
#
108+
# An example is given below that expands the ed25519 did:key
109+
# did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK into its
110+
# associated DID Document:
111+
#
112+
# {
113+
# "@context": [
114+
# "https://www.w3.org/ns/did/v1",
115+
# "https://w3id.org/security/suites/ed25519-2020/v1",
116+
# "https://w3id.org/security/suites/x25519-2020/v1"
117+
# ],
118+
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
119+
# "verificationMethod": [{
120+
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
121+
# "type": "Ed25519VerificationKey2020",
122+
# "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
123+
# "publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
124+
# }],
125+
# "authentication": [
126+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
127+
# ],
128+
# "assertionMethod": [
129+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
130+
# ],
131+
# "capabilityDelegation": [
132+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
133+
# ],
134+
# "capabilityInvocation": [
135+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
136+
# ],
137+
# "keyAgreement": [{
138+
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p",
139+
# "type": "X25519KeyAgreementKey2020",
140+
# "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
141+
# "publicKeyMultibase": "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p"
142+
# }]
143+
# }
144+
145+
# 3.1.2 https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
146+
# Initialize verificationMethod to an empty object.
147+
verification_method = {}
148+
149+
# Set multicodecValue and rawPublicKeyBytes to the result of passing
150+
# multibaseValue and options to § 3.1.3 Decode Public Key Algorithm.
151+
# Ensure the proper key length of rawPublicKeyBytes based on the
152+
# multicodecValue table provided below:
153+
154+
# Multicodec hexadecimal value public key byte length Description
155+
# 0xe7 33 bytes secp256k1-pub - Secp256k1 public key (compressed)
156+
# 0xec 32 bytes x25519-pub - Curve25519 public key
157+
# 0xed 32 bytes ed25519-pub - Ed25519 public key
158+
# 0x1200 33 bytes p256-pub - P-256 public key (compressed)
159+
# 0x1201 49 bytes p384-pub - P-384 public key (compressed)
160+
# 0x1202 ?? bytes p521-pub - P-521 public key (compressed)
161+
# 0x1205 ?? bytes rsa-pub - RSA public key. DER-encoded ASN.1 type RSAPublicKey according to IETF RFC 8017 (PKCS #1)
162+
public_key_length_MUST_be = MULTICODEC_VALUE_TABLE.get(
163+
multibase_value, MULTICODEC_VALUE_NOT_FOUND_IN_TABLE
164+
)
165+
if public_key_length_MUST_be is MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
166+
raise DIDKeyDecoderNotFoundError(
167+
f"multibase_value {multibase_value!r} not in MULTICODEC_VALUE_NOT_FOUND_IN_TABLE {MULTICODEC_VALUE_NOT_FOUND_IN_TABLE!r}"
168+
)
169+
170+
# If the byte length of rawPublicKeyBytes does not match the expected public key length for the associated multicodecValue, an invalidPublicKeyLength error MUST be raised.
171+
if public_key_length_MUST_be is not None and public_key_length_MUST_be != len(
172+
raw_public_key_bytes
173+
):
174+
raise DIDKeyInvalidPublicKeyLengthError(
175+
f"public_key_length_MUST_be: {public_key_length_MUST_be } != len(raw_public_key_bytes): {len(raw_public_key_bytes)}"
176+
)
177+
178+
# Ensure the rawPublicKeyBytes are a proper encoding of the public key type
179+
# as specified by the multicodecValue. This validation is often done by a
180+
# cryptographic library when importing the public key by, for example,
181+
# ensuring that an Elliptic Curve public key is a specific coordinate that
182+
# exists on the elliptic curve. If an invalid public key value is detected,
183+
# an invalidPublicKey error MUST be raised.
184+
#
185+
# SPEC ISSUE: Request for feedback on implementability: It is not clear if
186+
# this particular check is implementable across all public key types. The
187+
# group is accepting feedback on the implementability of this particular
188+
# feature.
189+
try:
190+
if multibase_value in (
191+
MULTICODEC_HEX_P256_PUBLIC_KEY,
192+
MULTICODEC_HEX_P384_PUBLIC_KEY,
193+
MULTICODEC_HEX_P521_PUBLIC_KEY,
194+
):
195+
public_key = cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey.from_encoded_point(
196+
cryptography.hazmat.primitives.asymmetric.ec.SECP384R1(),
197+
raw_public_key_bytes,
198+
)
199+
else:
200+
raise DIDKeyDecoderNotFoundError(
201+
f"No importer for multibase_value {multibase_value!r}"
202+
)
203+
except Exception as e:
204+
raise DIDKeyInvalidPublicKeyError(
205+
f"invalid raw_public_key_bytes: {raw_public_key_bytes!r}"
206+
) from e
207+
208+
return public_key
209+
210+
serialized_public = public_key.public_bytes(
211+
encoding=serialization.Encoding.PEM,
212+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
213+
)
214+
215+
# TODO
216+
217+
# Set the verificationMethod.id value by concatenating identifier, a hash
218+
# character (#), and the multicodecValue. If verificationMethod.id is not a
219+
# valid DID URL, an invalidDidUrl error MUST be raised.
220+
221+
# Set the publicKeyFormat value to the options.publicKeyFormat value.
222+
223+
# If publicKeyFormat is not known to the implementation, an
224+
# unsupportedPublicKeyType error MUST be raised.
225+
226+
# If options.enableExperimentalPublicKeyTypes is set to false and
227+
# publicKeyFormat is not Multikey, JsonWebKey2020, or
228+
# Ed25519VerificationKey2020, an invalidPublicKeyType error MUST be raised.
229+
230+
# Set verificationMethod.type to the publicKeyFormat value.
231+
232+
# Set verificationMethod.controller to the identifier value. If
233+
# verificationMethod.controller is not a valid DID, an invalidDid error MUST
234+
# be raised.
235+
236+
# If publicKeyFormat is Multikey or Ed25519VerificationKey2020, set the
237+
# verificationMethod.publicKeyMultibase value to multibaseValue.
238+
239+
# If publicKeyFormat is JsonWebKey2020, set the
240+
# verificationMethod.publicKeyJwk value to the result of passing
241+
# multicodecValue and rawPublicKeyBytes to § 3.1.4 Encode JWK Algorithm.
242+
243+
# Return verificationMethod.
244+
return verification_method
245+
246+
import jwcrypto.jwk
247+
248+
return jwcrypto.jwk.JWK(
249+
kty="EC",
250+
crv="P-384",
251+
x=decoded_key.x,
252+
y=decoded_key.y,
253+
).to_dict()
33254

34255

35256
def did_key_to_jwk_dict(
@@ -46,10 +267,14 @@ def did_key_to_jwk_dict(
46267
47268
Examples
48269
270+
- P-384: https://github.com/w3c-ccg/did-method-key/blob/f5abee840c31e92cd1ac11737e0b62103ab99d21/test-vectors/nist-curves.json#L112-L166
271+
49272
>>> did_key_to_jwk_dict("did:key:invalid")
50273
Traceback (most recent call last):
51274
DIDKeyDecoderNotFoundError: ...
52-
>>> did_key_to_jwk_dict("did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54")
275+
>>> public_key = did_key_to_jwk_dict("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9")
276+
>>> public_key.__class__
277+
<class 'cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey'>
53278
"""
54279
if decoders_by_prefix is None:
55280
decoders_by_prefix = {
@@ -61,8 +286,16 @@ def did_key_to_jwk_dict(
61286
)
62287
}
63288

64-
for prefix, decoder in decoders_by_prefix.items():
65-
if did_key.startswith(DID_KEY_METHOD + prefix):
66-
return decoder(did_key)
289+
try:
290+
multibase_value, raw_public_key_bytes = did_key_decode_public_key(
291+
did_key.replace(DID_KEY_METHOD, "", 1)
292+
)
293+
except Exception as e:
294+
raise DIDKeyDecoderNotFoundError(did_key) from e
295+
296+
try:
297+
return did_key_signature_method_creation(multibase_value, raw_public_key_bytes)
298+
except Exception as e:
299+
raise DIDKeyDecoderError(did_key) from e
67300

68301
raise DIDKeyDecoderNotFoundError(did_key)

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"cryptography",
2222
"cbor2",
2323
"cwt",
24+
"py-multicodec",
25+
"py-multibase",
2426
"jwcrypto",
2527
"pycose",
2628
"httpx",

0 commit comments

Comments
 (0)