# Request Signing Certain endpoints require secondary validation prior to the request being accepted for processing. We use Ed25519 based signatures to validate requests. These endpoints will specify the `x-signature` and `x-timestamp` headers as additional parameters. ## Signing Keys You will generate the signing key pair when onboarding. You are required to provide us the generated Ed25519 public key to complete onboarding. You must use the associated Ed25519 signing key (private key) when creating signatures for requests. Please note that signing keys (Ed25519 private keys) should be stored securely. The signing key should only be used to derive request signatures and should never be sent in a request. Layer2 will never request you share your private key. ## Generating the Signing Key You must securely generate an Ed25519 key pair on your own hardware and retain both the public and private portions. Layer2 requires the 64-character (32 bytes) hex-encoded Ed25519 during onboarding. Below is a NodeJS code sample to generate keypairs ```javascript const crypto = require('crypto'); // Generate the Ed25519 key pair const keyPair = crypto.generateKeyPairSync('ed25519'); // Extract raw private and public keys const rawPrivateKey = keyPair.privateKey.export({ type: 'pkcs8', format: 'der' }); const rawPublicKey = keyPair.publicKey.export({ type: 'spki', format: 'der' }); // Convert to hex format for easier representation const rawPrivateKeyHex = rawPrivateKey.toString('hex'); const rawPublicKeyHex = rawPublicKey.toString('hex'); console.log('Raw Private Key (Hex):', rawPrivateKeyHex); console.log('Raw Public Key (Hex): ', rawPublicKeyHex); ``` ## Signing a request To sign a request, generate a request signature using the Ed25519 private (signing) key you generated during onboarding (using the script above), and provide it alongside the request in the `x-signature` header together with the `x-timestamp` header. Signing is a 3 step process (below), details of each field using in signing is explained below. | Step | Action | Description | | --- | --- | --- | | 1 | Concatenate | Concatenate the `timestamp`, `method`, `request path`, and `body` into a string. There are no spaces or other characters between these values. The order of the fields MUST follow the order stipulated here. | | 2 | Sign | Take the string from step 1 and generate a signature using your Ed25519 private key | | 3 | Encode | Take the signed value and hex-encode the output. | The fields used to generate the signature are as follows. If the conditions below are not met, your signature will not validate. | Field | Description | | --- | --- | | Timestamp | The number of seconds since the Unix Epoch in UTC, and must be within one minute of the API service's time when the. The same value MUST be also placed in the `x-timestamp` header. | | Method | Uppercase HTTP verb (GET, PUT, POST, DELETE) | | Request Path | Lowercase path minus the domain, including ALL query parameters and values (ex. /api/v1/accounts/payments/1001-1234/address?type=abc). | | Body | Should be omitted where there is no body (e.g. GET). The `stringified` HTTP request body. | ## Signing Example You can validate your signing code using the example below; | Field | Value | | --- | --- | | Timestamp | 1527380000 | | Method | POST | | Request Path | /api/v1/accounts/payments/1001-1234/address?type=abc | | Body | {"amount": "100","payment_reference": "FUND01-00023423","payor_id": "0000-0003"} | | Signing Key | 302e020100300506032b6570042204200df0ce421b0830759ea9bfa727c0f4d0aa7086cfaf26c66e7e85bd10787d5728 | | Public Key | 302a300506032b657003210095de28d850d6be3525384323b5add134dcb9b3bb404f43cbf47dac5e11c351de | | Signature Key | 51b19da0a23377bbb72222ba78bc32f0ec24404ac24b1a0c8f6942f2eb9e26bd6ffb078b9630a376f45360b74861f29198a81d93c2ae09971969b19532a9a800 | Below is a NodeJS example of performing this signing operation ```javascript const crypto = require('crypto'); function hexToPem(hexString, type = 'PUBLIC') { const base64String = Buffer.from(hexString, 'hex').toString('base64'); const header = `-----BEGIN ${type} KEY-----\n`; const footer = `\n-----END ${type} KEY-----`; return header + base64String.match(/.{1,64}/g).join('\n') + footer; } function signMessage(privateKeyPem, message) { return crypto.sign(null, Buffer.from(message, 'utf8'), privateKeyPem).toString('hex'); } const encodedPrivateKey = "302e020100300506032b6570042204200df0ce421b0830759ea9bfa727c0f4d0aa7086cfaf26c66e7e85bd10787d5728"; // Replace with your actual encoded private key var time = "1527380000"; var method = "POST"; var path = "/api/v1/accounts/payments/1001-1234/address?type=abc"; var body = '{"amount": "100","payment_reference": "FUND01-00023423","payor_id": "0000-0003"}'; var message = `${time}${method}${path}${body}`; // sign const privateKeyPem = hexToPem(encodedPrivateKey, "PRIVATE"); const signature = signMessage(privateKeyPem, message); console.log(`Signature: ${signature}`); ``` ## Verify a request (webhooks) When receiving webhooks from Layer2, the signing operation is performed in reverse and you must validate a signature we provide to ensure the webhook originated from Layer2. When creating a subscription (using the subscription API), you are provided a `signature_verification_key`. This is the public portion of a Layer2 Ed25519 key pair that you must use to verify the signature of the webhook we send to you. You must save this locally as part of generating subscriptions. Base64 The returned `signature_verification_key` is base64 encoded binary. You should base64 decode and covert to HEX to use in these code samples. When the webhook is dispatched to you, Layer2 will populate the `x-signature` header with the signature, and provide the `x-timestamp` for you to reconstruct the message for verification. Verification is a 2 step process (below), details of each field is explained below. | Step | Action | Description | | --- | --- | --- | | 1 | Concatenate | Concatenate the `timestamp`, `method`, `request path`, and `body` into a string. There are no spaces or other characters between these values. The order of the fields MUST follow the order stipulated here. | | 2 | Verify | Take the string from step 1 and verify the signature using the provided Ed25519 public key | The fields used to generate the signature are as follows. If the conditions below are not met, your signature will not validate. | Field | Description | | --- | --- | | Timestamp | Taken directly from the `x-timestamp` header. | | Method | Uppercase HTTP verb (GET, PUT, POST, DELETE). This will ALWAYS be a POST for webhooks | | Request Path | Lowercase path minus the domain, including ALL query parameters and values. The `request path` is everything after the FQDN that the webhook was dispatched to. For example if you registered the following endpoint `https://you.url.com/callbacks/layer2/webhook_end_point?send=here`, the request path is `/callbacks/layer2/webhook_end_point?send=here` | | Body | The `stringified` HTTP request body of the webhook. | Stringify When `stringifying` the body, you MUST ensure that no parsing is performed. In many cases parsers (JavaScript parsers are particularly problematic) will reformat certain data field which will prevent the signing operation working correctly. In many cases trailing zeros on amounts are truncated. If using JavaScript, use a buffer parser and covert immediately to a string to prevent this happening. Below is a NodeJS example of a verification operation ```javascript const crypto = require('crypto'); function hexToPem(hexString, type = 'PUBLIC') { const base64String = Buffer.from(hexString, 'hex').toString('base64'); const header = `-----BEGIN ${type} KEY-----\n`; const footer = `\n-----END ${type} KEY-----`; return header + base64String.match(/.{1,64}/g).join('\n') + footer; } function verifySignature(publicKeyPem, message, signature) { return crypto.verify(null, Buffer.from(message, 'utf8'), publicKeyPem, Buffer.from(signature, 'hex')); } function decodeBase64(encodedString) { const buffer = Buffer.from(encodedString, 'base64'); return buffer.toString('hex'); } // need to base64 decode public key returned from creating a subscription AND convert to hex const encodedPublicKey = decodeBase64("MCowBQYDK2VwAyEAO79OxmhDQNqTo0cSfy3vO5t2hjZO7JWeiCDULvEMHAY="); var time = "1704931925543"; var method = "POST"; var path = "/layer2/events/0f4c9ce9f2766b2af37ea8ac3fcbb7b5"; var body = '{"event_id":"d2ca6da9-cbc5-4c46-affa-13771cb3dbcd","event_date":"2024-01-10T19:12:05","event_type":"TRANSACTION_POSTED","event_data":{"transaction":{"id":"7a16f3db-7f5c-4f35-b32b-51ff0b9e2df2","account_id":"REDFI_100512.FIAT_TESTNET_USD","value":150.000000000000000000,"transaction_date":"2024-01-10T19:12:01","transaction_posted_date":"2024-01-10T19:12:00","transaction_status":"POSTED","transaction_type":"FIAT_TRANSFER_IN","category_type":"DEPOSIT","category_id":"583e6911-dfc4-4fd5-a502-5fe6c215db9a"}}}'; var message = `${time}${method}${path}${body}`; // verify const signature = '1b228a400d0acb970272f97d6bc71e13602f459cf34607dfc003d09f22a94fc13bdd8b59718b0369df5bbbe2354e8e20a2ebca2330a4425d871075ebd6a0f00c'; const publicKeyPem = hexToPem(encodedPublicKey, "PUBLIC"); const isValid = verifySignature(publicKeyPem, message, signature); console.log(isValid ? "Signature is valid!" : "Signature is NOT valid!"); ```