This document describes the security model of @majikah/majik-signature, what it does and does not guarantee, and how to report a vulnerability. It is written to match the actual implementation — if something here looks inconsistent with the code, please report it.
- Security Policy
This library is currently pre-1.0 (0.0.x). There is no long-term support branch — only the latest published version on npm receives security fixes.
| Version | Supported |
|---|---|
Latest (0.0.x) |
✅ Yes |
Older 0.0.x |
❌ No — please upgrade |
Once the library reaches 1.0.0, this table will be updated with a defined support window. Until then, treat every release as the only supported release, and pin your dependency deliberately rather than assuming older minor versions remain patched.
Please do not open a public GitHub issue for security vulnerabilities.
Report privately via:
- GitHub Private Vulnerability Reporting — github.com/Majikah/majik-signature/security
- Email — business@majikah.solutions, with
SECURITYin the subject line
Please include:
- A description of the issue and its potential impact
- Steps to reproduce, or a minimal proof-of-concept
- The affected version(s), and whether the issue is in this package or a dependency
- Whether the issue is in the core cryptographic path (
sign/verify) or the file-embedding layer
This is a small, independently maintained project — response times are best-effort, not a contractual SLA. In good faith, we aim to:
| Stage | Target |
|---|---|
| Initial acknowledgment | Within 5 business days |
| Preliminary assessment | Within 10 business days |
| Fix or mitigation, for confirmed issues | Best-effort, prioritized by severity |
We will keep you informed as the report is triaged and credit reporters (if desired) once a fix ships, in line with the Disclosure Policy below.
Majik Signature provides integrity and authenticity, not confidentiality. It answers the question "was this exact content produced by this key, unmodified, at this time?" — it does not encrypt content, hide it, or control who can read it.
Every signature is produced by two independent algorithms over the same canonical payload, and verification requires both to pass:
- Ed25519 — classical, 128-bit security, via
@stablelib/ed25519 - ML-DSA-87 (FIPS-204) — post-quantum, NIST Category 5, via
@noble/post-quantum
This hybrid construction means a break in either algorithm alone — a classical cryptanalytic advance against Ed25519, or a future quantum attack against ML-DSA-87 — is not sufficient on its own to forge a valid signature.
Three independent domain-separation prefixes prevent a signature (or hash) produced for one purpose from being replayed as another:
| Purpose | Prefix |
|---|---|
| Content signing payload | majik-signature-v1: |
| Seal hash payload | majik-seal-v1: |
| Trusted timestamp payload | majikah-tsa-v-1: |
The signed payload binds: envelope version, signer fingerprint, ISO 8601 timestamp, advisory content type, SHA-256 content hash, and — only when establishing an allowlist — the allowlist's own hash. Content itself is never embedded in the envelope; only its hash is signed, so signature cost is independent of content size.
- Allowlists are cryptographically committed: the issuer's
allowlistHashis inside their own signed payload, so modifying the allowlist after the fact invalidates the issuer's signature. - Sealing computes a SHA3-512 hash over all current signatories plus a seal timestamp. This is a hash commitment, not a signature —
verifySeal()checks hash equality, and does not itself perform any Ed25519/ML-DSA-87 verification. Always callverifyFile()separately to validate the underlying signer signatures.
- The core signing/verification path (
sign,verify,verifyWithKey) inmajik-signature.ts - The canonical payload construction (
payload.ts) and any weakening of domain separation or hash binding - The file embedding/extraction/strip round-trip (
majik-embed.tsand all format handlers) - Multi-signature, allowlist, and seal logic (
multi-sig.ts) - Serialization/deserialization (
toJSON/fromJSON/serialize/deserialize) - Any vulnerability that allows a signature to verify as
valid: trueagainst content or a signer it should not
MajikKey— key generation, storage, and mnemonic handling live in the separate@majikah/majik-keypeer dependency. Report key-management issues there.- Image stamping module (
stampImage/verifyStamp/inspectStamp/isStamped) — explicitly marked@experimentalin source and not API-stable. Issues are welcome but are not treated as security-critical in the same sense as the core signing path. - Applications built on this library — misuse (e.g., trusting an unverified
signerId, skippingkey.lock(), treatingcontentTypeas enforced) is an application-level concern; see Secure Usage Guidelines. - Denial-of-service via large inputs — the library hashes and signs whatever it's given; enforcing size limits on untrusted input is the caller's responsibility.
Documented honestly, so you can design around them:
- No confidentiality. Signing does not encrypt or hide content. A signed file's contents (and the signature envelope itself) are fully readable by anyone with the file. If you need confidentiality, encrypt separately before or after signing.
- No revocation. There is no mechanism to revoke a previously issued signature. If a signer's key is later compromised, signatures made before compromise remain cryptographically valid forever — key compromise response is an application/key-management concern, not something
verify()can detect. - No freshness/expiry enforcement. The signed payload includes a timestamp, but
verify()does not check it against a validity window or current time. An old-but-legitimately-signed file will always verify as valid. If your use case needs "signed within the last N days," enforce that check onresult.timestampyourself. contentTypeis advisory only. It is included in the signed payload (so it can't be silently swapped after signing), but it is never used to restrict what verify() will accept. Don't rely on it as an access-control mechanism.- Signatures can be stripped, not forged. Anyone can remove an embedded signature from a file (
strip()is a public, deterministic operation) — that just produces an unsigned file. This is expected and analogous to removing a physical signature page: it does not let an attacker produce a valid signature over modified content, or make a stripped file appear signed. - Array-of-signatures does not imply array-of-valid-signatures.
extractFrom()and the raw envelope can technically contain malformed or invalid signature entries (e.g., a hand-edited file with a garbageedSignature). Always checkvalidon eachVerificationResultreturned byverifyFile()— never assume presence in the envelope implies cryptographic validity. - Allowlist enforcement is a library-level, not container-level, guarantee.
signAndEmbed()rejects non-listed signers before doing any cryptographic work when you go through the library's own APIs. Nothing stops a party with direct file access from hand-crafting an envelope with an unauthorized signer's ID and a bogus signature — it will simply fail verification, so this is not a forgery risk, but downstream code that inspectsgetSignatories()without also callingverifyFile()could be misled about who has "signed." - No built-in transport security. Trusted Timestamp requests/responses and any network calls your application layers on top of this library are your responsibility to secure (TLS, server authentication, etc.) — the library only handles the cryptographic payload.
- Pre-1.0 API stability. Until
1.0.0, minor versions may include breaking changes, including to security-relevant defaults. Read release notes before upgrading. - ML-DSA-87 is a relatively young standard (FIPS-204 finalized in 2024). The hybrid design exists specifically so that a weakness discovered in ML-DSA-87 alone does not compromise signatures already issued — Ed25519 coverage remains intact — but this is not a substitute for tracking NIST post-quantum guidance as the field matures.
Majik Signature has a deliberately small dependency surface and no native bindings:
| Dependency | Role |
|---|---|
@noble/post-quantum |
ML-DSA-87 (FIPS-204) implementation |
@stablelib/ed25519 |
Ed25519 implementation |
@stablelib/sha256 |
SHA-256 hashing |
fflate |
ZIP handling for Office/ODF format embedding |
pdf-lib |
PDF-related utilities |
@majikah/majik-key (peer) |
Key generation, unlocking, and secret material handling |
Cryptographic primitives are delegated entirely to the underlying libraries above — this project does not implement its own Ed25519, ML-DSA-87, or SHA-256/SHA3-512 primitives. Vulnerabilities in the cryptographic math itself should generally be reported upstream to those projects as well as to us, since we would need to update the dependency to ship a fix.
We recommend running npm audit (or your package manager's equivalent) against your own lockfile regularly, since transitive dependency advisories can appear between our releases.
The full list lives in the README's Security Considerations. The security-critical subset:
- ✅ Always check
result.validand cross-checkresult.signerIdagainst a fingerprint you actually trust — a cryptographically valid signature from an unexpected signer is not the same as a trusted one. - ✅ Call
key.lock()immediately after signing to purge secret key material from memory. - ✅ Treat every entry in
verifyFile()'s result array independently — a file with multiple signatures can have some valid and some invalid entries. - ✅ If you rely on sealing, call both
verifySeal()(structural) andverifyFile()(cryptographic) — they check different things. - ❌ Never treat
contentType, file extension, or MIME type as a trust boundary. - ❌ Never assume a signature's presence means it verified — always call
verify()/verifyFile(); don't infer trust fromisSigned()orextractFrom()alone.
| Property | Value |
|---|---|
| Classical signature algorithm | Ed25519 |
| Post-quantum signature algorithm | ML-DSA-87 (NIST FIPS-204, Category 5) |
| Content hash | SHA-256 |
| Allowlist hash | SHA-256 |
| Seal hash | SHA3-512 |
| Signature envelope version | 1 (MAJIK_SIGNATURE_VERSION) |
| Envelope wrapper version | 1 (MAJIK_ENVELOPE_VERSION) |
| Timestamp payload version | 1 (MAJIK_TIMESTAMP_VERSION) |
Version fields are embedded in every envelope specifically to support future cryptographic migration (e.g., a future MAJIK_SIGNATURE_VERSION = 2 using different or additional algorithms) without breaking verification of signatures issued under the current scheme.
We follow a coordinated disclosure approach:
- You report privately, per Reporting a Vulnerability.
- We confirm, assess severity, and work on a fix without public disclosure.
- Once a fix is published to npm, we credit the reporter (if they wish to be credited) and publish details of the issue.
- If a report goes unacknowledged for an extended period despite good-faith reporting attempts, we consider public disclosure by the reporter reasonable — we'd rather you responsibly disclose than sit on a real issue indefinitely because we were slow.
We do not currently operate a paid bug bounty program.
- Security reports: github.com/Majikah/majik-signature/security or business@majikah.solutions (subject:
SECURITY) - General/non-security issues: GitHub Issues
- Maintainer: Josef Elijah Fabian