export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."Hash the file locally, then sign the hash. Use the same file bytes when verifying.
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)
SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
--arg certId "$FESIGN_CERT_ID" \
--arg pin "$FESIGN_PIN" \
'{hash: $hash, certId: $certId, pin: $pin}' |
curl --silent --fail-with-body \
--request POST "$FESIGN_API_URL/documents/signHash" \
--header "x-api-key: $FESIGN_API_KEY" \
--header "Content-Type: application/json" \
--data-binary @- |
jq -r '.signature'
)jq -n \
--arg hash "$DOCUMENT_HASH" \
--arg signature "$SIGNATURE" \
'{hash: $hash, signature: $signature}' |
curl --silent --fail-with-body \
--request POST "$FESIGN_API_URL/documents/validate" \
--header "x-api-key: $FESIGN_API_KEY" \
--header "Content-Type: application/json" \
--data-binary @-A valid response returns "isValid": true.
printf '%s' "$SIGNATURE" |
base64 --decode |
jq -r '.signature' |
base64 --decode > signature.p7s
curl --silent --fail-with-body \
"$FESIGN_API_URL/certificates/root-ca" \
--header "x-api-key: $FESIGN_API_KEY" \
--output fesign-root-ca.crt
openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-out /dev/nullLocal verification does not check certificate revocation.
Upload the PDF and save the signed PDF returned by Formidable eSign.
curl --silent --fail-with-body \
--request POST "$FESIGN_API_URL/documents/signPDF" \
--header "x-api-key: $FESIGN_API_KEY" \
--form "pdf=@document.pdf;type=application/pdf" \
--form "certId=$FESIGN_CERT_ID" \
--form "pin=$FESIGN_PIN" |
jq -r '.signedPdf' |
base64 --decode > signed-document.pdfMaximum size: 10 MB. Verify the result in Adobe Acrobat or with
pdfsig:
pdfsig signed-document.pdfUses Node.js 20+ built-ins: node:crypto,
fetch, FormData, and Blob. No npm package is required.
// esign.mjs
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
const apiUrl = httpsUrl(required("FESIGN_API_URL"));
const apiKey = required("FESIGN_API_KEY");
const certId = required("FESIGN_CERT_ID");
const pin = required("FESIGN_PIN");
// JSON / FHIR: hash locally, sign only the hash, then verify it.
const fhir = await readFile("patient.fhir.json");
const hash = createHash("sha256").update(fhir).digest("hex");
const signedHash = await postJson("/documents/signHash", {
hash,
certId,
pin,
});
const verification = await postJson("/documents/validate", {
hash,
signature: signedHash.signature,
});
if (!verification.isValid) throw new Error("FHIR signature is invalid");
await writeFile("patient.fhir.signature", signedHash.signature);
// PDF: upload the bytes and save the returned PDF with its embedded signature.
const pdf = await readFile("document.pdf");
const form = new FormData();
form.append("pdf", new Blob([pdf], { type: "application/pdf" }), "document.pdf");
form.append("certId", certId);
form.append("pin", pin);
const pdfResponse = await fetch(`${apiUrl}/documents/signPDF`, {
method: "POST",
headers: { "x-api-key": apiKey },
body: form,
});
if (!pdfResponse.ok) throw new Error(await pdfResponse.text());
const signedPdf = await pdfResponse.json();
await writeFile("signed-document.pdf", Buffer.from(signedPdf.signedPdf, "base64"));
async function postJson(path, body) {
const response = await fetch(`${apiUrl}${path}`, {
method: "POST",
headers: {
"x-api-key": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(await response.text());
return response.json();
}
function required(name) {
const value = process.env[name];
if (!value?.trim()) throw new Error(`Missing ${name}`);
return value;
}
function httpsUrl(value) {
const url = new URL(value);
if (url.protocol !== "https:") throw new Error("FESIGN_API_URL must use HTTPS");
return url.href.replace(/\/$/, "");
}node esign.mjs
pdfsig signed-document.pdfUses SHA256, HttpClient, and
SignedCms.
Add the PKCS package for local JSON/FHIR verification:
dotnet add package System.Security.Cryptography.Pkcs// Program.cs — .NET 8+
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
var apiUrl = new Uri(Required("FESIGN_API_URL").TrimEnd('/') + "/");
if (apiUrl.Scheme != Uri.UriSchemeHttps)
throw new InvalidOperationException("FESIGN_API_URL must use HTTPS");
var apiKey = Required("FESIGN_API_KEY");
var certId = Required("FESIGN_CERT_ID");
var pin = Required("FESIGN_PIN");
using var client = new HttpClient { BaseAddress = apiUrl };
client.DefaultRequestHeaders.Add("x-api-key", apiKey);
// JSON / FHIR: hash locally and sign only the hash.
var fhir = await File.ReadAllBytesAsync("patient.fhir.json");
var hash = Convert.ToHexString(SHA256.HashData(fhir)).ToLowerInvariant();
using var signResponse = await client.PostAsJsonAsync(
"documents/signHash",
new { hash, certId, pin });
signResponse.EnsureSuccessStatusCode();
using var signJson = JsonDocument.Parse(await signResponse.Content.ReadAsStreamAsync());
var signature = signJson.RootElement.GetProperty("signature").GetString()
?? throw new CryptographicException("Signature missing");
await File.WriteAllTextAsync("patient.fhir.signature", signature);
// Verify with Formidable eSign.
using var verifyResponse = await client.PostAsJsonAsync(
"documents/validate",
new { hash, signature });
verifyResponse.EnsureSuccessStatusCode();
using var verifyJson = JsonDocument.Parse(await verifyResponse.Content.ReadAsStreamAsync());
if (!verifyJson.RootElement.GetProperty("isValid").GetBoolean())
throw new CryptographicException("FHIR signature is invalid");
// Verify the detached CMS signature and its certificate chain locally.
using var envelope = JsonDocument.Parse(Convert.FromBase64String(signature));
var cmsBytes = Convert.FromBase64String(
envelope.RootElement.GetProperty("signature").GetString()
?? throw new CryptographicException("CMS signature missing"));
var cms = new SignedCms(new ContentInfo(fhir), detached: true);
cms.Decode(cmsBytes);
cms.CheckSignature(verifySignatureOnly: true);
var rootPem = await client.GetStringAsync("certificates/root-ca");
using var root = X509Certificate2.CreateFromPem(rootPem);
var signer = cms.SignerInfos[0].Certificate
?? throw new CryptographicException("Signer certificate missing");
using var chain = new X509Chain();
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(root);
chain.ChainPolicy.ExtraStore.AddRange(cms.Certificates);
chain.ChainPolicy.ApplicationPolicy.Add(new Oid("1.3.6.1.5.5.7.3.4"));
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
if (!chain.Build(signer))
throw new CryptographicException("Certificate chain is invalid");
// PDF: upload the bytes and save the PDF with its embedded signature.
var pdf = await File.ReadAllBytesAsync("document.pdf");
using var pdfContent = new ByteArrayContent(pdf);
pdfContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
using var form = new MultipartFormDataContent
{
{ pdfContent, "pdf", "document.pdf" },
{ new StringContent(certId), "certId" },
{ new StringContent(pin), "pin" },
};
using var pdfResponse = await client.PostAsync("documents/signPDF", form);
pdfResponse.EnsureSuccessStatusCode();
using var pdfJson = JsonDocument.Parse(await pdfResponse.Content.ReadAsStreamAsync());
var signedPdf = Convert.FromBase64String(
pdfJson.RootElement.GetProperty("signedPdf").GetString()
?? throw new CryptographicException("Signed PDF missing"));
await File.WriteAllBytesAsync("signed-document.pdf", signedPdf);
static string Required(string name)
{
var value = Environment.GetEnvironmentVariable(name);
return !string.IsNullOrWhiteSpace(value)
? value
: throw new InvalidOperationException($"Missing {name}");
}dotnet run
pdfsig signed-document.pdf