diff --git a/aries_cloudagent/vc/ld_proofs/credentials_context.jsonld b/aries_cloudagent/vc/ld_proofs/credentials_context.jsonld new file mode 100644 index 0000000000..0124a3c41c --- /dev/null +++ b/aries_cloudagent/vc/ld_proofs/credentials_context.jsonld @@ -0,0 +1,237 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "credentialSchema": { + "@id": "cred:credentialSchema", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + + "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018" + } + }, + "credentialStatus": {"@id": "cred:credentialStatus", "@type": "@id"}, + "credentialSubject": {"@id": "cred:credentialSubject", "@type": "@id"}, + "evidence": {"@id": "cred:evidence", "@type": "@id"}, + "expirationDate": {"@id": "cred:expirationDate", "@type": "xsd:dateTime"}, + "holder": {"@id": "cred:holder", "@type": "@id"}, + "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, + "issuer": {"@id": "cred:issuer", "@type": "@id"}, + "issuanceDate": {"@id": "cred:issuanceDate", "@type": "xsd:dateTime"}, + "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, + "refreshService": { + "@id": "cred:refreshService", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + + "ManualRefreshService2018": "cred:ManualRefreshService2018" + } + }, + "termsOfUse": {"@id": "cred:termsOfUse", "@type": "@id"}, + "validFrom": {"@id": "cred:validFrom", "@type": "xsd:dateTime"}, + "validUntil": {"@id": "cred:validUntil", "@type": "xsd:dateTime"} + } + }, + + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + + "holder": {"@id": "cred:holder", "@type": "@id"}, + "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, + "verifiableCredential": {"@id": "cred:verifiableCredential", "@type": "@id", "@container": "@graph"} + } + }, + + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "EcdsaSecp256r1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "RsaSignature2018": { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@version": 1.1, + "@protected": true, + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "proof": {"@id": "https://w3id.org/security#proof", "@type": "@id", "@container": "@graph"} + } +} diff --git a/aries_cloudagent/vc/ld_proofs/did_documents_context.jsonld b/aries_cloudagent/vc/ld_proofs/did_documents_context.jsonld new file mode 100644 index 0000000000..55ab11cf93 --- /dev/null +++ b/aries_cloudagent/vc/ld_proofs/did_documents_context.jsonld @@ -0,0 +1,58 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/aries_cloudagent/vc/ld_proofs/document_downloader.py b/aries_cloudagent/vc/ld_proofs/document_downloader.py new file mode 100644 index 0000000000..741c90b4cb --- /dev/null +++ b/aries_cloudagent/vc/ld_proofs/document_downloader.py @@ -0,0 +1,168 @@ +"""Quick and dirty fix to use as alternative to pyld downloader. + +Allows keeping some context in local filesystem. +""" +import logging +import re +import string +from typing import Dict, Optional +import urllib.parse as urllib_parse + +import requests +from pyld import jsonld +from pyld.jsonld import JsonLdError, parse_link_header, LINK_HEADER_REL + +logger = logging.getLogger(__name__) + + +def _load_jsonld_file(original_url, filename: str): + with open(filename, "r") as f: + content = f.read() + return { + "contentType": "application/ld+json", + "contextUrl": None, + "documentUrl": original_url, + "document": content, + } + + +class StaticCacheJsonLdDownloader: + """Downloader checking local filesystem for known contexts.""" + + CONTEXT_FILE_MAPPING = { + "https://www.w3.org/2018/credentials/v1": "credentials_context.jsonld", + "https://w3id.org/vc/status-list/2021/v1": "status_list_context.jsonld", + "https://www.w3.org/ns/did/v1": "did_documents_context.jsonld", + } + + def __init__(self): + """.""" + self.documents_downloader = JsonLdDocumentDownloader() + self.document_parser = JsonLdDocumentParser() + + self.cache = { + url: self.document_parser.parse(_load_jsonld_file(url, filename), None) + for url, filename in StaticCacheJsonLdDownloader.CONTEXT_FILE_MAPPING + } + + def load(self, url, options=None): + """Load a jsonld document from url.""" + cached = self.cache.get(url) + + if cached is not None: + logger.info("Cache hit for context: %s", url) + return cached + + logger.info("Context %s not in static cache, resolving from URL.", url) + return self._live_load(url, options) + + def _live_load(self, url, options=None): + doc, link_header = self.documents_downloader.download(url, options) + return self.document_parser.parse(doc, link_header) + + +class JsonLdDocumentDownloader: + """JsonLd documents downloader.""" + + def download(self, url: str, options: Dict, **kwargs): + """Download json ld files, checking preconditions on URL.""" + """Retrieves JSON-LD at the given URL. + + :param url: the URL to retrieve. + + :return: the RemoteDocument. + """ + options = options or {} + + try: + # validate URL + pieces = urllib_parse.urlparse(url) + if ( + not all([pieces.scheme, pieces.netloc]) + or pieces.scheme not in ["http", "https"] + or set(pieces.netloc) + > set(string.ascii_letters + string.digits + "-.:") + ): + raise JsonLdError( + 'URL could not be dereferenced; only "http" and "https" ' + "URLs are supported.", + "jsonld.InvalidUrl", + {"url": url}, + code="loading document failed", + ) + if options["secure"] and pieces.scheme != "https": + raise JsonLdError( + "URL could not be dereferenced; secure mode enabled and " + 'the URL\'s scheme is not "https".', + "jsonld.InvalidUrl", + {"url": url}, + code="loading document failed", + ) + headers = options.get("headers") + if headers is None: + headers = {"Accept": "application/ld+json, application/json"} + response = requests.get(url, headers=headers, **kwargs) + + content_type = response.headers.get("content-type") + if not content_type: + content_type = "application/octet-stream" + doc = { + "contentType": content_type, + "contextUrl": None, + "documentUrl": response.url, + "document": response.json(), + } + + return doc, response.headers.get("link") + except Exception as cause: + raise JsonLdError( + "Could not retrieve a JSON-LD document from the URL.", + "jsonld.LoadDocumentError", + code="loading document failed", + cause=cause, + ) + + +class JsonLdDocumentParser: + """JsonLd documents parser.""" + + def parse(self, doc: Dict, link_header: Optional[str]): + """Parse a jsonld document after retrieval.""" + try: + if link_header: + linked_context = parse_link_header(link_header).get(LINK_HEADER_REL) + # only 1 related link header permitted + if linked_context and doc["content_type"] != "application/ld+json": + if isinstance(linked_context, list): + raise JsonLdError( + "URL could not be dereferenced, " + "it has more than one " + "associated HTTP Link Header.", + "jsonld.LoadDocumentError", + {"url": doc["url"]}, + code="multiple context link headers", + ) + doc["contextUrl"] = linked_context["target"] + linked_alternate = parse_link_header(link_header).get("alternate") + # if not JSON-LD, alternate may point there + if ( + linked_alternate + and linked_alternate.get("type") == "application/ld+json" + and not re.match( + r"^application\/(\w*\+)?json$", doc["content_type"] + ) + ): + doc["contentType"] = "application/ld+json" + doc["documentUrl"] = jsonld.prepend_base( + doc["url"], linked_alternate["target"] + ) + return doc + except JsonLdError as e: + raise e + except Exception as cause: + raise JsonLdError( + "Could not retrieve a JSON-LD document from the URL.", + "jsonld.LoadDocumentError", + code="loading document failed", + cause=cause, + ) diff --git a/aries_cloudagent/vc/ld_proofs/document_loader.py b/aries_cloudagent/vc/ld_proofs/document_loader.py index 3c35d7d985..029da19763 100644 --- a/aries_cloudagent/vc/ld_proofs/document_loader.py +++ b/aries_cloudagent/vc/ld_proofs/document_loader.py @@ -8,6 +8,7 @@ from pydid.did_url import DIDUrl from pyld.documentloader import requests +from .document_downloader import StaticCacheJsonLdDownloader from ...cache.base import BaseCache from ...core.profile import Profile from ...resolver.did_resolver import DIDResolver @@ -33,7 +34,8 @@ def __init__(self, profile: Profile, cache_ttl: int = 300) -> None: self.profile = profile self.resolver = profile.inject(DIDResolver) self.cache = profile.inject_or(BaseCache) - self.requests_loader = requests.requests_document_loader() + self.online_request_loader = requests.requests_document_loader() + self.requests_loader = StaticCacheJsonLdDownloader().load self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) self.cache_ttl = cache_ttl self._event_loop = asyncio.get_event_loop() diff --git a/aries_cloudagent/vc/ld_proofs/status_list_context.jsonld b/aries_cloudagent/vc/ld_proofs/status_list_context.jsonld new file mode 100644 index 0000000000..4027c1d499 --- /dev/null +++ b/aries_cloudagent/vc/ld_proofs/status_list_context.jsonld @@ -0,0 +1,55 @@ +{ + "@context": { + "@protected": true, + + "StatusList2021Credential": { + "@id": + "https://w3id.org/vc/status-list#StatusList2021Credential", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "description": "http://schema.org/description", + "name": "http://schema.org/name" + } + }, + + "StatusList2021": { + "@id": + "https://w3id.org/vc/status-list#StatusList2021", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "statusPurpose": + "https://w3id.org/vc/status-list#statusPurpose", + "encodedList": "https://w3id.org/vc/status-list#encodedList" + } + }, + + "StatusList2021Entry": { + "@id": + "https://w3id.org/vc/status-list#StatusList2021Entry", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "statusPurpose": + "https://w3id.org/vc/status-list#statusPurpose", + "statusListIndex": + "https://w3id.org/vc/status-list#statusListIndex", + "statusListCredential": { + "@id": + "https://w3id.org/vc/status-list#statusListCredential", + "@type": "@id" + } + } + } + } +}