Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support P256 keys & did:keys #3442

Merged
merged 16 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
gmulhearn marked this conversation as resolved.
Show resolved Hide resolved
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
Loading