Black Box Signatures

Black-box signing enables users to leverage Fordefi’s MPC technology to sign any arbitrary blob, subject to the client's transaction policy, in a distributed way, without exposing the private key.

📘

Note

API users must strongly authenticate transaction requests that are created programmatically by signing them. Learn more.

Black-box vaults

When creating a new vault using the Fordefi API, there is an option to set the vault type as black_box as well as its key type. Currently, the supported key types are ecdsa_stark and ecdsa_secp256k1.

Response body:

  • public_key_compressed is the public key of the vault in the standard compressed format, according to the SEC1 standard. This format encodes the least bit of the y-coordinate in the first byte. (\\x02 for even y-coordinate, and \\x03 for odd). Following is the x-coordinate.

    Use this public key on the client side to calculate the address, based on your specific use case (meaning, chain).

  • For stark vaults, under the details section, there is a stark_key field, which is the corresponding public key of this vault.

Black-box signatures

Black-box signing is possible only using Fordefi’s REST API. While calling the Create Transaction endpoint, the client must:

  • Provide a vault_id (for the vault that was previously created as a black_box type).
  • Set the type field to black_box_signature.
  • Use the details field to provide either a binary hash or integer hash, as described in the following section and in the code sample.

Stark signatures

Payloads to stark signing are a result of a Pedersen hash. Depending on your implementation of the Pedersen hash, the result of this hash might be an integer, or encoded in 32-byte representation, according to Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA).

Fordefi supports these formats:

  • hash_integer: This is the integer representation, encoded as a hex string, starting with 0x and followed by an even number of hex-digits.
  • hash_binary: This is the 32-byte encoded format.

Once the payload is signed, under the details field in the Get Transaction response, the signature object will hold the R- and S-values of the signature.

Example

This example demonstrate how to use the new stark vault for creating and signing stark signatures. The request should be inside the body, as demonstrated here.

import requests
import base64
import json
import datetime
import ecdsa
import hashlib

# https://github.com/starkware-libs/cairo-lang
from starkware.crypto.signature.signature import verify, pedersen_hash

# Obtain the hash of the transaction
payload_int = pedersen_hash(0xdeadbeaf)

# Build the signature request
request_json = {
    "vault_id": vault_id,
    "note":" string",
    "signer_type": "api_signer",
    "type": "black_box_signature",
    "details": {
        "format": "hash_integer",
        "hash_integer": "0x{:064x}".format(payload_int),
    },
}

# Alternatively, you can use the binary hash
# this is useful if pedersen_hash implementation you are using returns bytes according to https://www.rfc-editor.org/rfc/rfc6979#section-2.3.2
payload_bytes = b"0" * 32
payload_encoded = base64.b64encode(payload_bytes).decode()

request_json = {
    "vault_id": vault_id,
    "note":" string",
    "signer_type": "api_signer",
    "type": "black_box_signature",
    "details": {
        "format": "hash_binary",
        "hash_binary": payload_encoded,
    },
}

request_body = json.dumps(request_json)
private_key_file = "private.pem"
timestamp = datetime.datetime.now().strftime("%s")
payload = f"/api/v1/transactions|{timestamp}|{request_body}"

with open(private_key_file, "r") as f:
    signing_key = ecdsa.SigningKey.from_pem(f.read())

signature = signing_key.sign(data=payload.encode(), hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_der)

resp_tx = requests.post(
    f"https://{gateway_host}/api/v1/transactions",
    headers={
        "Authorization": f"Bearer {access_token}",
        "x-signature": base64.b64encode(signature),
        "x-timestamp": timestamp.encode(),
    },
    data=request_body,
).json()

# Obtain the signature result
transaction_id = resp_tx["id"]
signed_transaction = requests.get(
    f"https://{gateway_host}/api/v1/transactions/{transaction_id}",
    headers={
        "Authorization": f"Bearer {access_token}",
    }
).json()

r = int(signed_transaction["details"]["signature"]["r"], 16)
s = int(signed_transaction["details"]["signature"]["s"], 16)

# Obtain your vault's public key
vault = requests.get(
    f"https://{gateway_host}/api/v1/vaults/{vault_id}",
    headers={
        "Authorization": f"Bearer {access_token}",
    }
).json()

stark_key = int(vault["details"]["stark_key"], 16)

# Verify the signature
assert verify(payload_int, r, s, stark_key)

# You can also use the public key in the compressed format
compressed_public_key = base64.b64decode(vault["public_key_compressed"])

# This is the relation between the compressed public key and the stark key
assert int.from_bytes(compressed_public_key[1:], byteorder="big") == stark_key

ECDSA-secp256k1 signatures

Black-box signatures can be used to sign EVM transactions for EVM chains that Fordefi does not yet support. In this case, you will manage the EVM address derivation, the nonce, and the gas and send the transaction to the node yourself. You will use Fordefi’s API only to (black-box) sign the hash of the transaction.

Examples

The following sample shows how to use the public key on the client side to derive your EVM address from the new secp256k1 vault. The request should be inside the body, as demonstrated here.

import base64
import ecdsa
from Crypto.Hash import keccak
import requests

from eth_utils import to_checksum_address

import requests

# Obtain your vault's public key
vault = requests.get(
    f"https://{gateway_host}/api/v1/vaults/{vault_id}",
    headers={
        "Authorization": f"Bearer {access_token}",
    }
).json()

public_key_compressed = vault["public_key_compressed"]
public_key_compressed_decoded = base64.b64decode(public_key_compressed)
computed_public_key = ecdsa.VerifyingKey.from_string(public_key_compressed_decoded, curve=ecdsa.SECP256k1).to_string()
hasher = keccak.new(digest_bits=256)
computed_address = "0x" + hasher.update(computed_public_key).hexdigest()[-40:]
check_sum_address = to_checksum_address(computed_address)

This example demonstrate how to use the new secp256k1 vault for a specific use case of creating and signing EVM transactions. The request should be inside the body, as demonstrated here.

import base64
import datetime
import hashlib
import ecdsa
import json
import requests

from eth_account._utils.legacy_transactions import serializable_unsigned_transaction_from_dict, encode_transaction


chain_id = 1  # mainnet

# Build the signature request
transaction_dict = {
    'to': '0x95524e2Ab866D3615F81C6921C6A8fb6e85fA823',
    'value': 1,
    'gas': 21000,
    'gasPrice': 500000000,
    'nonce': 0,
    'chainId': chain_id,
}

unsigned_transaction = serializable_unsigned_transaction_from_dict(transaction_dict)
payload_encoded = base64.b64encode(unsigned_transaction.hash()).decode()

request_json = {
    "vault_id": vault_id,
    "signer_type": "api_signer",
    "type": "black_box_signature",
    "details": {
        "format": "hash_binary",
        "hash_binary": payload_encoded,
    },
}

request_body = json.dumps(request_json)
private_key_file = "private.pem"
timestamp = datetime.datetime.now().strftime("%s")
payload = f"/api/v1/transactions|{timestamp}|{request_body}"

with open(private_key_file, "r") as f:
    signing_key = ecdsa.SigningKey.from_pem(f.read())

signature = signing_key.sign(data=payload.encode(), hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_der)

resp_tx = requests.post(
    f"https://{gateway_host}/api/v1/transactions",
    headers={
        "Authorization": f"Bearer {access_token}",
        "x-signature": base64.b64encode(signature),
        "x-timestamp": timestamp.encode(),
    },
    data=request_body,
).json()
transaction_id = resp_tx["id"]

# Obtain the signature result
signed_transaction = requests.get(
    f"https://{gateway_host}/api/v1/transactions/{transaction_id}",
    headers={
        "Authorization": f"Bearer {access_token}",
    }
).json()

r = int(signed_transaction["details"]["signature"]["r"], 16)
s = int(signed_transaction["details"]["signature"]["s"], 16)
v = signed_transaction["details"]["signature"]["v"] + 35 + 2 * chain_id

encoded_transaction = encode_transaction(unsigned_transaction, vrs=(v, r, s))
hex_to_send = "0x" + encoded_transaction.hex()