Deception Markup Language (DML)
DML is a small, versioned, HMAC-signable schema for declaring deception assets — honeypots, canaries, and tripwires — as portable documents. Instead of scattering trap logic across your codebase, you describe traps once, in one file, and ship that file between systems with confidence that it has not been tampered with in transit or at rest.
A DML document is a versioned set of traps. Each trap pairs a trigger (what an attacker has to do to set it off) with a response (how the trap reacts), plus metadata (severity, MITRE ATT&CK tactic, tags) and alerting intent.
Why versioned + signed deception config matters
Deception only works if attackers can't distinguish traps from real assets — and operators can't afford to deploy traps they can't trust. Two properties make a deception config trustworthy:
- Versioned. A document declares the DML spec version it targets, so consumers can reject documents they don't understand instead of misinterpreting them. Trap IDs are namespaced and must be unique, so two teams can merge configs without silent collisions.
- Signed. Trap definitions are exactly the kind of high-value config an attacker who gains a foothold would love to quietly edit — disabling the tripwire that would catch them, or pointing a "block" response at log-only. DML signs every trap and the document as a whole with HMAC-SHA256, so any edit — to a field, a trap, or the set of traps — is detectable as long as the signing key stays secret.
Trigger types
DML 0.3.0 — WraithMesh sensors
v0.3.0 adds a sensors section for declaring WraithMesh nodes inside the
same signed document as your traps, plus an optional mesh_policy for
TIE corroboration rules.
dml_version: "0.3.0"
mesh_policy:
tie_min_corroboration: 2
forbid_raw_commands: true
forbid_src_ip: true
sensors:
- id: cowrie-east-1
name: Cowrie observer
sensor_class: cowrie
cowrie_log_path: /var/log/cowrie/cowrie.json
aggregator_url: http://127.0.0.1:8787
Export a WraithMesh mesh.json from a signed document:
DML_KEY=... WRAITHMESH_KEY=... dml sign examples/example_mesh.yaml -o signed_mesh.yaml
DML_KEY=... WRAITHMESH_KEY=... dml export-mesh signed_mesh.yaml cowrie-east-1 -o mesh.json
New trigger types: cowrie_session, canary_beacon, equivalence_match.
New response types: mesh_uplink, corroborate_alert.
Sensor classes: cowrie, canary, gateway, fingerprint.
Documents declaring dml_version: "0.2.0" remain valid (traps-only).
Trigger types
The trigger types — the kind of attacker activity that fires a trap:
| Trigger type | Fires when… | Key fields |
|---|---|---|
http_request |
a request hits a planted path/method | path (required), method |
dns_resolution |
a planted hostname is resolved | hostname (required) |
api_key_use |
a planted API key (by prefix) is used | api_key_prefix |
file_access |
a canary file is opened | path |
login_attempt |
a login is attempted against a decoy credential | match_regex |
data_access |
a canary database record is read | record_id |
timing_probe |
response-timing probing is detected | timing_target_ms (required) |
canary_email |
mail arrives at a canary address | email (required) |
jwt_use |
a planted/forged JWT is presented | match_regex |
cowrie_session |
a Cowrie session matches a known equivalence key | equivalence_key (required) |
canary_beacon |
a supply-chain canary token fires | package_name (required) |
equivalence_match |
any sensor sees a campaign equivalence key | equivalence_key (required) |
Response types
The eight response types — how a trap reacts once triggered:
| Response type | Effect | Key fields |
|---|---|---|
fake_data |
serve fabricated data to the attacker | fake_data_template, http_status |
redirect_sandbox |
divert the session into a sandbox/decoy environment | redirect_url, sandbox_reason |
delay_response |
tarpit — stall the response | delay_ms (required) |
mirror_engage |
engage the attacker (e.g. LLM-driven mirroring) | llm_prompt_override, llm_model |
block_ip |
block the source | — |
log_only |
record the event, no visible reaction | — |
alert_only |
fire an alert, no visible reaction | — |
honeypot_auth |
accept the credential into a honeypot auth flow | http_status |
mesh_uplink |
emit a signed observation to the TIE aggregator | aggregator_url (required) |
corroborate_alert |
request corroboration / high-priority TIE alert | aggregator_url |
Severity levels: critical, high, medium, low, info.
MITRE tactics: the standard ATT&CK enterprise tactic set (e.g.
credential_access, discovery, exfiltration).
Install
pip install . # from a checkout
# runtime dependency: PyYAML>=6
Quickstart
from dml import DMLValidator, DMLSigner, load, dumps
doc = load("examples/example_traps.yaml")
# 1. Validate against the spec.
errors = DMLValidator().validate_document(doc)
assert not errors, errors
# 2. Sign (the key is supplied by you, never hardcoded in the library).
signer = DMLSigner.from_env("DML_KEY") # or DMLSigner("my-secret")
signed = signer.sign_document(doc)
# 3. Verify — detects any tampering with traps or the document.
ok, errors = signer.verify_document(signed)
assert ok
print(dumps(signed, fmt="yaml"))
Build a document from your own records (no ORM/framework coupling):
from dml import DMLTrap, DMLTrigger, build_document
traps = [
DMLTrap(id="secret-path", name="Hidden endpoint",
trigger=DMLTrigger(type="http_request", path="/internal/.env")),
]
doc = build_document(traps, platform="my-app", namespace="prod")
Run the full round-trip example:
DML_KEY=test-key-123 python examples/quickstart.py
CLI
The signing key is read from an environment variable (default DML_KEY) — it
is never passed as a command-line literal, so it can't leak into shell
history or process listings.
python -m dml validate examples/example_traps.yaml
python -m dml sign examples/example_traps.yaml --key-env DML_KEY -o signed.yaml
python -m dml verify signed.yaml --key-env DML_KEY
python -m dml schema
Example output:
$ python -m dml validate examples/example_traps.yaml
OK: valid DML document (5 traps)
$ DML_KEY=test-key-123 python -m dml sign examples/example_traps.yaml --key-env DML_KEY -o signed.yaml
OK: signed document written to signed.yaml
$ DML_KEY=test-key-123 python -m dml verify signed.yaml --key-env DML_KEY
OK: document signature valid
$ DML_KEY=wrong python -m dml verify signed.yaml --key-env DML_KEY
FAILED: verification failed:
- Document signature mismatch — document may be tampered
The signing model
DML signs at two levels with HMAC-SHA256:
- Per-trap. Each trap carries a
signaturefield — an HMAC over the trap's contents with thesignaturefield itself excluded. This lets a consumer attribute tampering to a specific trap. - Whole-document. The document carries a
document_signature— an HMAC over the document identity fields (dml_version,platform,namespace) plus every trap with its per-trap signature stripped. This binds the set of traps together: adding, removing, or editing any trap (even with a forged per-trap signature) invalidates the document signature.
Canonicalization. Before signing, the relevant object is serialized deterministically as compact JSON with sorted keys:
canonical = json.dumps(obj, sort_keys=True, separators=(",", ":"))
signature = hmac.new(key, canonical.encode(), hashlib.sha256).hexdigest()[:32]
Verification recomputes the canonical form and compares signatures in constant time.
Key handling. The HMAC key is supplied by the caller — via the
DMLSigner(key) constructor or DMLSigner.from_env("DML_KEY"). The library
contains no default, fallback, or hardcoded key; a missing key raises rather
than signing with a guessable value. Keep the key secret: anyone who has it
can forge valid signatures.
Limitations
- HMAC-SHA256, not asymmetric. Signatures use HMAC-SHA256 (symmetric key), not public-key cryptography. Anyone who holds the signing key can forge signatures. Key distribution and rotation are outside DML's scope — treat the key as a shared secret scoped to a deployment environment.
- No encryption. DML signs documents but does not encrypt them. Trap definitions are visible to anyone who can read the file. If your trap definitions themselves are sensitive (e.g., they reveal internal network paths), sign and then encrypt at the transport or storage layer.
- Canonicalization is JSON-only. The deterministic serialisation used for signing is compact JSON with sorted keys. Documents stored in YAML are converted to JSON before signing, so the on-disk format and the signed representation differ. This is transparent to the library but matters if you verify signatures outside Python.
- No automatic key rotation. DML has no built-in key lifecycle. Rotating the key requires re-signing every document — plan your key rotation process separately.
- The spec is not a runtime. DML describes and signs traps; it does not deploy, manage, or enforce them. Integration with sensors, aggregators, and response systems is the responsibility of the platform consuming the DML document (e.g., WraithWall).
License
MIT — see LICENSE. Copyright (c) 2026 Niffyhunt.
Part of the WraithWall project — https://wraithwall.online · by Niffyhunt.
Metadata
Release files for dml-spec 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dml_spec-1.1.0.tar.gz | 22.9 kB | Without provenance | Details for dml_spec-1.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dml_spec-1.1.0-py3-none-any.whl | Python 3 | none | any | Details for dml_spec-1.1.0-py3-none-any.whl |
Total release size: 43.1 kB
Release files / dml_spec-1.1.0.tar.gz
| Download URL | dml_spec-1.1.0.tar.gz |
|---|---|
| Size | 22.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27f2329feed91117d75c46b485b506294a27f012bf0acfdbc9fa8cbb785b4bcf
|
|
BLAKE2b-256 checksum How to use checksums |
4720b3f98eebda0de549eb6fc4cb208f6bcb38a9ff2e22652d09e43bc50dc069
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / dml_spec-1.1.0-py3-none-any.whl
| Download URL | dml_spec-1.1.0-py3-none-any.whl |
|---|---|
| Size | 20.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
482eea2c134248a400203bb5ddd65e4e2231d2f551121279bced8157c0658043
|
|
BLAKE2b-256 checksum How to use checksums |
8aa486410105cf9096a2aa8b07d6bed622d15062160f4e24269f266b4d96c785
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for dml-spec 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.