Skip to content

Commit

Permalink
Merge pull request #3442 from anonyome/gm/p256-w3c
Browse files Browse the repository at this point in the history
Support P256 keys & did:keys
  • Loading branch information
dbluhm authored Jan 15, 2025
2 parents dea4e09 + 2f49821 commit cd3a623
Show file tree
Hide file tree
Showing 16 changed files with 514 additions and 175 deletions.
41 changes: 38 additions & 3 deletions acapy_agent/did/did_key.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""DID Key class and resolver methods."""

from ..vc.ld_proofs.constants import DID_V1_CONTEXT_URL
from typing import List, Optional
from ..vc.ld_proofs.constants import DID_V1_CONTEXT_URL, SECURITY_CONTEXT_MULTIKEY_URL
from ..wallet.crypto import ed25519_pk_to_curve25519
from ..wallet.key_type import (
BLS12381G1,
BLS12381G1G2,
BLS12381G2,
ED25519,
P256,
X25519,
KeyType,
KeyTypes,
Expand Down Expand Up @@ -276,15 +278,47 @@ def construct_did_key_ed25519(did_key: "DIDKey") -> dict:
return did_doc


def construct_did_signature_key_base(*, id: str, key_id: str, verification_method: dict):
def construct_did_key_p256(did_key: "DIDKey") -> dict:
"""Construct P256 did:key.
Args:
did_key (DIDKey): did key instance to parse p256 did:key document from
Returns:
dict: The p256 did:key did document
"""

did_doc = construct_did_signature_key_base(
id=did_key.did,
key_id=did_key.key_id,
verification_method={
"id": did_key.key_id,
"type": "Multikey",
"controller": did_key.did,
"publicKeyMultibase": did_key.fingerprint,
},
extra_context=[SECURITY_CONTEXT_MULTIKEY_URL],
)

return did_doc


def construct_did_signature_key_base(
*,
id: str,
key_id: str,
verification_method: dict,
extra_context: Optional[List[str]] = None,
):
"""Create base did key structure to use for most signature keys.
May not be suitable for all did key types
"""

return {
"@context": DID_V1_CONTEXT_URL,
"@context": [DID_V1_CONTEXT_URL] + (extra_context or []),
"id": id,
"verificationMethod": [verification_method],
"authentication": [key_id],
Expand All @@ -298,6 +332,7 @@ def construct_did_signature_key_base(*, id: str, key_id: str, verification_metho
DID_KEY_RESOLVERS = {
ED25519: construct_did_key_ed25519,
X25519: construct_did_key_x25519,
P256: construct_did_key_p256,
BLS12381G2: construct_did_key_bls12381g2,
BLS12381G1: construct_did_key_bls12381g1,
BLS12381G1G2: construct_did_key_bls12381g1g2,
Expand Down
61 changes: 61 additions & 0 deletions acapy_agent/did/tests/test_did_key_p256.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest import TestCase

from ...wallet.key_type import P256
from ...wallet.util import b58_to_bytes
from ..did_key import DID_KEY_RESOLVERS, DIDKey
from .test_dids import DID_P256_zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169

TEST_P256_BASE58_KEY = "23FF9c3MrW7NkEW6uNDvdSKQMJ4YFTBXNMEPytZfYeE33"
TEST_P256_FINGERPRINT = "zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
TEST_P256_DID = f"did:key:{TEST_P256_FINGERPRINT}"
TEST_P256_KEY_ID = f"{TEST_P256_DID}#{TEST_P256_FINGERPRINT}"
TEST_P256_PREFIX_BYTES = b"".join([b"\x80\x24", b58_to_bytes(TEST_P256_BASE58_KEY)])


class TestDIDKey(TestCase):
def test_p256_from_public_key(self):
key_bytes = b58_to_bytes(TEST_P256_BASE58_KEY)
did_key = DIDKey.from_public_key(key_bytes, P256)

assert did_key.did == TEST_P256_DID

def test_p256_from_public_key_b58(self):
did_key = DIDKey.from_public_key_b58(TEST_P256_BASE58_KEY, P256)

assert did_key.did == TEST_P256_DID

def test_p256_from_fingerprint(self):
did_key = DIDKey.from_fingerprint(TEST_P256_FINGERPRINT)

assert did_key.did == TEST_P256_DID
assert did_key.public_key_b58 == TEST_P256_BASE58_KEY

def test_p256_from_did(self):
did_key = DIDKey.from_did(TEST_P256_DID)

assert did_key.public_key_b58 == TEST_P256_BASE58_KEY

def test_p256_properties(self):
did_key = DIDKey.from_did(TEST_P256_DID)

assert did_key.fingerprint == TEST_P256_FINGERPRINT
assert did_key.did == TEST_P256_DID
assert did_key.public_key_b58 == TEST_P256_BASE58_KEY
assert did_key.public_key == b58_to_bytes(TEST_P256_BASE58_KEY)
assert did_key.key_type == P256
assert did_key.key_id == TEST_P256_KEY_ID
assert did_key.prefixed_public_key == TEST_P256_PREFIX_BYTES

def test_p256_diddoc(self):
did_key = DIDKey.from_did(TEST_P256_DID)

resolver = DID_KEY_RESOLVERS[P256]

assert resolver(did_key) == did_key.did_doc

def test_p256_resolver(self):
did_key = DIDKey.from_did(TEST_P256_DID)
resolver = DID_KEY_RESOLVERS[P256]
did_doc = resolver(did_key)

assert did_doc == DID_P256_zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169
32 changes: 29 additions & 3 deletions acapy_agent/did/tests/test_dids.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
DID_ED25519_z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th = {
"@context": "https://www.w3.org/ns/did/v1",
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th",
"verificationMethod": [
{
Expand Down Expand Up @@ -51,8 +51,34 @@
],
}

DID_P256_zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169 = {
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"],
"id": "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
"verificationMethod": [
{
"id": "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
"type": "Multikey",
"controller": "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
"publicKeyMultibase": "zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
}
],
"authentication": [
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
],
"assertionMethod": [
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
],
"capabilityDelegation": [
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
],
"capabilityInvocation": [
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
],
"keyAgreement": [],
}

DID_BLS12381G2_zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT = {
"@context": "https://www.w3.org/ns/did/v1",
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT",
"verificationMethod": [
{
Expand All @@ -78,7 +104,7 @@
}

DID_BLS12381G1_z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA = {
"@context": "https://www.w3.org/ns/did/v1",
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA",
"verificationMethod": [
{
Expand Down
2 changes: 2 additions & 0 deletions acapy_agent/utils/multiformats/multicodec.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class SupportedCodecs(Enum):
bls12381g2 = Multicodec("bls12_381-g2-pub", b"\xeb\x01")
bls12381g1g2 = Multicodec("bls12_381-g1g2-pub", b"\xee\x01")
secp256k1_pub = Multicodec("secp256k1-pub", b"\xe7\x01")
p256_pub = Multicodec("p256-pub", b"\x80\x24")

@classmethod
def by_name(cls, name: str) -> Multicodec:
Expand All @@ -45,6 +46,7 @@ def for_data(cls, data: bytes) -> Multicodec:
"bls12_381-g2-pub",
"bls12_381-g1g2-pub",
"secp256k1-pub",
"p256-pub",
]


Expand Down
2 changes: 2 additions & 0 deletions acapy_agent/vc/data_integrity/tests/test_cryptosuites.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from acapy_agent.utils.testing import create_test_profile
from acapy_agent.vc.data_integrity.cryptosuites import EddsaJcs2022
from acapy_agent.vc.data_integrity.models.options import DataIntegrityProofOptions
from acapy_agent.wallet.key_type import KeyTypes
from acapy_agent.wallet.keys.manager import MultikeyManager


Expand All @@ -34,6 +35,7 @@ async def asyncSetUp(self):
self.resolver.register_resolver(WebDIDResolver())
self.profile = await create_test_profile()
self.profile.context.injector.bind_instance(DIDResolver, self.resolver)
self.profile.context.injector.bind_instance(KeyTypes, KeyTypes())
try:
async with self.profile.session() as session:
await MultikeyManager(session=session).create(seed=self.seed)
Expand Down
2 changes: 2 additions & 0 deletions acapy_agent/vc/data_integrity/tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from acapy_agent.utils.testing import create_test_profile
from acapy_agent.vc.data_integrity.manager import DataIntegrityManager
from acapy_agent.vc.data_integrity.models.options import DataIntegrityProofOptions
from acapy_agent.wallet.key_type import KeyTypes
from acapy_agent.wallet.keys.manager import MultikeyManager


Expand All @@ -34,6 +35,7 @@ async def asyncSetUp(self):
self.resolver.register_resolver(WebDIDResolver())
self.profile = await create_test_profile()
self.profile.context.injector.bind_instance(DIDResolver, self.resolver)
self.profile.context.injector.bind_instance(KeyTypes, KeyTypes())
try:
async with self.profile.session() as session:
await MultikeyManager(session=session).create(seed=self.seed)
Expand Down
1 change: 1 addition & 0 deletions acapy_agent/vc/ld_proofs/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CREDENTIALS_CONTEXT_V1_URL = "https://www.w3.org/2018/credentials/v1"
SECURITY_CONTEXT_BBS_URL = "https://w3id.org/security/bbs/v1"
SECURITY_CONTEXT_ED25519_2020_URL = "https://w3id.org/security/suites/ed25519-2020/v1"
SECURITY_CONTEXT_MULTIKEY_URL = "https://w3id.org/security/multikey/v1"

CREDENTIALS_ISSUER_URL = "https://www.w3.org/2018/credentials#issuer"
SECURITY_PROOF_URL = "https://w3id.org/security#proof"
Expand Down
33 changes: 24 additions & 9 deletions acapy_agent/wallet/askar.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .did_method import SOV, DIDMethod, DIDMethods
from .did_parameters_validation import DIDParametersValidation
from .error import WalletDuplicateError, WalletError, WalletNotFoundError
from .key_type import BLS12381G2, ED25519, X25519, KeyType, KeyTypes
from .key_type import BLS12381G2, ED25519, P256, X25519, KeyType, KeyTypes
from .util import b58_to_bytes, bytes_to_b58

CATEGORY_DID = "did"
Expand Down Expand Up @@ -185,18 +185,22 @@ async def get_signing_key(self, verkey: str) -> KeyInfo:

if not verkey:
raise WalletNotFoundError("No key identifier provided")
key = await self._session.handle.fetch_key(verkey)
if not key:
key_entry = await self._session.handle.fetch_key(verkey)
if not key_entry:
raise WalletNotFoundError("Unknown key: {}".format(verkey))
metadata = json.loads(key.metadata or "{}")
metadata = json.loads(key_entry.metadata or "{}")

try:
kid = key.tags.get("kid")
kid = key_entry.tags.get("kid")
except Exception:
kid = None

# FIXME implement key types
return KeyInfo(verkey=verkey, metadata=metadata, key_type=ED25519, kid=kid)
key = cast(Key, key_entry.key)
key_types = self.session.inject(KeyTypes)
key_type = key_types.from_key_type(key.algorithm.value)
if not key_type:
raise WalletError(f"Unknown key type {key.algorithm.value}")
return KeyInfo(verkey=verkey, metadata=metadata, key_type=key_type, kid=kid)

async def replace_signing_key_metadata(self, verkey: str, metadata: dict):
"""Replace the metadata associated with a signing keypair.
Expand Down Expand Up @@ -775,6 +779,12 @@ async def verify_message(
return pk.verify_signature(message, signature)
except AskarError as err:
raise WalletError("Exception when verifying message signature") from err
elif key_type == P256:
try:
pk = Key.from_public_bytes(KeyAlg.P256, verkey)
return pk.verify_signature(message, signature)
except AskarError as err:
raise WalletError("Exception when verifying message signature") from err

# other key types are currently verified outside of Askar
return verify_signed_message(
Expand Down Expand Up @@ -869,6 +879,8 @@ def _create_keypair(key_type: KeyType, seed: Union[str, bytes, None] = None) ->
elif key_type == X25519:
alg = KeyAlg.X25519
method = None
elif key_type == P256:
alg = KeyAlg.P256
elif key_type == BLS12381G2:
alg = KeyAlg.BLS12_381_G2
method = SeedMethod.BlsKeyGen
Expand All @@ -878,14 +890,17 @@ def _create_keypair(key_type: KeyType, seed: Union[str, bytes, None] = None) ->
raise WalletError(f"Unsupported key algorithm: {key_type}")
if seed:
try:
if key_type == ED25519:
if key_type in (ED25519, P256):
# not a seed - it is the secret key
seed = validate_seed(seed)
return Key.from_secret_bytes(alg, seed)
else:
return Key.from_seed(alg, seed, method=method)
except AskarError as err:
if err.code == AskarErrorCode.INPUT:
raise WalletError("Invalid seed for key generation") from None
raise WalletError("Invalid seed for key generation") from err
else:
LOGGER.error(f"Unhandled Askar error code: {err.code}")
raise
else:
return Key.generate(alg)
4 changes: 2 additions & 2 deletions acapy_agent/wallet/did_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Dict, List, Mapping, Optional

from .error import BaseError
from .key_type import BLS12381G2, ED25519, X25519, KeyType
from .key_type import BLS12381G2, ED25519, P256, X25519, KeyType


class HolderDefinedDid(Enum):
Expand Down Expand Up @@ -67,7 +67,7 @@ def holder_defined_did(self) -> HolderDefinedDid:
)
KEY = DIDMethod(
name="key",
key_types=[ED25519, BLS12381G2],
key_types=[ED25519, P256, BLS12381G2],
rotation=False,
)
WEB = DIDMethod(
Expand Down
Loading

0 comments on commit cd3a623

Please sign in to comment.