Securing Privy API requests with authorization signatures
Authorization signatures protect your resources and wallet operations by cryptographically signing API requests. When enabled, these signatures prevent malicious actors from tampering with your request payload or forging requests to your wallets, ensuring that only authorized servers can trigger wallet actions.
Authorization signatures are a security feature used to secure update requests to all critical
resources.
All critical resources have an owner_id field, which indicates the authorization key or quorum whose signatures are required in order to modify the given resource.
This means, if the owner_id is set, authorization signatures are required for all PATCH and DELETE requests to the resource.
Key quorums do not have owners, but rather require a satisfaying set of signatures from the key quorum itself.
You can add additional, non-owner, signers to a wallet by specifying them in the additional_signers field on the wallet resource.
Signatures from the wallet’s owner are required to take actions on a wallet by default. If an owner_id is set, authorization signatures are required for:
POST requests to /v1/wallets/<wallet_id>/rpc
Authorization signatures are an important security measure and we strongly recommend registering
authorization keys for all production resources.
The private key (e.g. the Authorization key you copy) is generated on your device. Privy only
receives the public key for registration. The private key never leaves the client.
To register an authorization key with a resource, you need to specify the public key in the owner field when creating a resource (e.g. wallet or policy).
You can generate authorization signatures in three ways:
Using PrivyClient via the Server SDK. PrivyClient will automatically inject authorization signatures when required. Learn more.
Using utility functions from the Server SDK such as formatRequestForAuthorizationSignature and generateAuthorizationSignature.
Implementing your own signing logic.
Follow the steps below to generate an authorization signature with Server SDK utility functions or your own signing logic:
1
Build Payload
Generate a JSON payload containing the following fields. All fields are required unless otherwise specified.
Field
Type
Description
version
1
Authorization signature version. Currently, 1 is the only version.
method
`‘POST'
'PUT'
'PATCH'
'DELETE’`
HTTP method for the request. Signatures are not required on 'GET' requests.
url
string
The full URL for the request. Should not include a trailing slash.
body
JSON
JSON body for the request.
headers
JSON
JSON object containing any Privy-specific headers, e.g. those that are prefixed with 'privy-'. This should not include any other headers, such as authentication headers, content-type, or trace headers.
headers['privy-app-id']
string
Privy app ID header (required).
headers['privy-idempotency-key']
string
Privy idempotency key header (optional). If the request does not contain an idempotency key, leave this field out of the payload.
2
Format Payload
Canonicalize the payload per RFC 8785 and serialize it to a string. This GitHub repository links to various libraries for JSON canonicalization in different languages.
Copy
Ask AI
// You can use the Server SDK to format and serialize the payload.import {formatRequestForAuthorizationSignature} from '@privy-io/server-auth/wallet-api';const input: WalletApiRequestSignatureInput = ...;const serializedPayload = formatRequestForAuthorizationSignature(input);
3
Sign Payload
Sign the serialized JSON with ECDSA P-256 using your app’s private key and serialize it to a base64-encoded string.
Copy
Ask AI
// You can use the Server SDK to generate an authorization signature.// Note that this function will format, serialize, and sign the payload, making Step 2 redundant.import {generateAuthorizationSignature} from '@privy-io/server-auth/wallet-api';const input: WalletApiRequestSignatureInput = ...;const signature = generateAuthorizationSignature(input, authorizationPrivateKey);
We recommend using PrivyClient to send WalletApi requests, which will automatically inject authorization signatures when required.
However, if you must send WalletApi requests without PrivyClient, we recommend using generateAuthorizationSignature to sign the request or formatRequestForAuthorizationSignature to format and serialize the request if you need to sign elsewhere.
Copy
Ask AI
import {PrivyClient} from '@privy-io/server-auth';const client = new PrivyClient($PRIVY_APP_ID, $PRIVY_APP_SECRET, { walletApi: { // This is required in order to generate the privy-authorization-signature header. authorizationPrivateKey: $PRIVY_AUTHORIZATION_PRIVATE_KEY }});// This request will automatically include the privy-authorization-signature header.const res = await client.walletApi.ethereum.signMessage({ walletId: $WALLET_ID, message: 'Hello world'});
We recommend using PrivyClient to send WalletApi requests, which will automatically inject authorization signatures when required.
However, if you must send WalletApi requests without PrivyClient, we recommend using generateAuthorizationSignature to sign the request or formatRequestForAuthorizationSignature to format and serialize the request if you need to sign elsewhere.
Copy
Ask AI
import {PrivyClient} from '@privy-io/server-auth';const client = new PrivyClient($PRIVY_APP_ID, $PRIVY_APP_SECRET, { walletApi: { // This is required in order to generate the privy-authorization-signature header. authorizationPrivateKey: $PRIVY_AUTHORIZATION_PRIVATE_KEY }});// This request will automatically include the privy-authorization-signature header.const res = await client.walletApi.ethereum.signMessage({ walletId: $WALLET_ID, message: 'Hello world'});
Copy
Ask AI
import canonicalize from 'canonicalize'; // Support JSON canonicalizationimport crypto from 'crypto'; // Support P-256 signing// Replace this with your private key from the Dashboardconst PRIVY_AUTHORIZATION_KEY = 'wallet-auth:insert-your-private-key-here';// ...function getAuthorizationSignature({url, body}: {url: string; body: object}) { const payload = { version: 1, method: 'POST', url, body, headers: { 'privy-app-id': 'insert-your-app-id' // If your request includes an idempotency key, include that header here as well } }; // JSON-canonicalize the payload and convert it to a buffer const serializedPayload = canonicalize(payload) as string; const serializedPayloadBuffer = Buffer.from(serializedPayload); // Replace this with your app's authorization key. We remove the 'wallet-auth:' prefix // from the key before using it to sign requests const privateKeyAsString = PRIVY_AUTHORIZATION_KEY.replace('wallet-auth:', ''); // Convert your private key to PEM format, and instantiate a node crypto KeyObject for it const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`; const privateKey = crypto.createPrivateKey({ key: privateKeyAsPem, format: 'pem' }); // Sign the payload buffer with your private key and serialize the signature to a base64 string const signatureBuffer = crypto.sign('sha256', serializedPayloadBuffer, privateKey); const signature = signatureBuffer.toString('base64'); return signature;}const authorizationSignature = getAuthorizationSignature({ // Replace with your desired path url: 'https://api.privy.io/v1/wallets/<wallet_id>/rpc', // Replace with your desired body body: { method: 'personal_sign', params: { message: 'Hello world', // ... }, }});
Copy
Ask AI
import jsonimport base64from cryptography.hazmat.primitives import hashes, serializationfrom cryptography.hazmat.primitives.asymmetric import ec, utils# Replace this with your private key from the DashboardPRIVY_AUTHORIZATION_KEY = "wallet-auth:your-authorization-private-key"def canonicalize(obj): """ Simple JSON canonicalization function. Sorts dictionary keys and ensures consistent formatting. """ return json.dumps(obj, sort_keys=True, separators=(",", ":"))def get_authorization_signature(url, body): """ Generate authorization signature for Privy API requests using ECDSA and hashlib. """ # Construct the payload payload = { "version": 1, "method": "POST", "url": url, "body": body, "headers": {"privy-app-id": "insert-your-app-id"}, } # Serialize the payload to JSON serialized_payload = canonicalize(payload) # Create ECDSA P-256 signing key from private key private_key_string = PRIVY_AUTHORIZATION_KEY.replace("wallet-auth:", "") private_key_pem = ( f"-----BEGIN PRIVATE KEY-----\n{private_key_string}\n-----END PRIVATE KEY-----" ) # Load the private key from PEM format private_key = serialization.load_pem_private_key( private_key_pem.encode("utf-8"), password=None ) # Sign the message using ECDSA with SHA-256 signature = private_key.sign( serialized_payload.encode("utf-8"), ec.ECDSA(hashes.SHA256()) ) # Convert the signature to base64 for easy transmission return base64.b64encode(signature).decode("utf-8")authorization_signature = get_authorization_signature( url="https://api.privy.io/v1/wallets", body={ "chain_type": "ethereum", },)
Copy
Ask AI
use anyhow::{anyhow, Result};use p256::ecdsa::{signature::Signer, Signature, SigningKey};use base64::{engine::general_purpose::STANDARD, Engine as _};use serde_json::json;/// Signs the canonicalized JSON payload using ECDSA (P-256 + SHA-256).////// - `private_key_string` - A string containing your authorization key with the 'wallet-api:' prefix removed./// - `payload` - JSON payload to sign, serialized to a string///fn sign_payload(private_key_string: &str, payload: &str) -> Result<String> { let bytes = extract_32_byte_key_from_pkcs8_base64(private_key_string)?; let signing_key = SigningKey::from_slice(bytes.as_slice())?; // Sign the payload (SHA-256 is implied by ECDSA in P256's default) let signature: Signature = signing_key.sign(payload.as_bytes()); // base64 encode the signature let signature_b64 = STANDARD.encode(signature.to_der()); Ok(signature_b64)}/// Extracts the raw 32-byte private key from a base64-encoded PKCS#8 blob./// Returns an error if `0x04 0x20` cannot be found or if the data is too short.fn extract_32_byte_key_from_pkcs8_base64(pkcs8_b64: &str) -> Result<[u8; 32]> { // 1. Decode base64 let pkcs8_bytes = STANDARD.decode(pkcs8_b64)?; // 2. Search for the 2-byte pattern [0x04, 0x20] let pattern = [0x04, 0x20]; let private_key_start = pkcs8_bytes .windows(pattern.len()) .position(|window| window == pattern) .ok_or(anyhow!( "Invalid wallet authorization private key: marker not found" ))?; // 3. Extract the 32 bytes following 0x04, 0x20 let start = private_key_start + 2; let end = start + 32; if end > pkcs8_bytes.len() { return Err(anyhow!( "Invalid wallet authorization private key: data too short" )); } let mut private_key_bytes = [0u8; 32]; private_key_bytes.copy_from_slice(&pkcs8_bytes[start..end]); Ok(private_key_bytes)}/// Main function to generate the authorization signature.fn main() -> Result<()> { let privy_authorization_key = "wallet-auth:your-authorization-private-key"; let private_key_string = privy_authorization_key.replace("wallet-auth:", ""); let url = "https://api.privy.io/v1/wallets"; let body = json!({ "chain_type": "ethereum" }); // --- Build the payload to sign --- let mut payload = json!({ "version": 1, "method": "POST", "url": url, "body": body, "headers": { "privy-app-id": "insert-your-app-id" } }); // --- Canonicalize (sort keys, minimal separators) and serialize --- payload.sort_all_objects(); let serialized_payload = serde_json::to_string(&payload)?; println!("{}", serialized_payload); // --- Sign the serialized payload using P-256 ECDSA --- let authorization_signature = sign_payload(&private_key_string, &serialized_payload)?; println!("{}", authorization_signature); Ok(())}
Copy
Ask AI
import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "hash/fnv" "strings" "crypto/sha256")// SignPayload signs the canonicalized JSON payload using ECDSA (P-256 + SHA-256).//// privyAuthorizationKey - A string containing your authorization key with the 'wallet-auth:' prefix included.// payload - JSON payload to sign, serialized to a string//// Returns the base64-encoded DER signature or an error.func SignPayload(privyAuthorizationKey string, payload string) (string, error) { privateKey, err := parsePrivateKeyFromAuthorizationKey(privyAuthorizationKey) if err != nil { return "", fmt.Errorf("failed to parse private key: %w", err) } // Hash the payload using SHA-256 hash := sha256.Sum256([]byte(payload)) // Sign the hash signature, err := ecdsa.SignASN1(rand.Reader, privateKey, hash[:]) if err != nil { return "", fmt.Errorf("failed to sign payload: %w", err) } // Base64 encode the signature signatureB64 := base64.StdEncoding.EncodeToString(signature) return signatureB64, nil}// We parse the ecdsa key from the authorization key herefunc parsePrivateKeyFromAuthorizationKey(privyAuthorizationKey string) (*ecdsa.PrivateKey, error) { pkcs8B64 := strings.TrimPrefix(privyAuthorizationKey, "wallet-auth:") pkcs8Bytes, err := base64.StdEncoding.DecodeString(pkcs8B64) if err != nil { return nil, err } // This handles PKCS#8 parsing automatically key, err := x509.ParsePKCS8PrivateKey(pkcs8Bytes) if err != nil { return nil, err } // Type assert to ECDSA private key ecdsaKey, ok := key.(*ecdsa.PrivateKey) if !ok { return nil, fmt.Errorf("key provided is not an ECDSA private key") } return ecdsaKey, nil}// Utility function to verify the signature (for testing purposes)func VerifySignature(publicKey *ecdsa.PublicKey, payload, signatureB64 string) (bool, error) { // Decode the base64 signature signature, err := base64.StdEncoding.DecodeString(signatureB64) if err != nil { return false, fmt.Errorf("failed to decode signature: %w", err) } // Hash the payload hash := sha256.Sum256([]byte(payload)) // Verify the signature valid := ecdsa.VerifyASN1(publicKey, hash[:], signature) return valid, nil}