@zfer I’ve now made some progress and managed to produce valid payload data, but the signature is still wrong even I’ve tried to reproduce the all the steps from the JS implementation in python. Maybe there some parameter I’m missing. Is there anyone that could give me some advice re signing? My guess is that the problem lies within the sign_ed25519 function. Am I correct to assume that the signed payload is similar to this?
payload_for_signing = {
"did": did_str,
"payload": payload_cid.decode("utf-8"),
"linkedBlock": b64encode(encoded_bytes).decode('utf-8'),
}
FTR, this is the full example:
import dag_cbor
import hashlib
import json
from base64 import urlsafe_b64encode, b64encode
from jwcrypto import jwk, jws
from jwcrypto.common import json_encode
from multiformats import CID
from jwcrypto.common import json_encode, base64url_encode
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey
)
from cryptography.hazmat.primitives import serialization
DAG_CBOR_CODEC_CODE = 113
SHA2_256_CODE = 18
def encode_cid(multihash: bytearray, cid_version:int=1, code:int=DAG_CBOR_CODEC_CODE) -> bytearray:
"""CID encoding""" # JavasCript
code_offset = 1 # varint.encodingLength(cid_version)
hash_offset = 2 # codeOffset + varint.encodingLength(code)
_bytes = bytearray([0] * (hash_offset + len(multihash))) # new Uint8Array(hashOffset + multihash.byteLength)
_bytes.insert(0, cid_version) # varint.encodeTo(version, bytes, 0)
_bytes.insert(code_offset, code) # varint.encodeTo(code, bytes, codeOffset)
_bytes[hash_offset:] = multihash # bytes.set(multihash, hashOffset)
return _bytes
def create_digest(digest: bytearray, code:int=SHA2_256_CODE) -> bytearray:
"""Create a digest""" # JavasCript
size = len(digest)
size_offset = 1 # varint.encodingLength(code)
digest_offset = 2 # sizeOffset + varint.encodingLength(size)
_bytes = bytearray([0] * (digest_offset + size))
_bytes.insert(0, code) # varint.encodeTo(code, bytes, 0)
_bytes.insert(size_offset, size) # varint.encodeTo(size, bytes, sizeOffset)
_bytes[digest_offset:] = digest # _bytes.set(digest, digestOffset)
return _bytes
def base64UrlEncode(data):
""""Base64 encoding"""
return urlsafe_b64encode(data).rstrip(b"=")
def sign_ed25519(payload, seed):
"""Sign a payload using EdDSA (ed25519)"""
# Create an ed25519 from the seed
key_ed25519 = Ed25519PrivateKey.from_private_bytes(bytearray.fromhex(seed))
# Derive the public and private keys
# private key
d = base64url_encode(
key_ed25519.private_bytes(
serialization.Encoding.Raw,
serialization.PrivateFormat.Raw,
serialization.NoEncryption()
)
)
# public key
x = base64url_encode(
key_ed25519.public_key().public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw
)
)
# Create a JWK key compatible with the jwcrypto library
# https://jwcrypto.readthedocs.io/en/latest/jwk.html#classes
# To create a random key: key = jwk.JWK.generate(kty='OKP', size=256, crv='Ed25519')
key = jwk.JWK(
**{
"crv":"Ed25519",
"d": d, # private key
"kty":"OKP",
"size":256,
"x": x, # public key
}
)
# Create the JWS token from the payload
jwstoken = jws.JWS(json_encode(payload))
# Sign the payload
# https://github.com/latchset/jwcrypto/blob/fcdc7d76b5a5924f9343a92b2627944a855ae62a/jwcrypto/jws.py#L477
jwstoken.add_signature(
key=key,
alg=None,
protected=json_encode({"alg": "EdDSA"}), # the algorithm can be specified here or in the alg param, but results are different
header=json_encode({"kid": key.thumbprint()}) # optional
)
signature_data = jwstoken.serialize()
return signature_data
def create_JWS(payload: dict, did_seed: str) -> dict:
"""Create the JSON Web Signature of a Ceramic update payload"""
# This code replicates the following JS implementation:
# https://github.com/ceramicnetwork/js-did/blob/101e27cd306aced9322ec01a920b987006625d51/packages/dids/src/did.ts#L301
# https://github.com/ceramicnetwork/js-did/blob/101e27cd306aced9322ec01a920b987006625d51/packages/dids/src/did.ts#L265
# Encode the payload using the DAG_CBOR codec
encoded_bytes = dag_cbor.encode(payload)
# SHA256 hash
hashed = create_digest( bytearray.fromhex(hashlib.sha256(encoded_bytes).hexdigest()) )
# Create the hash CID
cid = CID(base="base32", version=1, codec=DAG_CBOR_CODEC_CODE, digest=hashed)
# Create the payload CID
cid_bytes = encode_cid(hashed)
payload_cid = base64UrlEncode(cid_bytes)
# Build the payload for signing
payload_for_signing = {
"did": did_str,
"payload": payload_cid.decode("utf-8"),
"linkedBlock": b64encode(encoded_bytes).decode('utf-8'),
}
# Sign the payload using ed25519
# https://github.com/ceramicnetwork/key-did-provider-ed25519/blob/60c78dce7df4d7231bc5280dfcb8d9c953d12a20/src/index.ts#L73
# https://github.com/decentralized-identity/did-jwt/blob/4efd9a755738a8a6347611cbded042a133d0b91a/src/JWT.ts#L272
signature_data = json.loads(sign_ed25519(payload_for_signing, did_seed))
# Build the update payload
update_payload = {
"jws": {
"payload": payload_cid.decode("utf-8"),
"signatures": [
{
"protected": signature_data["protected"],
"signature": signature_data["signature"]
}
],
"link": str(cid)
},
"linkedBlock": encoded_bytes.hex()
}
return update_payload
# Use the same seed and payload as in the JS example
did_seed = "0101010101010101010101010101010101010101010101010101010101010101"
did_str = "did:key:z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX"
payload = {"hello": "world"}
jws = create_JWS(payload, did_seed)
print(jws["jws"]["signatures"])
# Expected result
# {
# jws: {
# payload: 'AXESIHhRlyKdyLsRUpRdpY4jSPfiee7e0GzCynNtDoeYWLUB', # OK
# signatures: [ # NOT OK
# {
# protected: 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa29uM05lY2Q2TmtreWZvR29IeGlkMnpuR2M1OUxVM0s3bXViYVJjRmJMZkxYI3o2TWtvbjNOZWNkNk5ra3lmb0dvSHhpZDJ6bkdjNTlMVTNLN211YmFSY0ZiTGZMWCJ9',
# signature: 'pfC2-D8iobSVBZo_42NvEKoZyUSopvtLSC3SMfU-LWnps90b1rJ3qnDK6dlNf2DcEXTDHgRL7OuWTTC83yo-CA'
# }
# ],
# link: CID(bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae) # OK
# },
# linkedBlock: "a16568656c6c6f65776f726c64" # OK
# }