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 Case | Description |
|---|---|
| CI/CD artifact signing | Sign build artifacts, container images, and release binaries in your pipeline |
| Firmware signing | Sign firmware images before distribution to IoT devices |
| Document integrity | Sign PDFs, contracts, and legal documents without uploading them |
| Code signing | Sign source archives, packages, and commits |
| Large file signing | Sign 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_versionof 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 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
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
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
/api/v1/signature/sign-hashContent-Type: application/json
x-api-key: qph_your_key_here{
"hash": "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg=",
"hash_algorithm": "SHA-256",
"key_version": 1
}{
"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"
}| Field | Description |
|---|---|
signature | Base64-encoded Dilithium3 signature (3,293 bytes when decoded) |
key_version | The key version used for signing — store this alongside the signature |
algorithm | The signature algorithm used (Dilithium3 or Composite-ML-DSA) |
hash_algorithm | The hash algorithm specified in the request (SHA-256, SHA-384, or SHA-512) |
signature_type | Always "detached" — indicates the signature covers a hash, not the raw message |
request_id | Unique request identifier for tracing and support |
Supported Hash Algorithms
| Algorithm | Hash Size (bytes) | Base64 Length | Notes |
|---|---|---|---|
| SHA-256 | 32 | 44 chars | Recommended default |
| SHA-384 | 48 | 64 chars | Higher security margin |
| SHA-512 | 64 | 88 chars | Maximum security |
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+)
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.
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) |
|---|---|---|
| Input | Full message (base64) | Pre-computed hash (base64) |
| File leaves your environment | Yes | No — only the hash is sent |
| Verifier needs | Signature + key_version | Original file + signature + key_version + hash_algorithm |
| Best for | Small messages (<1 MB) | Large files, sensitive data, CI/CD |
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 Qpher CLI with npm install -g qpher-cli or pip install qpher-cli. Run qpher --help for all available commands.
Error Handling
| HTTP Status | Error Code | Cause | Resolution |
|---|---|---|---|
| 400 | ERR_SIG_010 | Unsupported hash algorithm | Use SHA-256, SHA-384, or SHA-512 |
| 400 | ERR_SIG_011 | Hash length does not match declared algorithm | Ensure the hash byte length matches: SHA-256=32, SHA-384=48, SHA-512=64 |
| 400 | ERR_INVALID_001 | Missing required field | Ensure hash, hash_algorithm, and key_version are provided |
| 401 | ERR_AUTH_001 | Invalid or missing API key | Check your x-api-key header |
| 404 | ERR_SIG_003 | Key version not found | Verify the key_version exists for your tenant |
| 409 | ERR_SIG_004 | Key is not in active status | Only active keys can sign — generate or rotate to get a new active key |
| 429 | ERR_RATE_001 | Rate limit exceeded | Reduce request frequency or upgrade your plan |
| 500 | ERR_CRYPTO_001 | Signing operation failed | Retry the request; contact support if persistent |
Related Guides
- Sign Documents — Sign messages directly (embedded signatures)
- Verify Signatures — Verify Dilithium3 signatures
- Key Management — Generate and manage your signing keys
- Hybrid Cryptography — Use Composite-ML-DSA for classical+PQC defense-in-depth