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
- 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 challengeString to be signed with your private key challengeIdentifierJWT identifying the signing session allowCredentialsList 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.
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.
- 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"})