Signing Requests (User Action Signature)

All mutating requests must be signed with a user or service account credential. A User Action Signature is required to authorize sensitive actions and to provide an auditable trail of changes. A cryptographically signed audit log is available through the Audit Logs API, allowing you to review who requested and validated each action in IBM Digital Asset Haven. For more information on implementing different signing flows, see User Action Signing flows.

Process

Signing is a four-step process.
Get a challenge from the IBM Digital Asset Haven system.
A signing challenge is returned from a call to Get a User Action Challenge. The response includes the following properties (additional properties exist for signing with WebAuthn).
Field Description
challenge String to be signed with your private key
challengeIdentifier JWT identifying the signing session
allowCredentials List of private key credentials enabled for the user
Sign the challenge
The signing method depends on the credential type.
  • Human users: Typically use passkeys. The OS and browser manage the signing process in the frontend.
  • Machine users: Use asymmetric keys. Signing occurs in the backend with a cryptographic library.
Format the challenge into a Client Data object, convert it to JSON, and sign it using one of the credentials listed in allowCredentials.
Return the signed challenge to the IBM Digital Asset Haven system
Call Create the User Action Signature. Provide the base64url-encoded signed challenge, client data, and the credential ID used for signing. The response includes a one-time-use token.
Include the User Action Signature with your API call
Use the token to authorize the original API request.

User signing flow using a Fido2 passkey

An example flow where the user signs the challenge in the frontend is displayed below.

For more information, see the FIDO2 signing flow. You can also test this flow with the delegated wallets tutorial.

Backend signing flow using an asymmetric key pair

Service accounts must also sign sensitive requests. When creating a service account, generate a key pair and register the public key with IBM Digital Asset Haven. The private key is used to sign challenges.

For more information, see the Asymmetric Keys signing flow. In the diagram, KMS represents a recommended secure storage solution for credentials.
Note: When using the TypeScript or Python SDK with a configured signer, signing is handled automatically. The examples below show manual implementation for reference.
TypeScript (manual)
const signChallenge = async (challenge: UserActionSignatureChallenge) : Promise<SignedChallenge> => {

  // The data being signed includes information that is important for validating the request originated from a valid location.
  const clientData: Buffer = Buffer.from(
    JSON.stringify({
      type: 'key.get',
      challenge: challenge.challenge,
      origin: origin,
      crossOrigin: false,
    } as ClientData)
  )

  // Signing can be done locally or by calling an external signer (like AWS KMS).
  const signature = crypto.sign(
    undefined,
    clientData,
    apiKeyPrivateKey
  )

  // Pass back the signature, and the data that was signed so both can be parsed and validated properly.
  return {
    clientData: clientData.toString('base64url'),
    credId: challenge.allowCredentials.key[0].id,
    signature: signature.toString('base64url'),
  }
}
Python (manual)
import json
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, ec

def sign_challenge(challenge: dict, private_key_pem: str, origin: str) -> dict:
    # The data being signed includes information for validating the request origin
    client_data = json.dumps({
        "type": "key.get",
        "challenge": challenge["challenge"],
        "origin": origin,
        "crossOrigin": False,
    }, separators=(',', ':')).encode()

    # Load the private key
    private_key = serialization.load_pem_private_key(
        private_key_pem.encode(),
        password=None
    )

    # Sign with the appropriate algorithm based on key type
    if isinstance(private_key, ec.EllipticCurvePrivateKey):
        signature = private_key.sign(client_data, ec.ECDSA(hashes.SHA256()))
    else:
        signature = private_key.sign(client_data)

    # Return base64url-encoded values
    def to_base64url(data: bytes) -> str:
        return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

    return {
        "clientData": to_base64url(client_data),
        "credId": challenge["allowCredentials"]["key"][0]["id"],
        "signature": to_base64url(signature),
    }
Python SDK (automatic)
from dfns_sdk import DfnsClient, DfnsClientConfig, KeySigner

# Configure the signer once - signing is then automatic
signer = KeySigner(
    credential_id="cr-...",
    private_key=open("/path/to/private-key.pem").read(),
    app_origin="https://your-app.example.com"
)

config = DfnsClientConfig(auth_token="...", signer=signer)

# Signing happens automatically for state-changing operations
with DfnsClient(config) as client:
    wallet = client.wallets.create_wallet({"network": "EthereumSepolia"})