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.mjsimport{createHash}from"node:crypto";import{readFile,writeFile}from"node:fs/promises";constapiUrl=httpsUrl(required("FESIGN_API_URL"));constapiKey=required("FESIGN_API_KEY");constcertId=required("FESIGN_CERT_ID");constpin=required("FESIGN_PIN");// JSON / FHIR: hash locally, sign only the hash, then verify it.constfhir=awaitreadFile("patient.fhir.json");consthash=createHash("sha256").update(fhir).digest("hex");constsignedHash=awaitpostJson("/documents/signHash",{
hash,
certId,
pin,});constverification=awaitpostJson("/documents/validate",{
hash,signature: signedHash.signature,});if(!verification.isValid)thrownewError("FHIR signature is invalid");awaitwriteFile("patient.fhir.signature",signedHash.signature);// PDF: upload the bytes and save the returned PDF with its embedded signature.constpdf=awaitreadFile("document.pdf");constform=newFormData();form.append("pdf",newBlob([pdf],{type: "application/pdf"}),"document.pdf");form.append("certId",certId);form.append("pin",pin);constpdfResponse=awaitfetch(`${apiUrl}/documents/signPDF`,{method: "POST",headers: {"x-api-key": apiKey},body: form,});if(!pdfResponse.ok)thrownewError(awaitpdfResponse.text());constsignedPdf=awaitpdfResponse.json();awaitwriteFile("signed-document.pdf",Buffer.from(signedPdf.signedPdf,"base64"));asyncfunctionpostJson(path,body){constresponse=awaitfetch(`${apiUrl}${path}`,{method: "POST",headers: {"x-api-key": apiKey,"Content-Type": "application/json",},body: JSON.stringify(body),});if(!response.ok)thrownewError(awaitresponse.text());returnresponse.json();}functionrequired(name){constvalue=process.env[name];if(!value?.trim())thrownewError(`Missing ${name}`);returnvalue;}functionhttpsUrl(value){consturl=newURL(value);if(url.protocol!=="https:")thrownewError("FESIGN_API_URL must use HTTPS");returnurl.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+usingSystem.Net.Http.Headers;usingSystem.Net.Http.Json;usingSystem.Security.Cryptography;usingSystem.Security.Cryptography.Pkcs;usingSystem.Security.Cryptography.X509Certificates;usingSystem.Text.Json;varapiUrl=newUri(Required("FESIGN_API_URL").TrimEnd('/')+"/");if(apiUrl.Scheme!=Uri.UriSchemeHttps)thrownewInvalidOperationException("FESIGN_API_URL must use HTTPS");varapiKey=Required("FESIGN_API_KEY");varcertId=Required("FESIGN_CERT_ID");varpin=Required("FESIGN_PIN");usingvarclient=newHttpClient{BaseAddress=apiUrl};client.DefaultRequestHeaders.Add("x-api-key",apiKey);// JSON / FHIR: hash locally and sign only the hash.varfhir=awaitFile.ReadAllBytesAsync("patient.fhir.json");varhash=Convert.ToHexString(SHA256.HashData(fhir)).ToLowerInvariant();usingvarsignResponse=awaitclient.PostAsJsonAsync("documents/signHash",new{hash,certId,pin});signResponse.EnsureSuccessStatusCode();usingvarsignJson=JsonDocument.Parse(awaitsignResponse.Content.ReadAsStreamAsync());varsignature=signJson.RootElement.GetProperty("signature").GetString()??thrownewCryptographicException("Signature missing");awaitFile.WriteAllTextAsync("patient.fhir.signature",signature);// Verify with Formidable eSign.usingvarverifyResponse=awaitclient.PostAsJsonAsync("documents/validate",new{hash,signature});verifyResponse.EnsureSuccessStatusCode();usingvarverifyJson=JsonDocument.Parse(awaitverifyResponse.Content.ReadAsStreamAsync());if(!verifyJson.RootElement.GetProperty("isValid").GetBoolean())thrownewCryptographicException("FHIR signature is invalid");// Verify the detached CMS signature and its certificate chain locally.usingvarenvelope=JsonDocument.Parse(Convert.FromBase64String(signature));varcmsBytes=Convert.FromBase64String(envelope.RootElement.GetProperty("signature").GetString()??thrownewCryptographicException("CMS signature missing"));varcms=newSignedCms(newContentInfo(fhir),detached:true);cms.Decode(cmsBytes);cms.CheckSignature(verifySignatureOnly:true);varrootPem=awaitclient.GetStringAsync("certificates/root-ca");usingvarroot=X509Certificate2.CreateFromPem(rootPem);varsigner=cms.SignerInfos[0].Certificate??thrownewCryptographicException("Signer certificate missing");usingvarchain=newX509Chain();chain.ChainPolicy.TrustMode=X509ChainTrustMode.CustomRootTrust;chain.ChainPolicy.CustomTrustStore.Add(root);chain.ChainPolicy.ExtraStore.AddRange(cms.Certificates);chain.ChainPolicy.ApplicationPolicy.Add(newOid("1.3.6.1.5.5.7.3.4"));chain.ChainPolicy.RevocationMode=X509RevocationMode.NoCheck;if(!chain.Build(signer))thrownewCryptographicException("Certificate chain is invalid");// PDF: upload the bytes and save the PDF with its embedded signature.varpdf=awaitFile.ReadAllBytesAsync("document.pdf");usingvarpdfContent=newByteArrayContent(pdf);pdfContent.Headers.ContentType=newMediaTypeHeaderValue("application/pdf");usingvarform=newMultipartFormDataContent{{pdfContent,"pdf","document.pdf"},{newStringContent(certId),"certId"},{newStringContent(pin),"pin"},};usingvarpdfResponse=awaitclient.PostAsync("documents/signPDF",form);pdfResponse.EnsureSuccessStatusCode();usingvarpdfJson=JsonDocument.Parse(awaitpdfResponse.Content.ReadAsStreamAsync());varsignedPdf=Convert.FromBase64String(pdfJson.RootElement.GetProperty("signedPdf").GetString()??thrownewCryptographicException("Signed PDF missing"));awaitFile.WriteAllBytesAsync("signed-document.pdf",signedPdf);staticstringRequired(stringname){varvalue=Environment.GetEnvironmentVariable(name);return!string.IsNullOrWhiteSpace(value)?value:thrownewInvalidOperationException($"Missing {name}");}dotnet run
pdfsig signed-document.pdf