Skip to content

Commit 40f5f68

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

File tree

3 files changed

+312
-4
lines changed

3 files changed

+312
-4
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: 304 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
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 multiformats
10+
711

812
def did_web_to_url(
913
did_web_string: str,
@@ -18,20 +22,304 @@ def did_web_to_url(
1822
)
1923

2024

25+
class DIDKeyInvalidPublicKeyLengthError(ValueError):
26+
"""
27+
If the byte length of rawPublicKeyBytes does not match the expected public
28+
key length for the associated multicodecValue, an invalidPublicKeyLength
29+
error MUST be raised.
30+
"""
31+
32+
2133
class DIDKeyDecoderNotFoundError(NotImplementedError):
2234
"""
2335
Raised when we don't have a function implemented to decode the given key
2436
"""
2537

2638

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

2953

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)
54+
def did_key_decode(
55+
did_key_without_method_or_key_type_prefix: str,
56+
) -> tuple[str, bytes]:
3257
return
3358

3459

60+
import sys, base58, multibase, multicodec
61+
import snoop
62+
63+
64+
def did_key_decode_public_key(multibase_value: str) -> dict[str, str]:
65+
# 3.1.2.3
66+
# Decode multibaseValue using the base58-btc multibase alphabet and set
67+
# multicodecValue to the multicodec header for the decoded value.
68+
multibase_value_decoded = multibase.decode(multibase_value)
69+
# Implementers are cautioned to ensure that the multicodecValue is set to
70+
# the result after performing varint decoding.
71+
multicodec_value = multicodec.extract_prefix(multibase_value_decoded)
72+
# Set the rawPublicKeyBytes to the bytes remaining after the multicodec
73+
# header.
74+
raw_public_key_bytes = multicodec.remove_prefix(multibase_value_decoded)
75+
# Return multicodecValue and rawPublicKeyBytes as the decodedPublicKey.
76+
return multicodec_value, raw_public_key_bytes
77+
78+
79+
class _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
80+
pass
81+
82+
83+
MULTICODEC_VALUE_NOT_FOUND_IN_TABLE = _MULTICODEC_VALUE_NOT_FOUND_IN_TABLE()
84+
85+
# Multicodec hexadecimal value, public key, byte length, Description
86+
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY = 0xE7
87+
MULTICODEC_HEX_X25519_PUBLIC_KEY = 0xEC
88+
MULTICODEC_HEX_ED25519_PUBLIC_KEY = 0xED
89+
MULTICODEC_HEX_P256_PUBLIC_KEY = 0x1200
90+
MULTICODEC_HEX_P384_PUBLIC_KEY = 0x1201
91+
MULTICODEC_HEX_P521_PUBLIC_KEY = 0x1202
92+
MULTICODEC_HEX_RSA_PUBLIC_KEY = 0x1205
93+
94+
MULTICODEC_VALUE_TABLE = {
95+
MULTICODEC_HEX_SECP256K1_PUBLIC_KEY: 33, # secp256k1-pub - Secp256k1 public key (compressed)
96+
MULTICODEC_HEX_X25519_PUBLIC_KEY: 32, # x25519-pub - Curve25519 public key
97+
MULTICODEC_HEX_ED25519_PUBLIC_KEY: 32, # ed25519-pub - Ed25519 public key
98+
MULTICODEC_HEX_P256_PUBLIC_KEY: 33, # p256-pub - P-256 public key (compressed)
99+
MULTICODEC_HEX_P384_PUBLIC_KEY: 49, # p384-pub - P-384 public key (compressed)
100+
MULTICODEC_HEX_P521_PUBLIC_KEY: None, # p521-pub - P-521 public key (compressed)
101+
MULTICODEC_HEX_RSA_PUBLIC_KEY: None, # rsa-pub - RSA public key. DER-encoded ASN.1 type RSAPublicKey according to IETF RFC 8017 (PKCS #1)
102+
}
103+
104+
import cryptography.hazmat.primitives.asymmetric.ec
105+
106+
107+
@snoop
108+
def import_ecc_public_key(raw_public_key_bytes: bytes):
109+
# https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
110+
# E: y^2 + x * y = x^3 + x^2 + b
111+
# The bytes we have are x, we compute Y using the equation above, b I
112+
# believe is defined by NIST...
113+
# D.1.2.4 Curve P-384
114+
# b = b3312fa7 e23ee7e4 988e056b e3f82d19 181d9c6e fe814112
115+
# 0314088f 5013875a c656398d 8a2ed19d 2a85c8ed d3ec2aef
116+
117+
# Annnnnd we're back...
118+
# This is a “Hazardous Materials” module. You should ONLY use it if you’re
119+
# 100% absolutely sure that you know what you’re doing because this module
120+
# is full of land mines, dragons, and dinosaurs with laser guns.
121+
122+
# TODO Decode x and y, ?, prophet
123+
124+
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/#cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicNumbers
125+
ecc_public_numbers = cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicNumbers.from_encoded_point(
126+
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/#cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve
127+
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve(
128+
# The name of the curve. Usually the name used for the ASN.1 OID
129+
# such as secp256k1.
130+
#
131+
# $ openssl ecparam -list_curves
132+
# secp224r1 : NIST/SECG curve over a 224 bit prime field
133+
# secp256k1 : SECG curve over a 256 bit prime field
134+
# secp384r1 : NIST/SECG curve over a 384 bit prime field
135+
# secp521r1 : NIST/SECG curve over a 521 bit prime field
136+
# prime256v1: X9.62/SECG curve over a 256 bit prime field
137+
# brainpoolP256r1: RFC 5639 curve over a 256 bit prime field
138+
# brainpoolP256t1: RFC 5639 curve over a 256 bit prime field
139+
# brainpoolP320r1: RFC 5639 curve over a 320 bit prime field
140+
# brainpoolP320t1: RFC 5639 curve over a 320 bit prime field
141+
# brainpoolP384r1: RFC 5639 curve over a 384 bit prime field
142+
# brainpoolP384t1: RFC 5639 curve over a 384 bit prime field
143+
# brainpoolP512r1: RFC 5639 curve over a 512 bit prime field
144+
# brainpoolP512t1: RFC 5639 curve over a 512 bit prime field
145+
name="secp384r1",
146+
# Size (in bits) of a secret scalar for the curve (as generated by
147+
# generate_private_key()).
148+
key_size=384,
149+
),
150+
raw_public_key_bytes,
151+
)
152+
153+
# XXX WARNING The point represented by this object is not validated in any
154+
# way until EllipticCurvePublicNumbers.public_key() is called and may not
155+
# represent a valid point on the curve. You should not attempt to perform
156+
# any computations using the values from this class until you have either
157+
# validated it yourself or called public_key() successfully.
158+
decoded_key = ecc_public_numbers.public_key()
159+
160+
return decoded_key
161+
162+
163+
@snoop
164+
def did_key_signature_method_creation(
165+
multibase_value: hex,
166+
raw_public_key_bytes: bytes,
167+
) -> dict[str, str]:
168+
# Creating a did:key value consists of creating a cryptographic key pair and
169+
# encoding the public key using the format provided in Section § 2.
170+
#
171+
# NOTE The did:key Format. The creation of a DID Document is also
172+
# performed by taking the public key value and expanding it into DID
173+
# Document.
174+
#
175+
# An example is given below that expands the ed25519 did:key
176+
# did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK into its
177+
# associated DID Document:
178+
#
179+
# {
180+
# "@context": [
181+
# "https://www.w3.org/ns/did/v1",
182+
# "https://w3id.org/security/suites/ed25519-2020/v1",
183+
# "https://w3id.org/security/suites/x25519-2020/v1"
184+
# ],
185+
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
186+
# "verificationMethod": [{
187+
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
188+
# "type": "Ed25519VerificationKey2020",
189+
# "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
190+
# "publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
191+
# }],
192+
# "authentication": [
193+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
194+
# ],
195+
# "assertionMethod": [
196+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
197+
# ],
198+
# "capabilityDelegation": [
199+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
200+
# ],
201+
# "capabilityInvocation": [
202+
# "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
203+
# ],
204+
# "keyAgreement": [{
205+
# "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p",
206+
# "type": "X25519KeyAgreementKey2020",
207+
# "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
208+
# "publicKeyMultibase": "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p"
209+
# }]
210+
# }
211+
212+
# 3.1.2 https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
213+
# Initialize verificationMethod to an empty object.
214+
verification_method = {}
215+
216+
# Set multicodecValue and rawPublicKeyBytes to the result of passing
217+
# multibaseValue and options to § 3.1.3 Decode Public Key Algorithm.
218+
# Ensure the proper key length of rawPublicKeyBytes based on the
219+
# multicodecValue table provided below:
220+
221+
# Multicodec hexadecimal value public key byte length Description
222+
# 0xe7 33 bytes secp256k1-pub - Secp256k1 public key (compressed)
223+
# 0xec 32 bytes x25519-pub - Curve25519 public key
224+
# 0xed 32 bytes ed25519-pub - Ed25519 public key
225+
# 0x1200 33 bytes p256-pub - P-256 public key (compressed)
226+
# 0x1201 49 bytes p384-pub - P-384 public key (compressed)
227+
# 0x1202 ?? bytes p521-pub - P-521 public key (compressed)
228+
# 0x1205 ?? bytes rsa-pub - RSA public key. DER-encoded ASN.1 type RSAPublicKey according to IETF RFC 8017 (PKCS #1)
229+
public_key_length_MUST_be = MULTICODEC_VALUE_TABLE.get(
230+
multibase_value, MULTICODEC_VALUE_NOT_FOUND_IN_TABLE
231+
)
232+
if public_key_length_MUST_be is MULTICODEC_VALUE_NOT_FOUND_IN_TABLE:
233+
raise DIDKeyDecoderNotFoundError(
234+
f"multibase_value {multibase_value!r} not in MULTICODEC_VALUE_NOT_FOUND_IN_TABLE {MULTICODEC_VALUE_NOT_FOUND_IN_TABLE!r}"
235+
)
236+
237+
# If the byte length of rawPublicKeyBytes does not match the expected public key length for the associated multicodecValue, an invalidPublicKeyLength error MUST be raised.
238+
if public_key_length_MUST_be is not None and public_key_length_MUST_be != len(
239+
raw_public_key_bytes
240+
):
241+
raise DIDKeyInvalidPublicKeyLengthError(
242+
f"public_key_length_MUST_be: {public_key_length_MUST_be } != len(raw_public_key_bytes): {len(raw_public_key_bytes)}"
243+
)
244+
245+
# Ensure the rawPublicKeyBytes are a proper encoding of the public key type
246+
# as specified by the multicodecValue. This validation is often done by a
247+
# cryptographic library when importing the public key by, for example,
248+
# ensuring that an Elliptic Curve public key is a specific coordinate that
249+
# exists on the elliptic curve. If an invalid public key value is detected,
250+
# an invalidPublicKey error MUST be raised.
251+
#
252+
# SPEC ISSUE: Request for feedback on implementability: It is not clear if
253+
# this particular check is implementable across all public key types. The
254+
# group is accepting feedback on the implementability of this particular
255+
# feature.
256+
try:
257+
if multibase_value in (
258+
MULTICODEC_HEX_P256_PUBLIC_KEY,
259+
MULTICODEC_HEX_P384_PUBLIC_KEY,
260+
MULTICODEC_HEX_P521_PUBLIC_KEY,
261+
):
262+
decoded_key = import_ecc_public_key(raw_public_key_bytes)
263+
else:
264+
raise DIDKeyDecoderNotFoundError(
265+
f"No importer for multibase_value {multibase_value!r}"
266+
)
267+
except Exception as e:
268+
raise DIDKeyInvalidPublicKeyError(
269+
f"invalid raw_public_key_bytes: {raw_public_key_bytes!r}"
270+
) from e
271+
272+
# STRFKR: Atlantis
273+
274+
import jwcrypto.jwk
275+
276+
return jwcrypto.jwk.JWK(
277+
kty="EC",
278+
crv="P-384",
279+
x=base64.urlsafe_b64encode("public_x").decode(),
280+
y=base64.urlsafe_b64encode("public_y").decode(),
281+
).to_dict()
282+
283+
# TODO
284+
285+
# Set the verificationMethod.id value by concatenating identifier, a hash
286+
# character (#), and the multicodecValue. If verificationMethod.id is not a
287+
# valid DID URL, an invalidDidUrl error MUST be raised.
288+
289+
# Set the publicKeyFormat value to the options.publicKeyFormat value.
290+
291+
# If publicKeyFormat is not known to the implementation, an
292+
# unsupportedPublicKeyType error MUST be raised.
293+
294+
# If options.enableExperimentalPublicKeyTypes is set to false and
295+
# publicKeyFormat is not Multikey, JsonWebKey2020, or
296+
# Ed25519VerificationKey2020, an invalidPublicKeyType error MUST be raised.
297+
298+
# Set verificationMethod.type to the publicKeyFormat value.
299+
300+
# Set verificationMethod.controller to the identifier value. If
301+
# verificationMethod.controller is not a valid DID, an invalidDid error MUST
302+
# be raised.
303+
304+
# If publicKeyFormat is Multikey or Ed25519VerificationKey2020, set the
305+
# verificationMethod.publicKeyMultibase value to multibaseValue.
306+
307+
# If publicKeyFormat is JsonWebKey2020, set the
308+
# verificationMethod.publicKeyJwk value to the result of passing
309+
# multicodecValue and rawPublicKeyBytes to § 3.1.4 Encode JWK Algorithm.
310+
311+
# Return verificationMethod.
312+
return verification_method
313+
314+
315+
def did_key_to_jwk_dict_is_p_384_startswith_z82(
316+
multibase_value, raw_public_key_bytes
317+
) -> dict[str, str]:
318+
did_key_signature_method_creation(multibase_value, raw_public_key_bytes)
319+
# TODO To JWK format
320+
321+
322+
@snoop
35323
def did_key_to_jwk_dict(
36324
did_key: str,
37325
*,
@@ -46,10 +334,12 @@ def did_key_to_jwk_dict(
46334
47335
Examples
48336
337+
- P-384: https://github.com/w3c-ccg/did-method-key/blob/f5abee840c31e92cd1ac11737e0b62103ab99d21/test-vectors/nist-curves.json#L112-L166
338+
49339
>>> did_key_to_jwk_dict("did:key:invalid")
50340
Traceback (most recent call last):
51341
DIDKeyDecoderNotFoundError: ...
52-
>>> did_key_to_jwk_dict("did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54")
342+
>>> did_key_to_jwk_dict("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9")
53343
"""
54344
if decoders_by_prefix is None:
55345
decoders_by_prefix = {
@@ -61,8 +351,18 @@ def did_key_to_jwk_dict(
61351
)
62352
}
63353

354+
try:
355+
multibase_value, raw_public_key_bytes = did_key_decode_public_key(
356+
did_key.replace(DID_KEY_METHOD, "", 1)
357+
)
358+
except Exception as e:
359+
raise DIDKeyDecoderNotFoundError(did_key) from e
360+
64361
for prefix, decoder in decoders_by_prefix.items():
65362
if did_key.startswith(DID_KEY_METHOD + prefix):
66-
return decoder(did_key)
363+
try:
364+
return decoder(multibase_value, raw_public_key_bytes)
365+
except Exception as e:
366+
raise DIDKeyDecoderError(did_key) from e
67367

68368
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)