How to sign requests made with the HTTP API?

@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
# }