Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 232 additions & 22 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
# Formidable eSign

Sign a document without sending the document or its URL to Formidable eSign.

## 1. Hash the document locally
## Setup

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r document.pdf | cut -d' ' -f1)
echo "$DOCUMENT_HASH"
export FESIGN_API_URL="https://api.fesign.formidable.care"
export FESIGN_API_KEY="..."
export FESIGN_CERT_ID="..."
export FESIGN_PIN="..."
```

The result is a 64-character SHA-256 hash. The same document bytes always
produce the same hash; any change produces a different hash.
## Sign a JSON / FHIR document

Hash the file locally, then sign the hash. Use the same file bytes when
verifying.

## 2. Sign the hash
### Hash and sign

```bash
DOCUMENT_HASH=$(openssl dgst -sha256 -r patient.fhir.json | cut -d' ' -f1)

SIGNATURE=$(
jq -n \
--arg hash "$DOCUMENT_HASH" \
Expand All@@ -30,10 +34,7 @@ SIGNATURE=$(
)
```

Only the hash is signed. Do not include the document URL, filename, or document
contents in the request or optional metadata.

## 3. Verify with Formidable eSign
### Verify with Formidable eSign

```bash
jq -n \
Expand All@@ -47,13 +48,9 @@ curl --silent --fail-with-body \
--data-binary @-
```

A valid response returns `"isValid": true` and the certificate, chain, hash,
and signature checks.
A valid response returns `"isValid": true`.

## 4. Verify locally

Decode the returned signature, fetch the Formidable eSign trust anchor, and
verify the original document with OpenSSL:
### Verify locally

```bash
printf '%s' "$SIGNATURE" |
Expand All@@ -70,14 +67,227 @@ openssl cms -verify \
-binary \
-inform DER \
-in signature.p7s \
-content document.pdf \
-content patient.fhir.json \
-CAfile fesign-root-ca.crt \
-purpose any \
-out /dev/null
```

OpenSSL exits successfully only when the document hash, signature, and
certificate chain are valid.
Local verification does not check certificate revocation.

## Sign a PDF

Upload the PDF and save the signed PDF returned by Formidable eSign.

```bash
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.pdf
```

Maximum size: 10 MB. Verify the result in Adobe Acrobat or with
[`pdfsig`](https://manpages.debian.org/pdfsig):

```bash
pdfsig signed-document.pdf
```

## JavaScript (Node.js)

Uses Node.js 20+ built-ins: [`node:crypto`](https://nodejs.org/api/crypto.html),
`fetch`, `FormData`, and `Blob`. No npm package is required.

```javascript
// 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(/\/$/, "");
}
```

```bash
node esign.mjs
pdfsig signed-document.pdf
```

## .NET (C#)

Uses `SHA256`, `HttpClient`, and
[`SignedCms`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.pkcs.signedcms).
Add the PKCS package for local JSON/FHIR verification:

```bash
dotnet add package System.Security.Cryptography.Pkcs
```

```csharp
// 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}");
}
```

```bash
dotnet run
pdfsig signed-document.pdf
```

## More

Expand Down