Kulcare Docs
Uhi

UHI Authentication & Header Generation

Every API call in the UHI network — both outbound requests we make and inbound callbacks we receive — must carry a signed Authorization header. This document explains the algorithm, the header format, and how Kulcare generates and verifies headers.


Algorithm Overview

UHI uses a two-step signing scheme:

1. Hash the request body   →  BLAKE-512 (via Blake2b, 64-byte output)
2. Sign the signing string →  Ed25519

Both algorithms are provided by the rbnacl gem.


Header Format

The Authorization header is a JSON string (not HTTP Signature format, not AWS Signature).

{
  "headers": "(created) (expires) digest",
  "keyId": "kulcare.eua|kulcare.euapid|ed25519",
  "algorithm": "ed25519",
  "created": "1713600000",
  "expires": "1713603600",
  "signature": "<base64-encoded Ed25519 signature>"
}

Field Reference

FieldFormatDescription
headersliteral stringAlways "(created) (expires) digest"
keyIdsubscriber_id|public_key_id|ed25519Identifies signer; used by recipient to fetch public key
algorithm"ed25519"Always fixed
createdUnix timestamp (string)When the header was generated
expiresUnix timestamp (string)Validity window (1 hour from created)
signatureBase64Ed25519 signature of the signing string

Signing String Construction

The signing string is built from three fields, joined by spaces. Note the space after the colon — this is mandated by the UHI spec.

(created): 1713600000 (expires): 1713603600 digest: BLAKE-512=<base64-hash>

Step by step:

# 1. Hash the body
digest = blake512_hash(request_body_json)   # Base64-encoded 64-byte Blake2b

# 2. Build the signing string
signing_string = "(created): #{created} (expires): #{expires} digest: BLAKE-512=#{digest}"

# 3. Sign with Ed25519
signature = ed25519_sign(signing_string, private_key_base64)

Private Key Formats

The NHA issues private keys in different formats. The AuthService handles all three:

Key length (bytes)FormatExtraction
32Raw Ed25519 seedUse as-is
48NHA standard formatExtract bytes [16..47]
OtherASN.1 DERParse with OpenSSL, extract inner value

Rails Credentials Structure

# config/credentials.yml.enc (edit with: rails credentials:edit)

uhi:
  gateway_host: "https://uhigatewaysandbox.abdm.gov.in"
  consumer_id: "SBX_001751"
  consumer_uri: "https://api-stg.kulcare.com/uhi"
  # signature_verification: false  # optional; see "Turning verification off" below

  # Physical & Tele Consultation (domain: nic2004:85111 / nic2004:85121)
  tele_physical_consultation:
    domain: "nic2004:85111"
    eua:
      subscriber_id: "kulcare.eua"
      public_key_id: "kulcare.euapid"
      public_key: "MCow..." # Ed25519 public key (Base64)
      private_key: "<base64>" # Ed25519 private key (Base64, keep secret)
    hspa:
      subscriber_id: "kulcare.hspa"
      public_key_id: "kulcare.hspapid"
      public_key: "MCow..."
      private_key: "<base64>"

  # PMJAY (domain: nic2004:85131)
  pmjay:
    domain: "nic2004:85131"
    eua:
      subscriber_id: "kulcare.eua"
      public_key_id: "kulcare.euapid.pmjay"
      public_key: "MCow..."
      private_key: "<base64>"

Security note: private_key values must never appear in source code or logs. The authorization_header stored in uhi_requests and uhi_webhooks contains the signed header (which includes the public key ID and signature), not the raw private key.

Turning verification off

Inbound callback signatures are verified in every environment. The only switch is the credentials key uhi.signature_verification: false, for local work against a gateway sandbox that does not sign for local IDs. Environment variables do not change it (the old UHI_SIGNATURE_VERIFICATION and UHI_SIGNATURE_VERIFICATION_ENABLED variables are no longer read). Staging takes it from config/credentials/staging.yml.enc and should leave it out. Production refuses to boot with it set to false.


Using Uhi::AuthService

Generating a header (outbound)

# EUA signing for physical consultation
auth = Uhi::AuthService.new(service_type: :physical, role: :eua)

header = auth.generate_header(
  http_method: "POST",
  endpoint:    "/api/v1/uhi/search",
  body:        request_payload.to_json
)
# => '{"headers":"(created) (expires) digest","keyId":"kulcare.eua|kulcare.euapid|ed25519",...}'

# Use in HTTP request
response = Net::HTTP.post(uri, body, "Authorization" => header, "Content-Type" => "application/json")

HSPA signing (outbound callbacks)

# When we act as HSPA and send on_confirm back to an EUA
auth = Uhi::AuthService.new(service_type: :physical, role: :hspa)

header = auth.generate_header(
  http_method: "POST",
  endpoint:    "/on_confirm",
  body:        on_confirm_payload.to_json
)

Verifying an incoming header

verify_header is a class method. It returns true, or raises Uhi::SignatureVerificationError for a malformed or expired header, a missing field, an unknown key, or a signature that does not match the body.

Uhi::AuthService.verify_header(
  header_json: request.headers["Authorization"],
  body:        request.body.read
  # public_key: "MCow..." to skip the Network Registry lookup by keyId
)
# => true, or raises Uhi::SignatureVerificationError

Every inbound callback controller (Uhi::UhiController) runs this before its action. A request that fails gets 422 with a NACK body: error.code is MISSING_SIGNATURE when there is no header and INVALID_SIGNATURE otherwise. The reason is logged as [UHI] Signature verification failed, and is not sent back to the caller.

Service type mapping

Uhi::AuthService.new(service_type: :physical)  # tele_physical_consultation creds
Uhi::AuthService.new(service_type: :tele)      # same creds as :physical
Uhi::AuthService.new(service_type: :pmjay)     # pmjay creds

Network Registry Lookup

To verify a header from an unknown sender, look up their public key from the UHI Network Registry:

registry = Uhi::NetworkRegistryService.new(service_type: :physical, role: :eua)

subscriber = registry.lookup_subscriber(
  subscriber_id: "nha.eua",
  type:          "EUA",
  domain:        "nic2004:85111"
)
# => { subscriber_id: "nha.eua", public_key: "MCow...", status: "success" }

The lookup itself makes a signed outbound call and is automatically logged to uhi_requests.

Network Registry lookup sequence


Communication Logging

Every UHI API call — outbound or inbound — is stored in the database including the full Authorization header, so that signature failures can be debugged by inspecting header values.

uhi_requests — outbound calls we make

ColumnDescription
transaction_idCorrelates requests with their callbacks
actionsearch, init, confirm, status, cancel, on_confirm_audit, etc.
endpointFull URL called
authorization_headerFull signed header we sent (for debugging)
payloadRequest body (JSONB)
responseResponse body (JSONB)
status_codeHTTP status
statuspending → success or failed

uhi_webhooks — inbound callbacks we receive

ColumnDescription
transaction_idLinks back to originating uhi_request
actionon_search, on_init, on_confirm, etc.
sourcegateway, hspa, or eua
authorization_headerHeader received from sender (for debugging)
payloadFull webhook body (JSONB)
processed_dataExtracted/normalized fields
statuspending → processed or failed

Debugging a signature failure

# Find all requests for a transaction
UhiRequest.where(transaction_id: "txn-abc123").includes(:uhi_webhooks)

# Compare sent header vs received webhook header
request = UhiRequest.find_by(transaction_id: "txn-abc123", action: "search")
puts request.authorization_header   # header we sent

webhook = request.uhi_webhooks.find_by(action: "on_search")
puts webhook.authorization_header   # header HSPA sent back to us

# Find all failed outbound requests
UhiRequest.where(status: "failed").order(created_at: :desc).limit(20)

Testing Header Generation

Use the script at scripts/uhi_header_generator.rb to generate a test header locally:

cd backend

# Generate header for a test body
ruby scripts/uhi_header_generator.rb '{"context":{"domain":"nic2004:85111"}}'

Expected output:

============================================================
UHI Authorization Header Generator
============================================================
Consumer ID: SBX_001751
Subscriber ID: kulcare.eua
...

✅ Generated Authorization Header:
{"headers":"(created) (expires) digest","keyId":"kulcare.eua|kulcare.euapid|ed25519","algorithm":"ed25519","created":"...","expires":"...","signature":"..."}

Paste the output directly as the Authorization header value in Postman.

On this page