Skip to content

Commit

Permalink
key loader: did: jwk: Ditch multibase did keys
Browse files Browse the repository at this point in the history
Signed-off-by: John Andersen <[email protected]>
  • Loading branch information
pdxjohnny committed Mar 10, 2024
1 parent 0c722b8 commit 9ab9429
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 255 deletions.
2 changes: 0 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,3 @@ dependencies:
- PyJWT==2.8.0
- werkzeug==2.2.2
- cwt==2.7.1
- py-multibase==1.0.3
- py-multicodec==0.2.1
27 changes: 4 additions & 23 deletions scitt_emulator/create_statement.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) SCITT Authors
# Licensed under the MIT License.
import base64
import pathlib
import argparse
from typing import Union, Optional
Expand All @@ -12,17 +13,10 @@
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.serialization import load_pem_private_key

# NOTE These are unmaintained but the
# https://github.com/hashberg-io/multiformats stuff and base58 modules don't
# produce the same results:
# https://grotto-networking.com/blog/posts/DID_Key.html#bug-in-multibase-library
import multibase
import multicodec

# TODO jwcrypto is LGPLv3, is there another option with a permissive licence?
import jwcrypto.jwk

from scitt_emulator.did_helpers import DID_KEY_METHOD, MULTICODEC_HEX_P384_PUBLIC_KEY
from scitt_emulator.did_helpers import DID_JWK_METHOD


@pycose.headers.CoseHeaderAttribute.register_attribute()
Expand Down Expand Up @@ -102,22 +96,9 @@ def create_claim(
cwt_cose_key_to_cose_key = cwt_cose_key.to_dict()
sign1_message_key = pycose.keys.ec2.EC2Key.from_dict(cwt_cose_key_to_cose_key)

# If issuer was not given used did:key of public key
# If issuer was not given used did:jwk of public key
if issuer is None:
multicodec_prefix_p_384 = "p384-pub"
multicodec.constants.NAME_TABLE[multicodec_prefix_p_384] = MULTICODEC_HEX_P384_PUBLIC_KEY
issuer = (
DID_KEY_METHOD
+ multibase.encode(
"base58btc",
multicodec.add_prefix(
multicodec_prefix_p_384,
load_pem_private_key(key_as_pem_bytes, password=None)
.public_key()
.public_bytes(Encoding.X962, PublicFormat.CompressedPoint),
),
).decode()
)
issuer = DID_JWK_METHOD + base64.urlsafe_b64encode(key.export_public().encode()).decode()

# CWT_Claims (label: 14 pending [CWT_CLAIM_COSE]): A CWT representing
# the Issuer (iss) making the statement, and the Subject (sub) to
Expand Down
178 changes: 3 additions & 175 deletions scitt_emulator/did_helpers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import os
import sys
import inspect
import urllib.parse
from typing import Optional, Callable, Dict, Tuple, Union
from typing import Optional

import multibase
import multicodec
import cryptography.hazmat.primitives.asymmetric.ec

DID_JWK_METHOD = "did:jwk:"


def did_web_to_url(
Expand All @@ -22,172 +19,3 @@ def did_web_to_url(
*[urllib.parse.unquote(i) for i in did_web_string.split(":")[2:]],
]
)


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_decode_public_key(multibase_value: str) -> Tuple[bytes, bytes]:
# 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)
}


def did_key_signature_method_creation(
multibase_value: hex,
raw_public_key_bytes: bytes,
) -> Union[cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey]:
# 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
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,
):
public_key = cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey.from_encoded_point(
cryptography.hazmat.primitives.asymmetric.ec.SECP384R1(),
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

return public_key


def did_key_to_cryptography_key(
did_key: str,
) -> Union[cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey]:
"""
References
- https://w3c-ccg.github.io/did-method-key/#p-384
- RFC7515: JSON Web Key (JWK): https://www.rfc-editor.org/rfc/rfc7517
- RFC8037: CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE): https://www.rfc-editor.org/rfc/rfc8037
Examples
- P-384: https://github.com/w3c-ccg/did-method-key/blob/f5abee840c31e92cd1ac11737e0b62103ab99d21/test-vectors/nist-curves.json#L112-L166
>>> did_key_to_cryptography_key("did:key:invalid")
Traceback (most recent call last):
DIDKeyDecoderNotFoundError: ...
>>> public_key = did_key_to_cryptography_key("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9")
>>> public_key.__class__
<class 'cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey'>
"""
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

try:
return did_key_signature_method_creation(multibase_value, raw_public_key_bytes)
except Exception as e:
raise DIDKeyDecoderError(did_key) from e

raise DIDKeyDecoderNotFoundError(did_key)
39 changes: 39 additions & 0 deletions scitt_emulator/key_loader_format_did_jwk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import base64
from typing import List, Tuple

import cwt
import cwt.algs.ec2
import pycose
import pycose.keys.ec2
import cryptography.hazmat.primitives.asymmetric.ec
from cryptography.hazmat.primitives import serialization

import jwcrypto.jwk

from scitt_emulator.did_helpers import DID_JWK_METHOD
from scitt_emulator.key_helper_dataclasses import VerificationKey


CONTENT_TYPE = "application/jwk+json"


def key_loader_format_did_jwk(
unverified_issuer: str,
) -> List[VerificationKey]:
if not unverified_issuer.startswith(DID_JWK_METHOD):
return []
key = jwcrypto.jwk.JWK.from_json(
base64.urlsafe_b64decode(unverified_issuer[len(DID_JWK_METHOD):]).decode()
)
return [
VerificationKey(
transforms=[key],
original=key,
original_content_type=CONTENT_TYPE,
original_bytes=unverified_issuer.encode("utf-8"),
original_bytes_encoding="utf-8",
usable=False,
cwt=None,
cose=None,
)
]
51 changes: 0 additions & 51 deletions scitt_emulator/key_loader_format_did_key.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,16 @@ def key_loader_format_url_referencing_ssh_authorized_keys(
)

return keys


def transform_key_instance_cryptography_ecc_public_to_jwcrypto_jwk(
key: cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey,
) -> jwcrypto.jwk.JWK:
if not isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
raise TypeError(key)
return jwcrypto.jwk.JWK.from_pem(
key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
)
6 changes: 2 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
'scitt-emulator=scitt_emulator.cli:main'
],
'scitt_emulator.verify_signature.key_loaders': [
'did_key=scitt_emulator.key_loader_format_did_key:key_loader_format_did_key',
'did_jwk=scitt_emulator.key_loader_format_did_jwk:key_loader_format_did_jwk',
'url_referencing_oidc_issuer=scitt_emulator.key_loader_format_url_referencing_oidc_issuer:key_loader_format_url_referencing_oidc_issuer',
'url_referencing_ssh_authorized_keys=scitt_emulator.key_loader_format_url_referencing_ssh_authorized_keys:key_loader_format_url_referencing_ssh_authorized_keys',
],
'scitt_emulator.key_helpers.transforms_key_instances': [
'transform_key_instance_cwt_cose_ec2_to_pycose_ec2=scitt_emulator.key_transforms:transform_key_instance_cwt_cose_ec2_to_pycose_ec2',
'transform_key_instance_cryptography_ecc_public_to_jwcrypto_jwk=scitt_emulator:key_loader_format_did_key.transform_key_instance_cryptography_ecc_public_to_jwcrypto_jwk',
'transform_key_instance_jwcrypto_jwk_to_cwt_cose=scitt_emulator.key_loader_format_url_referencing_oidc_issuer:transform_key_instance_jwcrypto_jwk_to_cwt_cose',
'transform_key_instance_cryptography_ecc_public_to_jwcrypto_jwk=scitt_emulator:key_loader_format_url_referencing_ssh_authorized_keys.transform_key_instance_cryptography_ecc_public_to_jwcrypto_jwk',
],
'scitt_emulator.key_helpers.verification_key_to_object': [
'to_object_oidc_issuer=scitt_emulator.key_loader_format_url_referencing_oidc_issuer:to_object_oidc_issuer',
Expand All @@ -30,8 +30,6 @@
"cryptography",
"cbor2",
"cwt",
"py-multicodec",
"py-multibase",
"jwcrypto",
"pycose",
"httpx",
Expand Down

0 comments on commit 9ab9429

Please sign in to comment.