From a11f8489e79ca95686e33decefa425589550b3ee Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Fri, 27 Sep 2024 21:57:30 +0000 Subject: [PATCH] add models Signed-off-by: PatStLouis --- .../cryptosuites/eddsa_jcs_2022.py | 132 ++++++++------ aries_cloudagent/vc/data_integrity/manager.py | 61 ++++--- .../vc/data_integrity/models/__init__.py | 17 +- .../vc/data_integrity/models/options.py | 37 ++-- .../vc/data_integrity/models/proof.py | 19 +-- .../models/verification_response.py | 161 ++++++++++++++++++ aries_cloudagent/vc/data_integrity/routes.py | 21 +-- aries_cloudagent/wallet/keys/manager.py | 68 ++++++-- 8 files changed, 385 insertions(+), 131 deletions(-) create mode 100644 aries_cloudagent/vc/data_integrity/models/verification_response.py diff --git a/aries_cloudagent/vc/data_integrity/cryptosuites/eddsa_jcs_2022.py b/aries_cloudagent/vc/data_integrity/cryptosuites/eddsa_jcs_2022.py index 21db51857c..43c65f8971 100644 --- a/aries_cloudagent/vc/data_integrity/cryptosuites/eddsa_jcs_2022.py +++ b/aries_cloudagent/vc/data_integrity/cryptosuites/eddsa_jcs_2022.py @@ -4,10 +4,18 @@ import canonicaljson from ....wallet.base import BaseWallet -from ....wallet.keys.manager import MultikeyManager +from ....wallet.keys.manager import ( + MultikeyManager, + multikey_to_verkey, + verkey_to_multikey, + key_type_from_multikey, +) from ....utils.multiformats import multibase from ....core.profile import ProfileSession from ....resolver.did_resolver import DIDResolver +from ..models.options import DataIntegrityProofOptions +from ..models.proof import DataIntegrityProof +from ..models.verification_response import ProblemDetails, DataIntegrityVerificationResult from ..errors import PROBLEM_DETAILS @@ -33,12 +41,14 @@ def __init__(self, *, session: ProfileSession): self.wallet = session.inject(BaseWallet) self.key_manager = MultikeyManager(session) - async def create_proof(self, unsecured_data_document: dict, options: dict): + async def create_proof( + self, unsecured_data_document: dict, options: DataIntegrityProofOptions + ): """Create proof algorithm. https://www.w3.org/TR/vc-di-eddsa/#create-proof-eddsa-jcs-2022. """ - proof = options.copy() + proof = DataIntegrityProof.deserialize(options.serialize().copy()) # Spec says to copy document context to the proof but it's unecessary IMO, # commenting out for the time being... @@ -51,53 +61,54 @@ async def create_proof(self, unsecured_data_document: dict, options: dict): hash_data = self.hashing(transformed_data, proof_config) proof_bytes = await self.proof_serialization(hash_data, options) - proof["proofValue"] = multibase.encode(proof_bytes, "base58btc") + proof.proof_value = multibase.encode(proof_bytes, "base58btc") return proof - def proof_configuration(self, options: dict): + def proof_configuration(self, options: DataIntegrityProofOptions): """Proof configuration algorithm. https://www.w3.org/TR/vc-di-eddsa/#proof-configuration-eddsa-jcs-2022. """ - proof_config = options.copy() - + proof_config = options assert ( - proof_config["type"] == "DataIntegrityProof" + proof_config.type == "DataIntegrityProof" ), 'Expected proof.type to be "DataIntegrityProof' assert ( - proof_config["cryptosuite"] == "eddsa-jcs-2022" + proof_config.cryptosuite == "eddsa-jcs-2022" ), 'Expected proof.cryptosuite to be "eddsa-jcs-2022' - if "created" in proof_config: + if proof_config.created: # TODO assert proper [XMLSCHEMA11-2] dateTimeStamp string - assert proof_config[ - "created" - ], "Expected proof.created to be a [XMLSCHEMA11-2] dateTimeStamp string." + assert ( + proof_config.created + ), "Expected proof.created to be a [XMLSCHEMA11-2] dateTimeStamp string." - if "expires" in proof_config: + if proof_config.expires: # TODO assert proper [XMLSCHEMA11-2] dateTimeStamp string - assert proof_config[ - "expires" - ], "Expected proof.expires to be a [XMLSCHEMA11-2] dateTimeStamp string." + assert ( + proof_config.expires + ), "Expected proof.expires to be a [XMLSCHEMA11-2] dateTimeStamp string." - return self._canonicalize(proof_config) + return self._canonicalize(proof_config.serialize()) - def transformation(self, unsecured_document: dict, options: dict): + def transformation( + self, unsecured_document: dict, options: DataIntegrityProofOptions + ): """Transformation algorithm. https://www.w3.org/TR/vc-di-eddsa/#transformation-eddsa-jcs-2022. """ assert ( - options["type"] == "DataIntegrityProof" + options.type == "DataIntegrityProof" ), "Expected proof.type to be `DataIntegrityProof`" assert ( - options["cryptosuite"] == "eddsa-jcs-2022" + options.cryptosuite == "eddsa-jcs-2022" ), "Expected proof.cryptosuite to be `eddsa-jcs-2022`" return self._canonicalize(unsecured_document) - def hashing(self, transformed_document, canonical_proof_config): + def hashing(self, transformed_document: bytes, canonical_proof_config: bytes): """Hashing algorithm. https://www.w3.org/TR/vc-di-eddsa/#hashing-eddsa-jcs-2022. @@ -107,48 +118,47 @@ def hashing(self, transformed_document, canonical_proof_config): + sha256(transformed_document).digest() ) - async def proof_serialization(self, hash_data: bytes, options: dict): + async def proof_serialization( + self, hash_data: bytes, options: DataIntegrityProofOptions + ): """Proof Serialization Algorithm. https://www.w3.org/TR/vc-di-eddsa/#proof-serialization-eddsa-jcs-2022. """ - # If the verification method is a did:key: URI, - # we derive the signing key from a multikey value - if options["verificationMethod"].startswith("did:key:"): - multikey = options["verificationMethod"].split("#")[-1] + # TODO encapsulate in a key manager method + if options.verification_method.startswith("did:key:"): + multikey = options.verification_method.split("#")[-1] key_info = await self.key_manager.from_multikey(multikey) - # Otherwise we derive the signing key from a kid else: - key_info = await self.key_manager.from_kid(options["verificationMethod"]) + key_info = await self.key_manager.from_kid(options.verification_method) return await self.wallet.sign_message( message=hash_data, - from_verkey=self.key_manager._multikey_to_verkey(key_info["multikey"]), + from_verkey=multikey_to_verkey(key_info["multikey"]), ) def _canonicalize(self, data: dict): """Json canonicalization.""" return canonicaljson.encode_canonical_json(data) - async def _get_multikey(self, kid: str): + async def _resolve_multikey(self, kid: str): """Derive a multikey from the verification method.""" + resolver = self.session.inject(DIDResolver) + verification_method = await resolver.dereference( + profile=self.session.profile, did_url=kid + ) - # If verification method is a did:key URI, - # we derive the multikey directly from the value. - if kid.startswith("did:key:"): - return kid.split("#")[-1] + if verification_method.type == "Ed25519VerificationKey2018": + multikey = verkey_to_multikey(verification_method.public_key_base58) - # Otherwise we resolve the verification method and extract the multikey. else: - verification_method = await DIDResolver().dereference( - profile=self.session.profile, did_url=kid - ) assert ( - verification_method["type"] == "Multikey" - ), "Expected Multikey verificationMethod type" + verification_method.type == "Multikey" + ), "Expecting Multikey verification method type" + multikey = verification_method.public_key_multibase - return verification_method["publicKeyMultibase"] + return multikey async def verify_proof(self, secured_document: dict): """Verify proof algorithm. @@ -171,31 +181,49 @@ async def verify_proof(self, secured_document: dict): # assert secured_document['@context'] == proof_options['@context'] # unsecured_document['@context'] = proof_options['@context'] + proof_options = DataIntegrityProofOptions.deserialize(proof_options) transformed_data = self.transformation(unsecured_document, proof_options) proof_config = self.proof_configuration(proof_options) hash_data = self.hashing(transformed_data, proof_config) - if not await self.proof_verification(hash_data, proof_bytes, proof_options): + verified = await self.proof_verification( + hash_data, proof_bytes, proof_options + ) + if not verified: raise CryptosuiteError("Invalid signature.") except (AssertionError, CryptosuiteError) as err: - problem_detail = PROBLEM_DETAILS["PROOF_VERIFICATION_ERROR"] | { - "message": str(err) - } - return {"verified": False, "proof": proof, "problemDetails": [problem_detail]} + problem_detail = ProblemDetails.deserialize( + PROBLEM_DETAILS["PROOF_VERIFICATION_ERROR"] + ) + problem_detail.detail = str(err) + return DataIntegrityVerificationResult( + verified=False, + proof=DataIntegrityProof.deserialize(proof), + problem_details=[problem_detail], + ) - return {"verified": True, "proof": proof, "problemDetails": []} + return DataIntegrityVerificationResult( + verified=True, + proof=DataIntegrityProof.deserialize(proof), + problem_details=[], + ) async def proof_verification( - self, hash_data: bytes, proof_bytes: bytes, options: dict + self, hash_data: bytes, proof_bytes: bytes, options: DataIntegrityProofOptions ): """Proof verification algorithm. https://www.w3.org/TR/vc-di-eddsa/#proof-verification-eddsa-jcs-2022. """ - multikey = await self._get_multikey(options["verificationMethod"]) + multikey = await MultikeyManager( + self.session + ).resolve_multikey_from_verification_method(options.verification_method) + # multikey = await self._resolve_multikey(options.verification_method) + verkey = multikey_to_verkey(multikey) + key_type = key_type_from_multikey(multikey) return await self.wallet.verify_message( message=hash_data, signature=proof_bytes, - from_verkey=self.key_manager._multikey_to_verkey(multikey), - key_type=self.key_manager.key_type_from_multikey(multikey), + from_verkey=verkey, + key_type=key_type, ) diff --git a/aries_cloudagent/vc/data_integrity/manager.py b/aries_cloudagent/vc/data_integrity/manager.py index 26a21f6c2e..dca9161fc4 100644 --- a/aries_cloudagent/vc/data_integrity/manager.py +++ b/aries_cloudagent/vc/data_integrity/manager.py @@ -2,6 +2,14 @@ from ...core.profile import ProfileSession from .cryptosuites import CRYPTOSUITES +from .models.proof import DataIntegrityProof +from .models.options import DataIntegrityProofOptions +from .models.verification_response import ( + DataIntegrityVerificationResponse, + DataIntegrityVerificationResult, + ProblemDetails, +) +from .errors import PROBLEM_DETAILS class DataIntegrityManagerError(Exception): @@ -15,14 +23,14 @@ def __init__(self, session: ProfileSession): """Initialize the DataIntegrityManager.""" self.session = session - async def add_proof(self, document, options): + async def add_proof(self, document: dict, options: DataIntegrityProofOptions): """Data integrity add proof algorithm. https://www.w3.org/TR/vc-data-integrity/#add-proof. """ # Instanciate a cryptosuite - suite = CRYPTOSUITES[options["cryptosuite"]](session=self.session) + suite = CRYPTOSUITES[options.cryptosuite](session=self.session) # Capture existing proofs if any all_proofs = document.pop("proof", []) @@ -32,10 +40,11 @@ async def add_proof(self, document, options): # Create secured document and create new proof secured_document = document.copy() secured_document["proof"] = all_proofs - secured_document["proof"].append(await suite.create_proof(document, options)) + proof = await suite.create_proof(document, options) + secured_document["proof"].append(proof.serialize()) return secured_document - async def verify_proof(self, secured_document): + async def verify_proof(self, secured_document: dict): """Verify a proof attached to a secured document. https://www.w3.org/TR/vc-data-integrity/#verify-proof. @@ -43,36 +52,40 @@ async def verify_proof(self, secured_document): unsecured_document = secured_document.copy() all_proofs = unsecured_document.pop("proof") all_proofs = all_proofs if isinstance(all_proofs, list) else [all_proofs] - verification_results = {} - verification_results["verifiedDocument"] = unsecured_document - verification_results["results"] = [] + verification_results = [] for proof in all_proofs: try: + proof = DataIntegrityProof.deserialize(proof) self.assert_proof(proof) # Instanciate a cryptosuite - suite = CRYPTOSUITES[proof["cryptosuite"]](session=self.session) + suite = CRYPTOSUITES[proof.cryptosuite](session=self.session) input_document = unsecured_document.copy() - input_document["proof"] = proof + input_document["proof"] = proof.serialize() verification_result = await suite.verify_proof(input_document) - verification_results["results"].append(verification_result) except AssertionError as err: - verification_result = { - "verified": False, - "problemDetails": [{"type": "", "message": str(err)}], - } - verification_results["results"].append(verification_result) - verification_results["verified"] = ( - True - if all(result["verified"] for result in verification_results["results"]) - else False + problem_detail = ProblemDetails.deserialize( + PROBLEM_DETAILS["PROOF_VERIFICATION_ERROR"] + ) + problem_detail.detail = str(err) + verification_result = DataIntegrityVerificationResult( + verified=False, + proof=proof, + problem_details=[problem_detail], + ) + verification_results.append(verification_result) + return DataIntegrityVerificationResponse( + verified=( + True if all(result.verified for result in verification_results) else False + ), + verified_document=unsecured_document, + results=verification_results, ) - return verification_results - def assert_proof(self, proof): + def assert_proof(self, proof: DataIntegrityProof): """Generic proof assertions for a data integrity proof.""" - assert proof["cryptosuite"] in CRYPTOSUITES, "Unsupported cryptosuite." - assert proof["proofValue"], "Missing proof value." - assert proof["proofPurpose"] in [ + assert proof.cryptosuite in CRYPTOSUITES, "Unsupported cryptosuite." + assert proof.proof_value, "Missing proof value." + assert proof.proof_purpose in [ "authentication", "assertionMethod", ], "Unknown proofPurpose." diff --git a/aries_cloudagent/vc/data_integrity/models/__init__.py b/aries_cloudagent/vc/data_integrity/models/__init__.py index 102286b3cd..c0b38b4a18 100644 --- a/aries_cloudagent/vc/data_integrity/models/__init__.py +++ b/aries_cloudagent/vc/data_integrity/models/__init__.py @@ -1,4 +1,15 @@ -from .proof import DIProof, DIProofSchema -from .options import AddProofOptionsSchema +from .proof import DataIntegrityProof, DataIntegrityProofSchema +from .options import DataIntegrityProofOptions, DataIntegrityProofOptionsSchema +from .verification_response import ( + DataIntegrityVerificationResponseSchema, + DataIntegrityVerificationResponse, +) -__all__ = ["DIProof", "DIProofSchema", "DIProofOptions", "AddProofOptionsSchema"] +__all__ = [ + "DataIntegrityProof", + "DataIntegrityProofSchema", + "DataIntegrityProofOptions", + "DataIntegrityProofOptionsSchema", + "DataIntegrityVerificationResponse", + "DataIntegrityVerificationResponseSchema", +] diff --git a/aries_cloudagent/vc/data_integrity/models/options.py b/aries_cloudagent/vc/data_integrity/models/options.py index 1f287d779f..551c38e89f 100644 --- a/aries_cloudagent/vc/data_integrity/models/options.py +++ b/aries_cloudagent/vc/data_integrity/models/options.py @@ -6,25 +6,24 @@ from ....messaging.models.base import BaseModel, BaseModelSchema from ....messaging.valid import ( - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_ISO8601_DATETIME_VALIDATE, + RFC3339_DATETIME_EXAMPLE, UUID4_EXAMPLE, Uri, ) -class AddProofOptions(BaseModel): +class DataIntegrityProofOptions(BaseModel): """Data Integrity Proof Options model.""" class Meta: """DataIntegrityProofOptions metadata.""" - schema_class = "DIProofOptionsSchema" + schema_class = "DataIntegrityProofOptionsSchema" def __init__( self, id: Optional[str] = None, - type: Optional[str] = "DataIntegrityProof", + type: Optional[str] = None, proof_purpose: Optional[str] = None, verification_method: Optional[str] = None, cryptosuite: Optional[str] = None, @@ -33,10 +32,11 @@ def __init__( domain: Optional[str] = None, challenge: Optional[str] = None, previous_proof: Optional[str] = None, + proof_value: Optional[str] = None, nonce: Optional[str] = None, **kwargs, ) -> None: - """Initialize the AddProofOptions instance.""" + """Initialize the DataIntegrityProofOptions instance.""" self.id = id self.type = type @@ -48,12 +48,13 @@ def __init__( self.domain = domain self.challenge = challenge self.previous_proof = previous_proof + self.proof_value = proof_value self.nonce = nonce self.extra = kwargs -class AddProofOptionsSchema(BaseModelSchema): - """Data Integrity Proof schema. +class DataIntegrityProofOptionsSchema(BaseModelSchema): + """Data Integrity Proof Options schema. Based on https://www.w3.org/TR/vc-data-integrity/#proofs @@ -63,7 +64,7 @@ class Meta: """Accept parameter overload.""" unknown = INCLUDE - model_class = AddProofOptions + model_class = DataIntegrityProofOptions id = fields.Str( required=False, @@ -125,27 +126,25 @@ class Meta: created = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "The date and time the proof was created is OPTIONAL and, if \ included, MUST be specified as an [XMLSCHEMA11-2] \ dateTimeStamp string" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": RFC3339_DATETIME_EXAMPLE, }, ) expires = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "The expires property is OPTIONAL and, if present, specifies when \ the proof expires. If present, it MUST be an [XMLSCHEMA11-2] \ dateTimeStamp string" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": RFC3339_DATETIME_EXAMPLE, }, ) @@ -181,6 +180,18 @@ class Meta: }, ) + proof_value = fields.Str( + required=False, + data_key="proofValue", + metadata={ + "description": "The value of the proof signature.", + "example": ( + "zsy1AahqbzJQ63n9RtekmwzqZeVj494VppdAVJBnMYrTwft6cLJJGeTSSxCCJ6HKnR" + "twE7jjDh6sB2z2AAiZY9BBnCD8wUVgwqH3qchGRCuC2RugA4eQ9fUrR4Yuycac3caiaaay" + ), + }, + ) + nonce = fields.Str( required=False, metadata={ diff --git a/aries_cloudagent/vc/data_integrity/models/proof.py b/aries_cloudagent/vc/data_integrity/models/proof.py index 75de72cdba..f52c69c3ae 100644 --- a/aries_cloudagent/vc/data_integrity/models/proof.py +++ b/aries_cloudagent/vc/data_integrity/models/proof.py @@ -6,20 +6,19 @@ from ....messaging.models.base import BaseModel, BaseModelSchema from ....messaging.valid import ( - INDY_ISO8601_DATETIME_EXAMPLE, - INDY_ISO8601_DATETIME_VALIDATE, + RFC3339_DATETIME_EXAMPLE, UUID4_EXAMPLE, Uri, ) -class DIProof(BaseModel): +class DataIntegrityProof(BaseModel): """Data Integrity Proof model.""" class Meta: """DataIntegrityProof metadata.""" - schema_class = "DIProofSchema" + schema_class = "DataIntegrityProofSchema" def __init__( self, @@ -37,7 +36,7 @@ def __init__( nonce: Optional[str] = None, **kwargs, ) -> None: - """Initialize the DIProof instance.""" + """Initialize the DataIntegrityProof instance.""" self.id = id self.type = type @@ -54,7 +53,7 @@ def __init__( self.extra = kwargs -class DIProofSchema(BaseModelSchema): +class DataIntegrityProofSchema(BaseModelSchema): """Data Integrity Proof schema. Based on https://www.w3.org/TR/vc-data-integrity/#proofs @@ -65,7 +64,7 @@ class Meta: """Accept parameter overload.""" unknown = INCLUDE - model_class = DIProof + model_class = DataIntegrityProof id = fields.Str( required=False, @@ -127,26 +126,24 @@ class Meta: created = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "The date and time the proof was created is OPTIONAL and, if included, \ MUST be specified as an [XMLSCHEMA11-2] dateTimeStamp string" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": RFC3339_DATETIME_EXAMPLE, }, ) expires = fields.Str( required=False, - validate=INDY_ISO8601_DATETIME_VALIDATE, metadata={ "description": ( "The expires property is OPTIONAL and, if present, specifies when the \ proof expires. If present, it MUST be an [XMLSCHEMA11-2] \ dateTimeStamp string" ), - "example": INDY_ISO8601_DATETIME_EXAMPLE, + "example": RFC3339_DATETIME_EXAMPLE, }, ) diff --git a/aries_cloudagent/vc/data_integrity/models/verification_response.py b/aries_cloudagent/vc/data_integrity/models/verification_response.py new file mode 100644 index 0000000000..552825f785 --- /dev/null +++ b/aries_cloudagent/vc/data_integrity/models/verification_response.py @@ -0,0 +1,161 @@ +"""DataIntegrityProof.""" + +from typing import Optional, List + +from marshmallow import INCLUDE, fields + +from ....messaging.models.base import BaseModel, BaseModelSchema +from .proof import DataIntegrityProof, DataIntegrityProofSchema + + +class ProblemDetails(BaseModel): + """ProblemDetails model.""" + + class Meta: + """ProblemDetails metadata.""" + + schema_class = "ProblemDetailsSchema" + + def __init__( + self, + type: Optional[str] = None, + title: Optional[str] = None, + detail: Optional[str] = None, + ) -> None: + """Initialize the ProblemDetails instance.""" + + self.type = type + self.title = title + self.detail = detail + + +class ProblemDetailsSchema(BaseModelSchema): + """ProblemDetails schema. + + Based on https://www.w3.org/TR/vc-data-model-2.0/#problem-details. + + """ + + class Meta: + """Accept parameter overload.""" + + unknown = INCLUDE + model_class = ProblemDetails + + type = fields.Str( + required=True, + metadata={ + "example": "https://w3id.org/security#PROOF_VERIFICATION_ERROR", + }, + ) + + title = fields.Str( + required=False, + metadata={}, + ) + + detail = fields.Str( + required=False, + metadata={}, + ) + + +class DataIntegrityVerificationResult(BaseModel): + """Data Integrity Verification Result model.""" + + class Meta: + """DataIntegrityVerificationResult metadata.""" + + schema_class = "DataIntegrityVerificationResultSchema" + + def __init__( + self, + verified: Optional[bool] = None, + proof: Optional[DataIntegrityProof] = None, + problem_details: Optional[List[ProblemDetails]] = None, + ) -> None: + """Initialize the DataIntegrityVerificationResult instance.""" + + self.verified = verified + self.proof = proof + self.problem_details = problem_details + + +class DataIntegrityVerificationResultSchema(BaseModelSchema): + """DataIntegrityVerificationResult schema.""" + + class Meta: + """Accept parameter overload.""" + + unknown = INCLUDE + model_class = DataIntegrityVerificationResult + + verified = fields.Bool( + required=True, + metadata={ + "example": False, + }, + ) + + proof = fields.Nested( + DataIntegrityProofSchema(), + required=True, + metadata={}, + ) + + problem_details = fields.Nested( + ProblemDetailsSchema(), + data_key="problemDetails", + required=True, + metadata={}, + ) + + +class DataIntegrityVerificationResponse(BaseModel): + """Data Integrity Verification Response model.""" + + class Meta: + """DataIntegrityVerificationResponse metadata.""" + + schema_class = "DataIntegrityVerificationResponseSchema" + + def __init__( + self, + verified: Optional[bool] = None, + verified_document: Optional[dict] = None, + results: Optional[List[DataIntegrityVerificationResult]] = None, + ) -> None: + """Initialize the DataIntegrityVerificationResponse instance.""" + + self.verified = verified + self.verified_document = verified_document + self.results = results + + +class DataIntegrityVerificationResponseSchema(BaseModelSchema): + """DataIntegrityVerificationResponse schema.""" + + class Meta: + """Accept parameter overload.""" + + unknown = INCLUDE + model_class = DataIntegrityVerificationResponse + + verified = fields.Bool( + required=True, + metadata={ + "example": False, + }, + ) + + verified_document = fields.Dict( + data_key="verifiedDocument", + required=False, + metadata={}, + ) + + results = fields.List( + fields.Dict(required=False), + required=False, + metadata={}, + ) diff --git a/aries_cloudagent/vc/data_integrity/routes.py b/aries_cloudagent/vc/data_integrity/routes.py index 65bba8bdb8..d2437c601a 100644 --- a/aries_cloudagent/vc/data_integrity/routes.py +++ b/aries_cloudagent/vc/data_integrity/routes.py @@ -11,7 +11,7 @@ from ...admin.request_context import AdminRequestContext from ...messaging.models.openapi import OpenAPISchema from .manager import DataIntegrityManager, DataIntegrityManagerError -from .models import AddProofOptionsSchema +from .models import DataIntegrityProofOptionsSchema, DataIntegrityProofOptions from ...wallet.error import WalletNotFoundError, WalletError LOGGER = logging.getLogger(__name__) @@ -22,7 +22,7 @@ class AddProofSchema(OpenAPISchema): document = fields.Dict(required=True, metadata={"example": {"hello": "world"}}) options = fields.Nested( - AddProofOptionsSchema, + DataIntegrityProofOptionsSchema, metadata={ "example": { "type": "DataIntegrityProof", @@ -92,6 +92,7 @@ async def add_di_proof(request: web.BaseRequest): options = body.get("options") try: + options = DataIntegrityProofOptions.deserialize(options) async with context.session() as session: secured_document = await DataIntegrityManager(session).add_proof( document, options @@ -124,14 +125,14 @@ async def verify_di_secured_document(request: web.BaseRequest): verification_response = await DataIntegrityManager(session).verify_proof( secured_document ) - - if verification_response["verified"]: - return web.json_response( - {"verificationResults": verification_response}, status=200 - ) - return web.json_response( - {"verificationResults": verification_response}, status=400 - ) + response = { + "verified": verification_response.verified, + "verifiedDocument": verification_response.verified_document, + "results": [result.serialize() for result in verification_response.results], + } + if verification_response.verified: + return web.json_response({"verificationResults": response}, status=200) + return web.json_response({"verificationResults": response}, status=400) except (WalletNotFoundError, WalletError, DataIntegrityManagerError) as err: raise web.HTTPNotFound(reason=err.roll_up) from err diff --git a/aries_cloudagent/wallet/keys/manager.py b/aries_cloudagent/wallet/keys/manager.py index 5cda77f484..ba1094bf9f 100644 --- a/aries_cloudagent/wallet/keys/manager.py +++ b/aries_cloudagent/wallet/keys/manager.py @@ -6,6 +6,7 @@ from ..util import b58_to_bytes, bytes_to_b58 from ...utils.multiformats import multibase from ...wallet.error import WalletNotFoundError +from ...resolver.did_resolver import DIDResolver DEFAULT_ALG = "ed25519" ALG_MAPPINGS = { @@ -18,6 +19,33 @@ } +def multikey_to_verkey(multikey: str, alg: str = DEFAULT_ALG): + """Transform multikey to verkey.""" + + prefix_length = ALG_MAPPINGS[alg]["prefix_length"] + public_bytes = bytes(bytearray(multibase.decode(multikey))[prefix_length:]) + + return bytes_to_b58(public_bytes) + + +def verkey_to_multikey(verkey: str, alg: str = DEFAULT_ALG): + """Transform verkey to multikey.""" + + prefix_hex = ALG_MAPPINGS[alg]["prefix_hex"] + prefixed_key_hex = f"{prefix_hex}{b58_to_bytes(verkey).hex()}" + + return multibase.encode(bytes.fromhex(prefixed_key_hex), "base58btc") + + +def key_type_from_multikey(multikey: str): + """Derive key_type class from multikey prefix.""" + for mapping in ALG_MAPPINGS: + if multikey.startswith(ALG_MAPPINGS[mapping]["multikey_prefix"]): + return ALG_MAPPINGS[mapping]["key_type"] + + raise MultikeyManagerError(f"Unsupported key algorithm for multikey {multikey}.") + + class MultikeyManagerError(Exception): """Generic MultikeyManager Error.""" @@ -28,23 +56,29 @@ class MultikeyManager: def __init__(self, session: ProfileSession): """Initialize the MultikeyManager.""" + self.session: ProfileSession = session self.wallet: BaseWallet = session.inject(BaseWallet) - def _multikey_to_verkey(self, multikey: str, alg: str = DEFAULT_ALG): - """Transform multikey to verkey.""" + async def resolve_multikey_from_verification_method(self, kid: str): + """Derive a multikey from the verification method.""" + resolver = self.session.inject(DIDResolver) + verification_method = await resolver.dereference( + profile=self.session.profile, did_url=kid + ) - prefix_length = ALG_MAPPINGS[alg]["prefix_length"] - public_bytes = bytes(bytearray(multibase.decode(multikey))[prefix_length:]) + if verification_method.type == "Multikey": + multikey = verification_method.public_key_multibase - return bytes_to_b58(public_bytes) + elif verification_method.type == "Ed25519VerificationKey2018": + multikey = verkey_to_multikey(verification_method.public_key_base58) - def _verkey_to_multikey(self, verkey: str, alg: str = DEFAULT_ALG): - """Transform verkey to multikey.""" + elif verification_method.type == "Ed25519VerificationKey2020": + multikey = verkey_to_multikey(verification_method.public_key_multibase) - prefix_hex = ALG_MAPPINGS[alg]["prefix_hex"] - prefixed_key_hex = f"{prefix_hex}{b58_to_bytes(verkey).hex()}" + else: + raise MultikeyManagerError("Unknown verification method type.") - return multibase.encode(bytes.fromhex(prefixed_key_hex), "base58btc") + return multikey def key_type_from_multikey(self, multikey: str): """Derive key_type class from multikey prefix.""" @@ -73,19 +107,17 @@ async def from_kid(self, kid: str): return { "kid": key_info.kid, - "multikey": self._verkey_to_multikey(key_info.verkey), + "multikey": verkey_to_multikey(key_info.verkey), } async def from_multikey(self, multikey: str): """Fetch a single key.""" - key_info = await self.wallet.get_signing_key( - verkey=self._multikey_to_verkey(multikey) - ) + key_info = await self.wallet.get_signing_key(verkey=multikey_to_verkey(multikey)) return { "kid": key_info.kid, - "multikey": self._verkey_to_multikey(key_info.verkey), + "multikey": verkey_to_multikey(key_info.verkey), } async def create(self, seed: str = None, kid: str = None, alg: str = DEFAULT_ALG): @@ -104,7 +136,7 @@ async def create(self, seed: str = None, kid: str = None, alg: str = DEFAULT_ALG return { "kid": key_info.kid, - "multikey": self._verkey_to_multikey(key_info.verkey), + "multikey": verkey_to_multikey(key_info.verkey), } async def update(self, multikey: str, kid: str): @@ -114,10 +146,10 @@ async def update(self, multikey: str, kid: str): raise MultikeyManagerError(f"kid '{kid}' already exists in wallet.") key_info = await self.wallet.assign_kid_to_key( - verkey=self._multikey_to_verkey(multikey), kid=kid + verkey=multikey_to_verkey(multikey), kid=kid ) return { "kid": key_info.kid, - "multikey": self._verkey_to_multikey(key_info.verkey), + "multikey": verkey_to_multikey(key_info.verkey), }