Skip to content

Message Signature

To generate a message signature, cryptographic keys of the ED25519 variants must first be created.

The example signature base below is determined from the content digest example request.

"content-type": application/json
"content-digest": sha-512=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:
"content-length": 18
"authorization": Bearer 123
"x-client-id": 123456
"x-idempotency-key": 123456
"@method": POST
"@target-uri": https://api.pipevest.com/v1/customers?sort=ASC
"@path": /v1/customers
"@query": ?sort=ASC
"@signature-params": ("content-type" "content-digest" "content-length" "authorization" "x-client-id" "x-idempotency-key" "@method" "@target-uri" "@path" "@query");keyid="staging-pipevest-ed25519";created=1732893484;expires=1732893584

The field @signature-params contains an ordered list of components that make up the signature base. It is made up of two sections, fields and meta data.

"@signature-params": (fields...);meta

  • fields: ordered, space ( ) separated and contained within ellipsis (...)
  • meta: unorder, semi-colon (;) separated and specify additional information about the cryptographic operation

Retrieve Private Key

Identify the private key for the given environment. You should have one for:

  • Staging
  • Production

Determine Signature Base

Use the instructions here to determine which fields to include to generate the signature.

Calculate Signature

  1. Sign the signature base directly with the environment-specific ED25519 private key. ED25519 applies SHA-512 internally as part of the algorithm (RFC 8032) — do not pre-hash the base yourself.
  2. Base64 encode the raw signature bytes. This is the final computed message signature (computed-signature).

Signature Input

The signature input is the same as the @signature-params and will be passed along the header. The inputs allow for the signature to be recomputed and verified.

Make Signed Request

Add Signature and Signature-Input to the request header

  • Signature: sig1=:<computed-signature>:
  • Signature-Input: sig1=<@signature-params>
Terminal window
curl --request POST \
--url https://api.pipevest.com/v1/customers?sort=ASC \
--header 'Content-Digest: sha-512=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:' \
--header 'Content-Length: 18' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer 123456' \
--header 'Signature: sig1=:OTEyMjY4...A5NTNDMEQ=:' \
--header 'Signature-Input: sig1=("content-type" "content-digest" "content-length" "authorization" "x-client-id" "x-idempotency-key" "@method" "@target-uri" "@path" "@query");keyid="staging-pipevest-ed25519";created=1732893484;expires=1732893584' \
--header 'X-Client-Id: 123456' \
--header 'X-Idempotency-Key: 123456' \
...
--data '{"firstName": "John", "lastName": "Doe"}'

Retrieve Public Key

Identify the public key for the given environment. You should have one for:

  • Staging
  • Production

Rebuild the Signature Base

Use the Signature-Input from the HTTP header to reconstruct the signature base. The covered components and their order come directly from Signature-Input, so the base you rebuild is byte-identical to the one the sender signed.

Identify Header Signature

Strip the sig1=: prefix and the trailing : suffix from the Signature header to obtain the header-base64-signature.

Terminal window
curl --request POST \
--url https://api.staging.pipevest.com/v1/customers \
--header 'Signature: sig1=:OTEyMjY4...A5NTNDMEQ=:' \
....

Decode Signature

Base64-decode the header-base64-signature into the raw signature bytes (header-message-signature).

Verify with the Public Key

Verify the header-message-signature against the rebuilt signature base directly, using the partner’s ED25519 public key. ED25519 verification hashes the base with SHA-512 internally — there is no separate digest step.

verified = ed25519_verify(publicKey, signatureBase, header-message-signature)

Accept or Reject

Accept the message only if verification succeeds. Reject it otherwise.

import crypto from 'node:crypto'
// Get the ED25519 public key (SPKI PEM)
const publicKey = ...
// Get Signature and Signature-Input from the original http message header
const signature = request.headers['Signature']
const signatureInput = request.headers['Signature-Input']
// Rebuild the exact signature base using the covered components in `signatureInput`
const signatureBase = `...` // determined using `signatureInput`
// Strip `sig1=:` / `:` and base64-decode to the raw signature bytes
const headerBase64Signature = signature.match(/:(.*):/).pop()
const headerMessageSignature = Buffer.from(headerBase64Signature, 'base64')
// ED25519 verifies the signature over the signature base directly.
// Pass `null` as the algorithm (SHA-512 is internal to ED25519); do NOT pre-hash.
const isSignatureVerified = crypto.verify(
null,
Buffer.from(signatureBase, 'utf8'),
publicKey,
headerMessageSignature,
)
Fields Request Types Required Note
Content-Type POST, PUT, PATCH Yes
Content-Digest POST, PUT, PATCH Yes
Content-Length POST, PUT, PATCH Yes
Authorization GET, POST, PUT, PATCH, DELETE Yes See note below
X-Client-Id GET, POST, PUT, PATCH, DELETE Yes See note below
X-Idempotency-Key POST, PUT, PATCH, DELETE Yes
@method GET, POST, PUT, PATCH, DELETE Yes
@target-uri GET, POST, PUT, PATCH, DELETE Yes
@path GET, POST, PUT, PATCH, DELETE Yes
@query GET, POST, PUT, PATCH, DELETE Yes
@signature-params GET, POST, PUT, PATCH, DELETE Yes
Name Description Required Note
keyid The id of the public key registered with Pipevest. Yes ex: staging-pipevest-ed25519
created The Unix timestamp (seconds) when the cryptographic operation took place Yes
expires Unix timestamp (seconds) after which the signature is rejected — e.g. created + 100 No Not needed, but recommended