QSeal API Signature Generation
Complete guide to creating QSeal signatures for Neonomics API requests, including HTTP headers, digest creation, and signature verification with Java and JavaScript examples.
QSeal API Signature Generation
This comprehensive guide explains how to create and sign API requests to Neonomics using QSeal certificates, ensuring message integrity and authentication for secure communication.
Get up and running with QSeal signatures in minutes using our ready-to-use examples
Enterprise-grade authentication using industry-standard QSeal certificates
What You'll Learn
This guide provides everything you need to implement QSeal signing:
- HTTP Header Requirements - Required headers for different request types
- Digest Creation - SHA-256 hash generation for request bodies
- Signature Verification - Step-by-step signature creation and validation
- Ready-to-Use Examples - Complete implementations in Java and JavaScript
Why QSeal Signatures?
QSeal (Qualified Electronic Seal) certificates provide legally recognized digital signatures that ensure:
- Authentication - Verify the request sender's identity
- Integrity - Detect any tampering with request data
- Non-repudiation - Legal proof of transaction authenticity
Request Signing Overview
Each API request to Neonomics must include a cryptographic signature created with your QSeal certificate. The signing process varies based on the HTTP method:
Requests without a body
These requests require basic headers for identification and timing, plus the signature.
Requests with a body
These requests require additional headers including content type and a digest of the request body.
Required Headers
The headers you need depend on your HTTP method:
GET and DELETE Requests
| Header | Description | Example |
|---|---|---|
(request-target) | HTTP method and path (auto-generated by signing libraries) | get /ics/v3/banks |
x-originating-host | Host name from which the request originates | neonomics.io |
x-originating-date | Request creation timestamp (RFC 7231 format) | Mon, 07 Feb 2023 00:28:05 GMT |
Signature | Generated signature string | See examples below |
POST, PUT, and PATCH Requests
| Header | Description | Example |
|---|---|---|
(request-target) | HTTP method and path | post /ics/v3/session |
x-originating-host | Host name from which the request originates | neonomics.io |
x-originating-date | Request creation timestamp (RFC 7231 format) | Mon, 07 Feb 2023 00:28:05 GMT |
Content-Type | MIME type of the request body | application/json |
Digest | Base64-encoded SHA-256 hash of the request body | SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= |
Signature | Generated signature string | See examples below |
Digest Creation
For requests with a body, you must create a digest header containing the SHA-256 hash:
Digest: SHA-256=<base64(sha256(request_body))>
Digest Calculation Process
- Hash the Body - Calculate SHA-256 hash of the request body bytes
- Encode to Base64 - Convert the hash bytes to Base64 string
- Format Header - Prepend "SHA-256=" to create the digest header
Neonomics validates this digest by hashing the received payload and comparing values.
Example:
Digest: SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
Signature Format
Key Requirements
- Key ID: Must be your Neonomics application/client ID
- Algorithm: Always use
rsa-sha256 - Headers: List all headers included in the signature
Without Request Body (GET/DELETE)
Signature: keyId="your-neonomics-app-id",
algorithm="rsa-sha256",
headers="(request-target) x-originating-host x-originating-date",
signature="MEUCIQD1r3F3EwVQx1D9YVjfq0V..."
With Request Body (POST/PUT/PATCH)
Signature: keyId="your-neonomics-app-id",
algorithm="rsa-sha256",
headers="(request-target) x-originating-host x-originating-date content-type digest",
signature="goYZZGdm2TI30jWpn9LR7KUZV6FHTen3e5M23PbXOfmmn+SjX29vXBNDieUSgDzxEEKodqwVh/Nf7mGi251vB34VVdzTGi6vJ5jditpwIXilsBZjn8ctmDLOPgAZlCx7PKE0ad+DTw3mDSbamB/m2xwBnhaAOD6g5ikXCyjATTFRhGZuBdF8qYXpcthbD0+EPrRWUszPj38nMdrfxZJCZ1Ef2XX88Qw7s4XSez9MOPbjl0EscR+Y4jbb6PckjEPLnqBAJJ8e2tYUbdAeTKWpZV1C1LtUML3o/y2BdJf2L6PMqzGHd3GwaNTBIVgNLEcOKafTyju9am1u87hX5GJUSw=="
Implementation Examples
Choose your preferred language to get started quickly:
Java Implementation
Complete example using the Tomitribe HTTP Signatures library.
Maven Dependency
<dependency>
<groupId>org.tomitribe</groupId>
<artifactId>tomitribe-http-signatures</artifactId>
<version>1.8</version>
</dependency>Complete Example
import org.tomitribe.auth.signatures.*;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.stream.Collectors;
import java.util.Base64;
/**
* QSeal signature generation for Neonomics API requests
*
* This example demonstrates the complete flow:
* 1. Loading QSeal private key from PEM file
* 2. Creating request digest (for POST/PUT/PATCH)
* 3. Generating cryptographic signature
* 4. Building complete HTTP headers
*/
public class NeonomicsQSealSigner {
public static void main(String[] args) throws Exception {
// Example usage
SignedRequest request = createSignedRequest();
System.out.println("=== SIGNED REQUEST HEADERS ===");
request.getHeaders().forEach((key, value) ->
System.out.println(key + ": " + value));
System.out.println("\n=== REQUEST BODY ===");
System.out.println(request.getBody());
}
/**
* Create a complete signed request ready to send to Neonomics
*/
public static SignedRequest createSignedRequest() throws Exception {
// Load your QSeal private key
RSAPrivateKey privateKey = loadPrivateKey("path/to/qseal-key.pem");
SignedRequest request = new SignedRequest();
request.setMethod("POST");
request.setUrl("https://api.neonomics.io/ics/v3/session");
request.setBody("{\"bankId\":\"YWt0aWEuZmkudjFIRUxTRklISA==\"}");
// Step 1: Create digest for request body
String digest = createDigest(request.getBody());
// Step 2: Prepare required headers
Map<String, String> headers = new LinkedHashMap<>();
headers.put("x-originating-host", "neonomics.io");
headers.put("x-originating-date", getCurrentRFC7231Date());
headers.put("content-type", "application/json");
headers.put("digest", digest);
// Step 3: Generate signature
String signature = generateSignature(request, headers, privateKey);
headers.put("signature", signature);
request.setHeaders(headers);
return request;
}
/**
* Load RSA private key from PEM file
*/
public static RSAPrivateKey loadPrivateKey(String filePath) throws Exception {
String keyContent = Files.readString(new File(filePath).toPath());
// Remove PEM headers and whitespace
String privateKeyPEM = keyContent
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
// Decode and create key
byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
}
/**
* Create SHA-256 digest header for request body
*/
private static String createDigest(String body) throws NoSuchAlgorithmException {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash = sha256.digest(body.getBytes(Charset.forName("UTF-8")));
return "SHA-256=" + Base64.getEncoder().encodeToString(hash);
}
/**
* Generate cryptographic signature using Tomitribe library
*/
private static String generateSignature(SignedRequest request,
Map<String, String> headers,
PrivateKey privateKey) throws IOException {
URL url = new URL(request.getUrl());
// Define headers to include in signature
List<String> headerNames = headers.keySet().stream()
.map(String::toLowerCase)
.collect(Collectors.toList());
headerNames.add("(request-target)");
// Create signature configuration
String keyId = "your-neonomics-app-id"; // Replace with your actual ID
Signature signatureConfig = new Signature(keyId, "rsa-sha256", null, headerNames);
Signer signer = new Signer(privateKey, signatureConfig);
// Generate path with query parameters
String path = url.getPath();
if (url.getQuery() != null) {
path += "?" + url.getQuery();
}
// Create and return signature
Signature signature = signer.sign(request.getMethod(), path, headers);
return signature.toString();
}
/**
* Get current date in RFC 7231 format
*/
private static String getCurrentRFC7231Date() {
// Implementation would return current date in RFC 7231 format
// For example: "Mon, 07 Feb 2023 00:28:05 GMT"
return "Mon, 07 Feb 2023 00:28:05 GMT";
}
/**
* Simple data structure to hold request information
*/
public static class SignedRequest {
private String method;
private String url;
private String body;
private Map<String, String> headers = new HashMap<>();
// Getters and setters
public String getMethod() { return method; }
public void setMethod(String method) { this.method = method; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getBody() { return body; }
public void setBody(String body) { this.body = body; }
public Map<String, String> getHeaders() { return headers; }
public void setHeaders(Map<String, String> headers) { this.headers = headers; }
}
}Key Features
- Automatic digest calculation for request bodies
- PEM key loading with proper format handling
- Complete signature generation using industry standards
- Ready-to-use structure that integrates with your existing code
JavaScript/Node.js Implementation
Complete example using Node.js built-in crypto module.
Prerequisites
- Node.js 18+ (for native crypto support)
- QSeal private key in PEM format
Complete Example
import fs from "node:fs";
import crypto from "node:crypto";
import { URL } from "node:url";
/**
* QSeal signature generation for Neonomics API requests
*
* This example demonstrates:
* 1. Reading QSeal private key from PEM file
* 2. Creating request digest using SHA-256
* 3. Generating HTTP signature according to QSeal standards
* 4. Building complete signed request object
*/
// Configuration constants
const CONFIG = {
PRIVATE_KEY_PATH: "/path/to/your/qseal-key.pem",
API_BASE_URL: "https://api.neonomics.io",
KEY_ID: "your-neonomics-app-id", // Replace with your actual ID
ALGORITHM: "rsa-sha256"
};
/**
* Create a complete signed request for the Neonomics API
*/
export function createSignedRequest(endpoint, method = "POST", body = null) {
try {
// Load private key
const privateKey = loadPrivateKey(CONFIG.PRIVATE_KEY_PATH);
// Build request object
const request = {
method: method.toUpperCase(),
url: `${CONFIG.API_BASE_URL}${endpoint}`,
body: body,
headers: {}
};
// Create required headers
const headers = buildRequiredHeaders(request, privateKey);
request.headers = headers;
return request;
} catch (error) {
throw new Error(`Failed to create signed request: ${error.message}`);
}
}
/**
* Load RSA private key from PEM file
*/
function loadPrivateKey(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`Private key file not found: ${filePath}`);
}
return fs.readFileSync(filePath, "utf8");
}
/**
* Build all required headers for the request
*/
function buildRequiredHeaders(request, privateKey) {
const headers = {
"x-originating-host": "neonomics.io",
"x-originating-date": getCurrentRFC7231Date()
};
// Add content-specific headers for requests with body
if (request.body) {
headers["content-type"] = "application/json";
headers["digest"] = createDigestHeader(request.body);
}
// Generate and add signature
headers["signature"] = createSignatureHeader(request, headers, privateKey);
return headers;
}
/**
* Create SHA-256 digest header for request body
*/
function createDigestHeader(body) {
if (!body) {
throw new Error("Cannot create digest for empty body");
}
const hash = crypto
.createHash("sha256")
.update(body, "utf8")
.digest("base64");
return `SHA-256=${hash}`;
}
/**
* Create the HTTP signature header
*/
function createSignatureHeader(request, headers, privateKey) {
const url = new URL(request.url);
const path = url.pathname + (url.search || "");
// Determine which headers to include in signature
const headerNames = Object.keys(headers)
.map(h => h.toLowerCase())
.concat(["(request-target)"]);
// Build the signing string
const signingString = headerNames
.map(name => {
if (name === "(request-target)") {
return `(request-target): ${request.method.toLowerCase()} ${path}`;
}
return `${name}: ${headers[name]}`;
})
.join("\n");
// Create signature
const signature = crypto
.createSign("RSA-SHA256")
.update(signingString)
.sign(privateKey, "base64");
// Format complete signature header
return [
`keyId="${CONFIG.KEY_ID}"`,
`algorithm="${CONFIG.ALGORITHM}"`,
`headers="${headerNames.join(" ")}"`,
`signature="${signature}"`
].join(", ");
}
/**
* Get current date in RFC 7231 format
*/
function getCurrentRFC7231Date() {
return new Date().toUTCString();
}
/**
* Example usage and testing
*/
async function demonstrateUsage() {
try {
// Example 1: Create session (POST with body)
const sessionRequest = createSignedRequest(
"/ics/v3/session",
"POST",
JSON.stringify({ bankId: "YWt0aWEuZmkudjFIRUxTRklISA==" })
);
console.log("=== SESSION REQUEST ===");
console.log(`${sessionRequest.method} ${sessionRequest.url}`);
console.log("\nHeaders:");
Object.entries(sessionRequest.headers).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
console.log(`\nBody: ${sessionRequest.body}`);
// Example 2: Get banks (GET without body)
const banksRequest = createSignedRequest("/ics/v3/banks", "GET");
console.log("\n=== BANKS REQUEST ===");
console.log(`${banksRequest.method} ${banksRequest.url}`);
console.log("\nHeaders:");
Object.entries(banksRequest.headers).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
} catch (error) {
console.error("Error:", error.message);
}
}
// Run example if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
demonstrateUsage();
}Usage Examples
// Create different types of requests
import { createSignedRequest } from './qseal-signer.js';
// POST request with JSON body
const sessionRequest = createSignedRequest(
"/ics/v3/session",
"POST",
JSON.stringify({ bankId: "bank-id-here" })
);
// GET request without body
const banksRequest = createSignedRequest("/ics/v3/banks", "GET");
// Use with your HTTP client
const response = await fetch(sessionRequest.url, {
method: sessionRequest.method,
headers: sessionRequest.headers,
body: sessionRequest.body
});Key Features
- Native Node.js crypto - No external dependencies required
- Flexible request builder - Works with any HTTP method and endpoint
- Automatic header management - Handles all required QSeal headers
- Error handling - Clear error messages for troubleshooting
- Production ready - Includes proper validation and edge case handling
Testing Your Implementation
Test your digest generation by comparing the SHA-256 hash with online tools or other implementations.
Ensure all required headers are present and correctly formatted according to the HTTP Signature specification.
Common Issues and Solutions
Signature Verification Failed
Common causes:
- Incorrect key ID (must match your Neonomics application ID)
- Wrong header order in the signature
- Malformed digest calculation
- Incorrect date format (must be RFC 7231)
Solution: Double-check each header value and ensure they match exactly what was used to generate the signature.
Digest Mismatch
Common causes:
- Character encoding issues (ensure UTF-8)
- Including extra whitespace or newlines
- Wrong hash algorithm (must be SHA-256)
Solution: Hash the exact request body bytes that will be sent over the wire.
Next Steps
Once you have QSeal signatures working:
- Test thoroughly with different request types and endpoints
- Implement error handling for signature failures and retries
- Cache your private key to avoid repeated file I/O operations
- Monitor signature performance in production environments
- Keep certificates up to date and handle rotation properly
Copy the examples above and adapt them to your specific needs. Both Java and JavaScript implementations are production-ready and follow industry best practices.
Updated 8 months ago
