Skip to main content

Hash-Based Signing

This guide shows you how to create post-quantum detached signatures by sending only a pre-computed hash to Qpher. The original file never leaves your environment — only its hash is transmitted for signing.

What is Hash-Based Signing?

With hash-based signing, you compute a cryptographic hash (SHA-256, SHA-384, or SHA-512) of your data locally, then send only the hash to the Qpher API for signing with Dilithium3 or Composite-ML-DSA. This produces a detached signature that can be verified independently by anyone who has the original file and your public key.

This approach is ideal when:

  • Your files are too large to upload (>1 MB)
  • Sensitive data must not leave your environment
  • You need to integrate signing into automated pipelines

When to Use Sign-Hash

Use CaseDescription
CI/CD artifact signingSign build artifacts, container images, and release binaries in your pipeline
Firmware signingSign firmware images before distribution to IoT devices
Document integritySign PDFs, contracts, and legal documents without uploading them
Code signingSign source archives, packages, and commits
Large file signingSign files >1 MB without transmitting the full content

Prerequisites

  • A Qpher account with an active API key
  • At least one active Dilithium3 key pair (see Key Management), or an active Composite-ML-DSA key pair for hybrid signatures
  • The key_version of the active signing key you want to use

Quick Start

The workflow is three steps: hash locally, sign the hash, verify later.

Step 1: Compute the Hash Locally

Compute SHA-256 Hash
# Compute SHA-256 hash of a file
HASH=$(shasum -a 256 artifact.tar.gz | awk '{print $1}')

# Convert hex hash to base64
HASH_B64=$(echo -n "$HASH" | xxd -r -p | base64)

echo "Hash (base64): $HASH_B64"

Step 2: Sign the Hash

POST/api/v1/signature/sign-hashSign a pre-computed hash using Dilithium3 or Composite-ML-DSA
Sign the Hash
curl -X POST https://api.qpher.ai/api/v1/signature/sign-hash \
  -H "Content-Type: application/json" \
  -H "x-api-key: qph_your_key_here" \
  -d '{
    "hash": "'$HASH_B64'",
    "hash_algorithm": "SHA-256",
    "key_version": 1
  }'

Step 3: Verify the Signature

POST/api/v1/signature/verify-hashVerify a detached signature against a pre-computed hash
Verify the Hash Signature
curl -X POST https://api.qpher.ai/api/v1/signature/verify-hash \
  -H "Content-Type: application/json" \
  -H "x-api-key: qph_your_key_here" \
  -d '{
    "hash": "'$HASH_B64'",
    "hash_algorithm": "SHA-256",
    "signature": "base64-encoded-signature...",
    "key_version": 1
  }'

Understand the Response

RequestPOST/api/v1/signature/sign-hash
Content-Type: application/json
x-api-key: qph_your_key_here
{
  "hash": "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg=",
  "hash_algorithm": "SHA-256",
  "key_version": 1
}
Response200
{
  "data": {
    "signature": "base64-encoded-signature-3293-bytes...",
    "key_version": 1,
    "algorithm": "Dilithium3",
    "hash_algorithm": "SHA-256",
    "signature_type": "detached"
  },
  "request_id": "880e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2026-02-15T10:40:00Z"
}
FieldDescription
signatureBase64-encoded Dilithium3 signature (3,293 bytes when decoded)
key_versionThe key version used for signing — store this alongside the signature
algorithmThe signature algorithm used (Dilithium3 or Composite-ML-DSA)
hash_algorithmThe hash algorithm specified in the request (SHA-256, SHA-384, or SHA-512)
signature_typeAlways "detached" — indicates the signature covers a hash, not the raw message
request_idUnique request identifier for tracing and support

Supported Hash Algorithms

AlgorithmHash Size (bytes)Base64 LengthNotes
SHA-2563244 charsRecommended default
SHA-3844864 charsHigher security margin
SHA-5126488 charsMaximum security
Hash Algorithm Validation

The API validates that the hash length matches the declared hash_algorithm. A 32-byte hash with hash_algorithm: "SHA-512" will be rejected with ERR_SIG_011.

Algorithm Support

Dilithium3 (Default)

PQC-only signing using ML-DSA-65 (NIST FIPS 204). Available on all plans.

Composite-ML-DSA Hybrid Signatures (Starter+)

Composite-ML-DSA Hybrid Signatures (Starter+)

For defense-in-depth, add "algorithm": "Composite-ML-DSA" to combine ECDSA P-256 classical signatures with ML-DSA-65. Both signature components must verify for the result to be valid. Requires an active Composite-ML-DSA key pair — see Hybrid Cryptography.

Sign Hash with Composite-ML-DSA
curl -X POST https://api.qpher.ai/api/v1/signature/sign-hash \
  -H "Content-Type: application/json" \
  -H "x-api-key: qph_your_key_here" \
  -d '{
    "hash": "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg=",
    "hash_algorithm": "SHA-256",
    "key_version": 1,
    "algorithm": "Composite-ML-DSA"
  }'

Integration Example: GitHub Actions

Sign and verify build artifacts in your CI/CD pipeline:

name: Build and Sign
on:
push:
tags: ["v*"]

jobs:
build-and-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build artifact
run: |
tar -czf artifact.tar.gz dist/

- name: Sign artifact with Qpher
env:
QPHER_API_KEY: ${{ secrets.QPHER_API_KEY }}
QPHER_KEY_VERSION: ${{ vars.QPHER_SIGN_KEY_VERSION }}
run: |
# Compute SHA-256 hash
HASH_B64=$(shasum -a 256 artifact.tar.gz | awk '{print $1}' | xxd -r -p | base64)

# Sign the hash
RESPONSE=$(curl -s -X POST https://api.qpher.ai/api/v1/signature/sign-hash \
-H "Content-Type: application/json" \
-H "x-api-key: $QPHER_API_KEY" \
-d "{
\"hash\": \"$HASH_B64\",
\"hash_algorithm\": \"SHA-256\",
\"key_version\": $QPHER_KEY_VERSION
}")

# Save the signature
echo "$RESPONSE" | jq -r '.data.signature' > artifact.tar.gz.sig
echo "Artifact signed successfully"

- name: Verify signature
env:
QPHER_API_KEY: ${{ secrets.QPHER_API_KEY }}
QPHER_KEY_VERSION: ${{ vars.QPHER_SIGN_KEY_VERSION }}
run: |
HASH_B64=$(shasum -a 256 artifact.tar.gz | awk '{print $1}' | xxd -r -p | base64)
SIGNATURE=$(cat artifact.tar.gz.sig)

RESPONSE=$(curl -s -X POST https://api.qpher.ai/api/v1/signature/verify-hash \
-H "Content-Type: application/json" \
-H "x-api-key: $QPHER_API_KEY" \
-d "{
\"hash\": \"$HASH_B64\",
\"hash_algorithm\": \"SHA-256\",
\"signature\": \"$SIGNATURE\",
\"key_version\": $QPHER_KEY_VERSION
}")

VALID=$(echo "$RESPONSE" | jq -r '.data.valid')
if [ "$VALID" != "true" ]; then
echo "ERROR: Signature verification failed!"
exit 1
fi
echo "Signature verified successfully"

- name: Upload signed artifact
uses: actions/upload-artifact@v4
with:
name: signed-artifact
path: |
artifact.tar.gz
artifact.tar.gz.sig

Detached vs Embedded Signatures

Qpher offers two signing modes:

Feature/signature/sign (Embedded)/signature/sign-hash (Detached)
InputFull message (base64)Pre-computed hash (base64)
File leaves your environmentYesNo — only the hash is sent
Verifier needsSignature + key_versionOriginal file + signature + key_version + hash_algorithm
Best forSmall messages (<1 MB)Large files, sensitive data, CI/CD
sign-hash is NOT the same as sign(hash(message))

The result of /signature/sign-hash differs from calling /signature/sign with a pre-hashed message. Dilithium3 internally hashes its input during signing, so sign(hash(m)) produces Sign(H(hash(m))) while sign-hash(hash(m)) uses a dedicated code path that correctly binds the hash algorithm metadata. Always use the matching verify endpoint: signatures from sign-hash must be verified with verify-hash.

CLI Convenience Commands

The qpher CLI computes the hash locally and calls the API for you:

# Sign a file (computes SHA-256 locally, sends hash to API)
qpher sign-file artifact.tar.gz --key-version 1

# Sign with a specific hash algorithm
qpher sign-file firmware.bin --key-version 1 --hash-algorithm SHA-512

# Verify a signed file
qpher verify-file artifact.tar.gz --signature artifact.tar.gz.sig --key-version 1

# Verify with explicit hash algorithm
qpher verify-file firmware.bin --signature firmware.bin.sig --key-version 1 --hash-algorithm SHA-512
Install the CLI

Install the Qpher CLI with npm install -g qpher-cli or pip install qpher-cli. Run qpher --help for all available commands.

Error Handling

HTTP StatusError CodeCauseResolution
400ERR_SIG_010Unsupported hash algorithmUse SHA-256, SHA-384, or SHA-512
400ERR_SIG_011Hash length does not match declared algorithmEnsure the hash byte length matches: SHA-256=32, SHA-384=48, SHA-512=64
400ERR_INVALID_001Missing required fieldEnsure hash, hash_algorithm, and key_version are provided
401ERR_AUTH_001Invalid or missing API keyCheck your x-api-key header
404ERR_SIG_003Key version not foundVerify the key_version exists for your tenant
409ERR_SIG_004Key is not in active statusOnly active keys can sign — generate or rotate to get a new active key
429ERR_RATE_001Rate limit exceededReduce request frequency or upgrade your plan
500ERR_CRYPTO_001Signing operation failedRetry the request; contact support if persistent