"""Contains the functions needed to produce and verify a json-ld signature.
This file was ported from
https://github.com/transmute-industries/Ed25519Signature2018/blob/master/src/createVerifyData/index.js
"""
import datetime
import hashlib
from pyld import jsonld
from .error import (
DroppedAttributeError,
MissingVerificationMethodError,
SignatureTypeError,
)
def _canonize(data, document_loader=None):
return jsonld.normalize(
data,
{
"algorithm": "URDNA2015",
"format": "application/n-quads",
**{opt: document_loader for opt in ["documentLoader"] if document_loader},
},
)
def _sha256(data):
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def _canonize_signature_options(signatureOptions, document_loader=None):
_signatureOptions = {**signatureOptions, "@context": "https://w3id.org/security/v2"}
_signatureOptions.pop("jws", None)
_signatureOptions.pop("signatureValue", None)
_signatureOptions.pop("proofValue", None)
return _canonize(_signatureOptions, document_loader)
def _canonize_document(doc, document_loader=None):
_doc = {**doc}
_doc.pop("proof", None)
return _canonize(_doc, document_loader)
def _created_at():
"""Creation Timestamp."""
stamp = datetime.datetime.now(datetime.timezone.utc)
return stamp.strftime("%Y-%m-%dT%H:%M:%SZ")
[docs]def create_verify_data(data, signature_options, document_loader=None):
"""Encapsulate process of constructing string used during sign and verify."""
signature_options["type"] = signature_options.get("type", "Ed25519Signature2018")
type_ = signature_options.get("type")
if type_ != "Ed25519Signature2018":
raise SignatureTypeError(f"invalid signature type {type_}.")
if not signature_options.get("verificationMethod"):
raise MissingVerificationMethodError(
"signature_options.verificationMethod is required"
)
signature_options["created"] = signature_options.get("created", _created_at())
[expanded] = jsonld.expand(
data,
options={
**{opt: document_loader for opt in ["documentLoader"] if document_loader}
},
)
framed = jsonld.compact(
expanded,
"https://w3id.org/security/v2",
options={
"skipExpansion": True,
**{opt: document_loader for opt in ["documentLoader"] if document_loader},
},
)
# Detect any dropped attributes during the expand/contract step.
if len(data) > len(framed):
# > check indicates dropped attrs < is a different error
# attempt to collect error report data
for_diff = jsonld.compact(
expanded,
data.get("@context"),
options={
**{
opt: document_loader
for opt in ["documentLoader"]
if document_loader
}
},
)
dropped = set(data.keys()) - set(for_diff.keys())
raise DroppedAttributeError(
f"{dropped} attributes dropped. "
"Provide definitions in context to correct."
)
# Check proof for dropped attributes
attr = [
("proof", "proof"),
("credentialSubject", "https://www.w3.org/2018/credentials#credentialSubject"),
]
data_context = data.get("@context")
for mapping in attr:
data_attribute = data.get(mapping[0], {})
frame_attribute = framed.get(mapping[1], {})
if len(data_attribute) > len(frame_attribute):
for_diff = jsonld.compact(
expanded,
data_context,
options={
**{
opt: document_loader
for opt in ["documentLoader"]
if document_loader
}
},
)
for_diff_attribute = for_diff.get(mapping[1], {})
dropped = set(data_attribute.keys()) - set(for_diff_attribute.keys())
raise DroppedAttributeError(
f"in {mapping[0]}, {dropped} attributes dropped. "
"Provide definitions in context to correct."
)
canonized_signature_options = _canonize_signature_options(
signature_options, document_loader
)
hash_of_canonized_signature_options = _sha256(canonized_signature_options)
canonized_document = _canonize_document(framed, document_loader)
hash_of_canonized_document = _sha256(canonized_document)
return (framed, hash_of_canonized_signature_options + hash_of_canonized_document)