⚠
Testing only — do not use in production documents.
The Meerkat e-Seal is not a qualified or accredited trust service. Signatures issued here carry no legal
weight and must not be embedded in documents intended for production use. The e-Seal signing certificate
is issued under a private, untrusted CA hierarchy that is not recognized by any public root store.
e-Seal Identity
| Endpoint URL |
https://thameur.org/eseal |
| Signing Certificate |
/C=TN/O=Meerkat MPCA by Thameur Belghith/CN=e-Seal Signer/organizationIdentifier=Bou Taba3
|
| Valid From |
2026-06-05 |
| Valid Until |
2029-06-04 |
| Policy OID |
2.16.788.1.99.1.60 |
| Signer Validity |
1095 days |
| Key Usage |
digitalSignature, nonRepudiation (critical) |
| Signing Key |
ECDSA P-256 |
| Hash Algorithms |
SHA-256 · SHA-384 · SHA-512 |
| e-Seal CA Certificate |
http://thameur.org/directory/mpca/mpca-eseal.crt
|
| Chain (CA + Root) |
http://thameur.org/directory/mpca/mpca-eseal-chain.pem
|
| Standard |
eIDAS, ETSI EN 319 412-3, ETSI EN 319 122-1 (CAdES) |
API Endpoint
| Method | URL | Description |
| POST |
https://thameur.org/eseal |
Submit a JSON request with a hash digest. Returns a CMS SignedData (CAdES) or XML ds:Signature (XAdES) based on the format parameter. Primary endpoint. |
| GET |
https://thameur.org/eseal |
Redirects to this documentation page. |
Request
| Header / Body field | Presence | Notes |
| Content-Type |
required |
Must be application/json |
hash |
required |
Hex or base64-encoded digest of the document to seal. The algorithm is inferred from the byte length (32 → SHA-256, 48 → SHA-384, 64 → SHA-512). Spaces, colons, and dashes are stripped automatically. |
alg |
optional |
Hint for the hash algorithm (sha256, sha384, sha512). Ignored — algorithm is always inferred from hash length. |
format |
optional |
cms (default) for CAdES/CMS SignedData output, or xades for XAdES XML output. |
ts |
optional |
Boolean. true (default) to embed an RFC 3161 SignatureTimeStamp (T level); false for baseline level only (B level). If the TSA is unavailable, the timestamp is silently omitted regardless of this flag. |
Request Body Examples
{
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
{
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"format": "xades",
"ts": true
}
{
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"format": "cms",
"ts": false
}
Success Response — CAdES (HTTP 200)
| Header / Body | Value |
| Content-Type | application/cms |
| X-Eseal-Format | cms |
| X-Eseal-Level | CAdES-T if timestamped; CAdES-B if the TSA was unavailable or ts was false. |
| Body |
DER-encoded CMS SignedData (RFC 5652). The signed content (hash bytes) is embedded (eContent — non-detached). When X-Eseal-Level: CAdES-T, an id-aa-signatureTimeStampToken unsigned attribute (OID 1.2.840.113549.1.9.16.2.14) is included per ETSI EN 319 122-1 §5.3.3. |
Success Response — XAdES (HTTP 200)
| Header / Body | Value |
| Content-Type | application/xml; charset=utf-8 |
| Content-Disposition | attachment; filename="eseal.xades" |
| X-Eseal-Format | xades |
| X-Eseal-Level | XAdES-B-T if timestamped; XAdES-B-B if ts was false or the TSA was unavailable. |
| Body |
UTF-8 XML document containing a detached ds:Signature per W3C XMLDSig + ETSI EN 319 132-2. The document digest is referenced via a ds:Reference (not embedded). When X-Eseal-Level: XAdES-B-T, a xades:SignatureTimeStamp unsigned attribute is present in xades:UnsignedSignatureProperties per ETSI EN 319 132-1 §5.3.4. |
Error Response (JSON)
{ "error": "human-readable error message" }
Integration Guide
If you just want to e-seal a file without writing any code,
use 🔏 Meerkat e-Seal Signer —
it handles the hash computation and API call for you and lets you download the token directly.
The steps below are for integrating the e-Seal endpoint into your own tooling.
Step 1 — Compute the hash of your document
sha256sum myfile.pdf | awk '{print $1}'
sha384sum myfile.pdf | awk '{print $1}'
sha512sum myfile.pdf | awk '{print $1}'
openssl dgst -sha256 myfile.pdf | awk '{print $2}'
Step 2 — Send the hash to the e-Seal endpoint
HASH=$(sha256sum myfile.pdf | awk '{print $1}')
curl -s -X POST https://thameur.org/eseal \
-H 'Content-Type: application/json' \
-d "{\"hash\": \"$HASH\"}" \
-o signature.cms
openssl asn1parse -inform DER -in signature.cms | head -40
Step 3 — Verify the e-Seal signature
curl -s -o eseal_chain.pem http://thameur.org/directory/mpca/mpca-eseal-chain.pem
openssl cms -verify -inform DER -in signature.cms \
-CAfile eseal_chain.pem -purpose any -noout
openssl cms -verify -inform DER -in signature.cms \
-CAfile eseal_chain.pem -purpose any | xxd
One-liner (hash + e-seal in one step)
curl -s -X POST https://thameur.org/eseal \
-H 'Content-Type: application/json' \
-d "{\"hash\": \"$(openssl dgst -sha256 myfile.pdf | awk '{print $2}')\"}" \
-o signature.cms \
&& openssl asn1parse -inform DER -in signature.cms | head -30
Python (using the requests library)
import hashlib, requests
with open('myfile.pdf', 'rb') as f:
digest = hashlib.sha256(f.read()).hexdigest()
resp = requests.post(
'https://thameur.org/eseal',
json={'hash': digest},
)
resp.raise_for_status()
with open('signature.cms', 'wb') as f:
f.write(resp.content)
JavaScript (Node.js)
const crypto = require('crypto');
const fs = require('fs');
const hash = crypto.createHash('sha256')
.update(fs.readFileSync('myfile.pdf'))
.digest('hex');
const response = await fetch('https://thameur.org/eseal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hash }),
});
fs.writeFileSync('signature.cms', Buffer.from(await response.arrayBuffer()));
CMS SignedData Structure (RFC 5652)
| Field | Description |
| contentType | id-signedData (1.2.840.113549.1.7.2) |
| version | v1 (1) |
| digestAlgorithms | Hash algorithm inferred from the input (SHA-256, SHA-384, or SHA-512). |
| encapContentInfo.eContentType | id-data (1.2.840.113549.1.7.1) |
| encapContentInfo.eContent | The raw hash bytes submitted in the request (embedded, non-detached). |
| certificates | e-Seal signing certificate + e-Seal CA + Root CA (full chain). |
| signerInfo.digestAlgorithm | Same as digestAlgorithms. |
| signerInfo.signatureAlgorithm | ECDSA with the matching SHA algorithm (e.g. ecdsa-with-SHA256). |
| signerInfo.signature | DER-encoded ECDSA signature over the signed attributes (which include the message digest of the content). |
| signerInfo.unsignedAttrs[0] | id-aa-signatureTimeStampToken (OID 1.2.840.113549.1.9.16.2.14) — RFC 3161 TimeStampToken covering SHA-256(signatureValue). Sourced from the Meerkat TSA at signing time. This is the CAdES-T extension. |
The signed content is the hash bytes you supplied, not your original document.
This means the CMS token proves that the e-Seal signing key operated on those specific bytes —
to tie it back to your document, you must independently verify that the hash bytes match
the expected digest of your document.
XAdES Signature Structure (ETSI EN 319 132-2)
When format=xades, the response is an XML document containing a detached
ds:Signature element with XAdES qualifying properties.
The signature covers the document's hash digest — the original document is not embedded.
| Element | Description |
| ds:Signature | Root W3C XMLDSig signature element. Carries Id for internal cross-references. |
| ds:SignedInfo / ds:CanonicalizationMethod | http://www.w3.org/TR/2001/REC-xml-c14n-20010315 (inclusive C14N). |
| ds:SignedInfo / ds:SignatureMethod | ECDSA with the hash algorithm matching the input length (e.g. ecdsa-sha256). |
| ds:Reference[@Id="Ref-Content"] | References the signed document by digest only (detached, URI=""). ds:DigestValue is the base64-encoded hash bytes from the request. |
| ds:Reference[@Id="Ref-SignedProperties"] | References the xades:SignedProperties element after C14N. The DigestValue covers the canonicalized SignedProperties XML. |
| ds:SignatureValue | Raw ECDSA signature (r||s concatenation, 64 bytes for P-256) over the canonicalized ds:SignedInfo, base64-encoded. |
| ds:KeyInfo / ds:X509Data | Full DER of the e-Seal signing certificate (base64), embedded for offline verification. |
| xades:SignedProperties | Contains xades:SigningTime and xades:SigningCertificateV2 (SHA-256 digest of the signing certificate for binding). |
| xades:UnsignedSignatureProperties / xades:SignatureTimeStamp | (B-T only) Contains xades:EncapsulatedTimeStamp: base64-encoded DER of an RFC 3161 TimeStampToken covering SHA-256 of the canonicalized ds:SignatureValue element. Per ETSI EN 319 132-1 §5.3.4. |
Verify with xmlsec1
curl -s -o eseal_chain.pem http://thameur.org/directory/mpca/mpca-eseal-chain.pem
curl -s -X POST https://thameur.org/eseal \
-H 'Content-Type: application/json' \
-d '{"hash":"'"$(openssl dgst -sha256 myfile.pdf | awk '{print $2}')"'","format":"xades"}' \
-o eseal.xades
xmlsec1 --verify --trusted-pem eseal_chain.pem eseal.xades
xmllint --format eseal.xades
XAdES vs CAdES — when to choose which
| Criterion | CAdES (CMS) | XAdES (XML) |
| Output format | Binary DER (.cms) | UTF-8 XML (.xades) |
| Content embedding | Hash bytes embedded in eContent | Detached — only digest referenced |
| Verification tooling | openssl cms -verify | xmlsec1 --verify |
| Common use cases | Binary document signing, email, S/MIME | XML documents, EU eIDAS, web services |
| Human-readable | No (ASN.1/DER) | Yes (XML) |
Error Responses
Errors return JSON {"error": "..."} with the matching HTTP status code.
| HTTP | Cause |
| 400 | Missing or unparseable hash field, or hash decodes to wrong byte length (not 32, 48, or 64 bytes). |
| 405 | Wrong HTTP method (only GET and POST are accepted). |
| 415 | Wrong Content-Type — must be application/json. |
| 500 | OpenSSL cms -sign failed — internal error. The error message contains the OpenSSL stderr output. |
| 503 | e-Seal not initialized — the signing key or certificate is missing. Run scripts/mpca_init.sh. |
Technical Notes
- CAdES format: The CMS output is a CAdES-T (by default) or CAdES-B (when
ts=false) token per ETSI EN 319 122-1. The RFC 3161 signature timestamp (id-aa-signatureTimeStampToken, OID 1.2.840.113549.1.9.16.2.14) is embedded as an unsigned attribute in the SignerInfo, covering the SHA-256 hash of the ECDSA signature value.
- XAdES format: The XML output follows ETSI EN 319 132-2 (XAdES baseline profile). At B-B level it contains only signed qualifying properties. At B-T level it adds a
xades:SignatureTimeStamp unsigned attribute per ETSI EN 319 132-1 §5.3.4. The signature is detached — only the document's digest is referenced, not the document itself. The ECDSA signature value is raw r||s (not DER), base64-encoded, as required by the W3C XMLDSig specification.
- Standard basis: The e-Seal signing certificate conforms to ETSI EN 319 412-3 (Certificate Profiles for Legal Persons). Key Usage is
digitalSignature + nonRepudiation (critical). The subject includes organizationIdentifier in the ETSI-defined format.
- eIDAS scope: Under eIDAS Regulation (EU) 910/2014, an e-Seal is the legal-person equivalent of an e-Signature. This testing service mimics the structure but is not qualified and has no legal standing.
- CMS format: The response is a non-detached CMS SignedData (the hash bytes are embedded inside the token). This simplifies verification — you do not need the original hash separately to verify the signature, but you do need it to confirm the token covers your document.
- Hash-only input: The endpoint accepts only a pre-computed hash, not the document itself. This keeps your document data off the server and matches the architecture of the RFC 3161 TSA endpoint.
- Policy OID
2.16.788.1.99.1.60: A private OID registered under the Meerkat test PKI namespace (2.16.788.1.99). It has no meaning outside of this testing environment.
- Signing key: ECDSA P-256. The signing certificate is issued by the Meerkat MPCA e-Seal CA (P-384), which chains to the Meerkat MPCA Root CA. None of these CAs are trusted by any public root store.
- CORS: The endpoint responds with
Access-Control-Allow-Origin: * to allow browser-based testing tools to call it directly.