zeropx-layercomp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ dist_bin/
9
+ build_pyi/
10
+ landing/
11
+ uv.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dr. Vander Geraldo Rodrigues da Cunha Júnior
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.5
2
+ Name: zeropx-layercomp
3
+ Version: 0.1.0
4
+ Summary: Algebraic composition of defense-in-depth layers — pick min, sum-dB, or max correctly
5
+ Project-URL: Homepage, https://github.com/NixEngine/zeropx-layercomp
6
+ Project-URL: Repository, https://github.com/NixEngine/zeropx-layercomp
7
+ Project-URL: Issues, https://github.com/NixEngine/zeropx-layercomp/issues
8
+ Author-email: "Dr. Vander Geraldo Rodrigues da Cunha Júnior" <vgrcjr@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: audit,composition,container,cryptography,defense-in-depth,security,zk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Security :: Cryptography
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # zeropx-layercomp
32
+
33
+ **Algebraic composition of defense-in-depth layers.** Pick the right operator (min, sum-dB, or max) based on the threat model — not by habit.
34
+
35
+ ## Why
36
+
37
+ There is a subtle but consequential inconsistency in how the literature composes layered defenses:
38
+
39
+ - **ZK / cryptographic security** uses `λ_efetiva = min{λ_alg, λ_FRI, λ_hash, ...}` — the weakest link.
40
+ - **EM shielding / passive attenuation** uses `SE_total_dB = SE_1 + SE_2 + ...` — additive in decibels (product in linear).
41
+ - **Consensus systems** use `max` — you fail only if the majority does.
42
+
43
+ The same word "defense-in-depth" describes all three, but the arithmetic is different, and the wrong choice can inflate a bound by orders of magnitude.
44
+
45
+ `layercomp` implements the decision tree and lets you feed a stack description and get the correctly composed score, with an explanation of which operator was used and why.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install zeropx-layercomp
51
+ ```
52
+ *(Python import module: `layercomp` | CLI command: `layercomp`)*
53
+
54
+ Zero runtime dependencies. Python 3.10+.
55
+
56
+ ## Quick start
57
+
58
+ Describe your stack in a JSON file:
59
+
60
+ ```json
61
+ {
62
+ "layers": [
63
+ { "name": "arithmetization", "score": 128, "unit": "bits", "threat_model": "adversarial" },
64
+ { "name": "FRI", "score": 100, "unit": "bits", "threat_model": "adversarial" },
65
+ { "name": "Poseidon2", "score": 95, "unit": "bits", "threat_model": "adversarial",
66
+ "notes": "BBLP22 instance ~2^95, marked Unresolved by Least Authority" }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ Then:
72
+
73
+ ```bash
74
+ layercomp score my-zk-stack.json
75
+ ```
76
+
77
+ Output:
78
+
79
+ ```
80
+ layercomp v0.1.0
81
+
82
+ Composition result
83
+ operator: min
84
+ total: 95 bits
85
+ determined: Poseidon2
86
+
87
+ Rationale: adversarial threat model — attacker selects weakest layer.
88
+ Composition is the minimum score across layers.
89
+
90
+ Per-layer:
91
+ arithmetization: 128 bits [adversarial, indep=True]
92
+ FRI: 100 bits [adversarial, indep=True]
93
+ * Poseidon2: 95 bits [adversarial, indep=True] — BBLP22 instance ~2^95, marked Unresolved by Least Authority
94
+ ```
95
+
96
+ ## Decision tree
97
+
98
+ ```
99
+ layercomp explain
100
+ ```
101
+
102
+ Prints:
103
+
104
+ ```
105
+ 1. Any layer ADVERSARIAL or mixed → min (most conservative)
106
+ 2. All CONSENSUS → max (honest-majority)
107
+ 3. All PASSIVE and all independent → sum-in-dB
108
+ 4. PASSIVE but dependent → downgrade to min with warning
109
+ ```
110
+
111
+ ## The common mistake this fixes
112
+
113
+ Two frameworks routinely conflate these operators:
114
+
115
+ - Container hardening scored with `SE_container = SE_ns + SE_cgroup + SE_seccomp + SE_net` is wrong under the adversarial model (attacker needs to escape *one* namespace, not all). It should be `min(...)`.
116
+ - Meanwhile, `sum-in-dB` is the right composition for a Faraday cage against a passive EM leakage — the field really is attenuated through every layer in series.
117
+
118
+ The distinction is `who is choosing the attack path`: an adversary chooses the weakest link; nature/noise flows through everything.
119
+
120
+ ## As a library
121
+
122
+ ```python
123
+ from layercomp import Layer, ThreatModel, ScoreUnit, compose
124
+
125
+ result = compose([
126
+ Layer("arithmetization", 128, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
127
+ Layer("FRI", 100, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
128
+ Layer("Poseidon2", 95, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
129
+ ])
130
+
131
+ print(result.total) # 95
132
+ print(result.operator_used) # 'min'
133
+ print(result.layer_that_determined) # 'Poseidon2'
134
+ print(result.rationale) # human-readable explanation
135
+ ```
136
+
137
+ ## Testing
138
+
139
+ ```bash
140
+ pip install -e ".[test]"
141
+ python -m pytest tests/ -v
142
+ ```
143
+
144
+ ## Companion tools
145
+
146
+ `zeropx-layercomp` is part of the ZeroPx Auditor's Toolkit of zero-dependency Python packages:
147
+
148
+ - **zeropx-gguf-inspect** — static read-only inspection of GGUF model files without loading them
149
+ - **zeropx-corpus-scan** — byte-honest SHA-256 inventory of a document corpus
150
+ - **zeropx-receipt-chain** — append-only tamper-evident event ledger
151
+ - **zeropx-poseidon2-goldilocks** — pure-Python Poseidon2 reference implementation over Goldilocks
152
+
153
+ ## Author & Provenance
154
+
155
+ **Sole Author:** Dr. Vander Geraldo Rodrigues da Cunha Júnior (`vgrcjr@gmail.com`).
156
+
157
+ *Technical & AI-Assisted Provenance Disclosure:* Auxiliary computational/LLM tooling was used strictly for boilerplate and test-harness scaffolding under 1:1 human architectural design, algebraic verification, code review, and sole human authorship. Zero runtime third-party or AI dependencies.
158
+
159
+ ## License
160
+
161
+ MIT.
@@ -0,0 +1,131 @@
1
+ # zeropx-layercomp
2
+
3
+ **Algebraic composition of defense-in-depth layers.** Pick the right operator (min, sum-dB, or max) based on the threat model — not by habit.
4
+
5
+ ## Why
6
+
7
+ There is a subtle but consequential inconsistency in how the literature composes layered defenses:
8
+
9
+ - **ZK / cryptographic security** uses `λ_efetiva = min{λ_alg, λ_FRI, λ_hash, ...}` — the weakest link.
10
+ - **EM shielding / passive attenuation** uses `SE_total_dB = SE_1 + SE_2 + ...` — additive in decibels (product in linear).
11
+ - **Consensus systems** use `max` — you fail only if the majority does.
12
+
13
+ The same word "defense-in-depth" describes all three, but the arithmetic is different, and the wrong choice can inflate a bound by orders of magnitude.
14
+
15
+ `layercomp` implements the decision tree and lets you feed a stack description and get the correctly composed score, with an explanation of which operator was used and why.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install zeropx-layercomp
21
+ ```
22
+ *(Python import module: `layercomp` | CLI command: `layercomp`)*
23
+
24
+ Zero runtime dependencies. Python 3.10+.
25
+
26
+ ## Quick start
27
+
28
+ Describe your stack in a JSON file:
29
+
30
+ ```json
31
+ {
32
+ "layers": [
33
+ { "name": "arithmetization", "score": 128, "unit": "bits", "threat_model": "adversarial" },
34
+ { "name": "FRI", "score": 100, "unit": "bits", "threat_model": "adversarial" },
35
+ { "name": "Poseidon2", "score": 95, "unit": "bits", "threat_model": "adversarial",
36
+ "notes": "BBLP22 instance ~2^95, marked Unresolved by Least Authority" }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ Then:
42
+
43
+ ```bash
44
+ layercomp score my-zk-stack.json
45
+ ```
46
+
47
+ Output:
48
+
49
+ ```
50
+ layercomp v0.1.0
51
+
52
+ Composition result
53
+ operator: min
54
+ total: 95 bits
55
+ determined: Poseidon2
56
+
57
+ Rationale: adversarial threat model — attacker selects weakest layer.
58
+ Composition is the minimum score across layers.
59
+
60
+ Per-layer:
61
+ arithmetization: 128 bits [adversarial, indep=True]
62
+ FRI: 100 bits [adversarial, indep=True]
63
+ * Poseidon2: 95 bits [adversarial, indep=True] — BBLP22 instance ~2^95, marked Unresolved by Least Authority
64
+ ```
65
+
66
+ ## Decision tree
67
+
68
+ ```
69
+ layercomp explain
70
+ ```
71
+
72
+ Prints:
73
+
74
+ ```
75
+ 1. Any layer ADVERSARIAL or mixed → min (most conservative)
76
+ 2. All CONSENSUS → max (honest-majority)
77
+ 3. All PASSIVE and all independent → sum-in-dB
78
+ 4. PASSIVE but dependent → downgrade to min with warning
79
+ ```
80
+
81
+ ## The common mistake this fixes
82
+
83
+ Two frameworks routinely conflate these operators:
84
+
85
+ - Container hardening scored with `SE_container = SE_ns + SE_cgroup + SE_seccomp + SE_net` is wrong under the adversarial model (attacker needs to escape *one* namespace, not all). It should be `min(...)`.
86
+ - Meanwhile, `sum-in-dB` is the right composition for a Faraday cage against a passive EM leakage — the field really is attenuated through every layer in series.
87
+
88
+ The distinction is `who is choosing the attack path`: an adversary chooses the weakest link; nature/noise flows through everything.
89
+
90
+ ## As a library
91
+
92
+ ```python
93
+ from layercomp import Layer, ThreatModel, ScoreUnit, compose
94
+
95
+ result = compose([
96
+ Layer("arithmetization", 128, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
97
+ Layer("FRI", 100, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
98
+ Layer("Poseidon2", 95, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
99
+ ])
100
+
101
+ print(result.total) # 95
102
+ print(result.operator_used) # 'min'
103
+ print(result.layer_that_determined) # 'Poseidon2'
104
+ print(result.rationale) # human-readable explanation
105
+ ```
106
+
107
+ ## Testing
108
+
109
+ ```bash
110
+ pip install -e ".[test]"
111
+ python -m pytest tests/ -v
112
+ ```
113
+
114
+ ## Companion tools
115
+
116
+ `zeropx-layercomp` is part of the ZeroPx Auditor's Toolkit of zero-dependency Python packages:
117
+
118
+ - **zeropx-gguf-inspect** — static read-only inspection of GGUF model files without loading them
119
+ - **zeropx-corpus-scan** — byte-honest SHA-256 inventory of a document corpus
120
+ - **zeropx-receipt-chain** — append-only tamper-evident event ledger
121
+ - **zeropx-poseidon2-goldilocks** — pure-Python Poseidon2 reference implementation over Goldilocks
122
+
123
+ ## Author & Provenance
124
+
125
+ **Sole Author:** Dr. Vander Geraldo Rodrigues da Cunha Júnior (`vgrcjr@gmail.com`).
126
+
127
+ *Technical & AI-Assisted Provenance Disclosure:* Auxiliary computational/LLM tooling was used strictly for boilerplate and test-harness scaffolding under 1:1 human architectural design, algebraic verification, code review, and sole human authorship. Zero runtime third-party or AI dependencies.
128
+
129
+ ## License
130
+
131
+ MIT.
@@ -0,0 +1,15 @@
1
+ {
2
+ "description": "Container hardening under the adversarial model — corrects the sum-in-dB approach from EM analogy",
3
+ "layers": [
4
+ { "name": "namespaces", "score": 40, "unit": "bits", "threat_model": "adversarial",
5
+ "notes": "PID/NET/MNT/UTS/IPC/USER/CGROUP — one escape suffices" },
6
+ { "name": "cgroups", "score": 45, "unit": "bits", "threat_model": "adversarial",
7
+ "notes": "CVE-2022-0492 class demonstrated cgroup release_agent escape" },
8
+ { "name": "seccomp", "score": 35, "unit": "bits", "threat_model": "adversarial",
9
+ "notes": "Docker default blocks ~44 syscalls; wide surface if unconfined" },
10
+ { "name": "capabilities","score": 50, "unit": "bits", "threat_model": "adversarial",
11
+ "notes": "cap-add=ALL nullifies this" },
12
+ { "name": "runc", "score": 30, "unit": "bits", "threat_model": "adversarial",
13
+ "notes": "CVE-2019-5736 / CVE-2024-21626 / CVE-2025-31133 series" }
14
+ ]
15
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "description": "Physical Faraday cage against passive EM leakage — sum-in-dB is correct here",
3
+ "layers": [
4
+ { "name": "outer mesh", "score": 40, "unit": "dB", "threat_model": "passive", "independent": true,
5
+ "notes": "conductive mesh, skin depth argument" },
6
+ { "name": "inner foil layer", "score": 30, "unit": "dB", "threat_model": "passive", "independent": true },
7
+ { "name": "gasket seal", "score": 15, "unit": "dB", "threat_model": "passive", "independent": true,
8
+ "notes": "seam attenuation" }
9
+ ]
10
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "description": "PLONK/Plonky2 stack in the adversarial model — Poseidon2 pins the bound at ~95 bits",
3
+ "layers": [
4
+ { "name": "arithmetization", "score": 128, "unit": "bits", "threat_model": "adversarial",
5
+ "notes": "PLONK gate/permutation constraints; bound from field size" },
6
+ { "name": "FRI", "score": 100, "unit": "bits", "threat_model": "adversarial",
7
+ "notes": "FRI proximity gap with typical FriConfig" },
8
+ { "name": "Poseidon2", "score": 95, "unit": "bits", "threat_model": "adversarial",
9
+ "notes": "BBLP22 instance ~2^95, Least Authority marked Unresolved" },
10
+ { "name": "Fiat-Shamir", "score": 128, "unit": "bits", "threat_model": "adversarial",
11
+ "notes": "assuming complete transcript per Frozen Heart checklist" },
12
+ { "name": "impl", "score": 120, "unit": "bits", "threat_model": "adversarial",
13
+ "notes": "informal upper bound; excludes CVE-2025-24802-class regressions" }
14
+ ]
15
+ }
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "zeropx-layercomp"
7
+ version = "0.1.0"
8
+ description = "Algebraic composition of defense-in-depth layers — pick min, sum-dB, or max correctly"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Dr. Vander Geraldo Rodrigues da Cunha Júnior", email = "vgrcjr@gmail.com" }]
13
+ keywords = ["security", "cryptography", "defense-in-depth", "composition", "audit", "zk", "container"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Information Technology",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Security",
27
+ "Topic :: Security :: Cryptography",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=7"]
34
+
35
+ [project.scripts]
36
+ layercomp = "layercomp.cli:main"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/NixEngine/zeropx-layercomp"
40
+ Repository = "https://github.com/NixEngine/zeropx-layercomp"
41
+ Issues = "https://github.com/NixEngine/zeropx-layercomp/issues"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/layercomp"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = ["/src", "/tests", "/examples", "/README.md", "/LICENSE"]
@@ -0,0 +1,28 @@
1
+ """layercomp — algebraic composition of defense-in-depth layers.
2
+
3
+ Resolves the min-vs-sum-dB tension observed across security frameworks:
4
+ - ZK/cryptography → min (weakest-link adversarial model)
5
+ - EM shielding → sum-in-dB (passive independent attenuation)
6
+ - Consensus systems → max (honest-majority)
7
+
8
+ The correct operator depends on (threat_model, independence, correlation)
9
+ of each layer stack. Default is min (most conservative).
10
+ """
11
+
12
+ from .composition import (
13
+ Layer,
14
+ ThreatModel,
15
+ ScoreUnit,
16
+ CompositionResult,
17
+ compose,
18
+ )
19
+
20
+ __version__ = "0.1.0"
21
+ __all__ = [
22
+ "Layer",
23
+ "ThreatModel",
24
+ "ScoreUnit",
25
+ "CompositionResult",
26
+ "compose",
27
+ "__version__",
28
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,131 @@
1
+ """layercomp CLI."""
2
+ import argparse
3
+ import json
4
+ import sys
5
+ from typing import List
6
+
7
+ from .composition import Layer, ThreatModel, ScoreUnit, compose
8
+ from . import __version__
9
+
10
+
11
+ def _load_stack(path: str) -> List[Layer]:
12
+ with open(path, encoding="utf-8") as f:
13
+ data = json.load(f)
14
+ layers = []
15
+ for l in data["layers"]:
16
+ layers.append(
17
+ Layer(
18
+ name=l["name"],
19
+ score=float(l["score"]),
20
+ unit=ScoreUnit(l.get("unit", "bits")),
21
+ threat_model=ThreatModel(l.get("threat_model", "adversarial")),
22
+ independent=bool(l.get("independent", True)),
23
+ notes=l.get("notes"),
24
+ )
25
+ )
26
+ return layers
27
+
28
+
29
+ def cmd_score(args) -> int:
30
+ layers = _load_stack(args.file)
31
+ result = compose(layers)
32
+
33
+ if args.format == "json":
34
+ out = {
35
+ "total": result.total,
36
+ "unit": result.unit.value,
37
+ "operator_used": result.operator_used,
38
+ "layer_that_determined": result.layer_that_determined,
39
+ "rationale": result.rationale,
40
+ "per_layer": result.per_layer,
41
+ "warnings": result.warnings,
42
+ }
43
+ print(json.dumps(out, indent=2, ensure_ascii=False))
44
+ else:
45
+ print(f"layercomp v{__version__}")
46
+ print()
47
+ print(f"Composition result")
48
+ print(f" operator: {result.operator_used}")
49
+ print(f" total: {result.total} {result.unit.value}")
50
+ if result.layer_that_determined:
51
+ print(f" determined: {result.layer_that_determined}")
52
+ print()
53
+ print(f"Rationale: {result.rationale}")
54
+ print()
55
+ print("Per-layer:")
56
+ for name, meta in result.per_layer.items():
57
+ marker = " * " if name == result.layer_that_determined else " "
58
+ note = f" — {meta['notes']}" if meta.get("notes") else ""
59
+ print(
60
+ f"{marker}{name}: {meta['score']} {meta['unit']} "
61
+ f"[{meta['threat_model']}, indep={meta['independent']}]{note}"
62
+ )
63
+ if result.warnings:
64
+ print()
65
+ print("Warnings:")
66
+ for w in result.warnings:
67
+ print(f" ! {w}")
68
+
69
+ return 0
70
+
71
+
72
+ def cmd_explain(args) -> int:
73
+ """Show the decision tree used by compose()."""
74
+ print(
75
+ """layercomp decision tree
76
+ =======================
77
+
78
+ Given a stack of defense layers, the operator is chosen as follows:
79
+
80
+ 1. If ANY layer has threat_model = ADVERSARIAL, or if threat models are mixed
81
+ → use min (weakest-link, most conservative). This matches:
82
+ - ZK security: lambda_efetiva = min{lambda_alg, lambda_FRI, ...}
83
+ - Container escape: attacker needs to break just one namespace/cap
84
+ - Jurisdictional gates: one regulator denying is enough
85
+
86
+ 2. If ALL layers are CONSENSUS
87
+ → use max (honest-majority; system fails only if majority fails). Matches:
88
+ - BFT protocols
89
+ - Multi-sig (m-of-n where n-m+1 must fail)
90
+
91
+ 3. If ALL layers are PASSIVE and all independent
92
+ → use sum-in-dB (equivalent to product in linear). Matches:
93
+ - EM shielding: SE_total_dB = SE_ns + SE_cgroup + ...
94
+ - Statistical decorrelation of independent side channels
95
+ - Noise attenuation stages
96
+
97
+ 4. If PASSIVE but layers are dependent
98
+ → downgrade to min (with warning). Correlation invalidates additivity.
99
+
100
+ Notes
101
+ -----
102
+ - Mixed units emit a warning; composition assumes homogeneous units.
103
+ - "Independent" means: attacker cannot use one layer's weakness to bypass another.
104
+ - "Adversarial" is the default and safest assumption when in doubt.
105
+ """
106
+ )
107
+ return 0
108
+
109
+
110
+ def main() -> None:
111
+ parser = argparse.ArgumentParser(
112
+ prog="layercomp",
113
+ description="Compose defense layers with the correct algebraic operator (min/sum-dB/max)",
114
+ )
115
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
116
+ subs = parser.add_subparsers(dest="command", required=True)
117
+
118
+ p_score = subs.add_parser("score", help="Compute composed score for a defense stack")
119
+ p_score.add_argument("file", help="Path to JSON file describing the layer stack")
120
+ p_score.add_argument("--format", choices=["text", "json"], default="text")
121
+ p_score.set_defaults(func=cmd_score)
122
+
123
+ p_explain = subs.add_parser("explain", help="Show the decision tree used by compose()")
124
+ p_explain.set_defaults(func=cmd_explain)
125
+
126
+ args = parser.parse_args()
127
+ sys.exit(args.func(args))
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
@@ -0,0 +1,149 @@
1
+ """Core composition algebra."""
2
+ from dataclasses import dataclass, field
3
+ from enum import Enum
4
+ from typing import List, Optional, Dict, Any
5
+
6
+
7
+ class ThreatModel(Enum):
8
+ ADVERSARIAL = "adversarial" # attacker chooses weakest link (crypto, security bounds)
9
+ PASSIVE = "passive" # random noise/leakage, all layers filter (EM shielding, statistical isolation)
10
+ CONSENSUS = "consensus" # need majority to fail (BFT, multi-sig, honest-majority)
11
+
12
+
13
+ class ScoreUnit(Enum):
14
+ BITS = "bits" # cryptographic bit-security
15
+ DB = "dB" # decibels (10*log10 or 20*log10 of ratio)
16
+ LINEAR = "linear" # linear probability, factor, or dimensionless ratio
17
+ ORDINAL = "ordinal" # relative rank only, no arithmetic guaranteed
18
+
19
+
20
+ @dataclass
21
+ class Layer:
22
+ name: str
23
+ score: float
24
+ unit: ScoreUnit = ScoreUnit.BITS
25
+ threat_model: ThreatModel = ThreatModel.ADVERSARIAL
26
+ independent: bool = True
27
+ notes: Optional[str] = None
28
+
29
+ def __post_init__(self):
30
+ if not isinstance(self.unit, ScoreUnit):
31
+ self.unit = ScoreUnit(self.unit)
32
+ if not isinstance(self.threat_model, ThreatModel):
33
+ self.threat_model = ThreatModel(self.threat_model)
34
+
35
+
36
+ @dataclass
37
+ class CompositionResult:
38
+ total: float
39
+ unit: ScoreUnit
40
+ operator_used: str
41
+ layer_that_determined: Optional[str]
42
+ per_layer: Dict[str, Dict[str, Any]]
43
+ warnings: List[str] = field(default_factory=list)
44
+ rationale: str = ""
45
+
46
+
47
+ def compose(layers: List[Layer], target_unit: Optional[ScoreUnit] = None) -> CompositionResult:
48
+ """Compose defense layers into a single bound using the correct operator.
49
+
50
+ Decision tree:
51
+ - If any threat model is ADVERSARIAL, or mixed models → use min (most conservative)
52
+ - If all layers PASSIVE and all independent → use sum-in-dB (equivalent to product in linear)
53
+ - If all layers CONSENSUS → use max
54
+ - If dependent layers with passive/consensus model → downgrade to min with warning
55
+ """
56
+ if not layers:
57
+ raise ValueError("no layers provided")
58
+
59
+ warnings: List[str] = []
60
+ threat_models = set(l.threat_model for l in layers)
61
+ units = set(l.unit for l in layers)
62
+
63
+ if len(units) > 1:
64
+ warnings.append(
65
+ f"mixed units {[u.value for u in units]} — composition assumes homogeneous units; "
66
+ "convert layers to a single unit before composition or interpret result cautiously"
67
+ )
68
+
69
+ if target_unit is None:
70
+ target_unit = layers[0].unit
71
+
72
+ # Determine operator
73
+ if ThreatModel.ADVERSARIAL in threat_models:
74
+ primary = ThreatModel.ADVERSARIAL
75
+ if len(threat_models) > 1:
76
+ warnings.append(
77
+ f"mixed threat models {[t.value for t in threat_models]} — falling back to adversarial (min)"
78
+ )
79
+ elif threat_models == {ThreatModel.CONSENSUS}:
80
+ primary = ThreatModel.CONSENSUS
81
+ elif threat_models == {ThreatModel.PASSIVE}:
82
+ all_indep = all(l.independent for l in layers)
83
+ if not all_indep:
84
+ warnings.append(
85
+ "passive threat model with dependent layers — additive composition unsafe; "
86
+ "falling back to min"
87
+ )
88
+ primary = ThreatModel.ADVERSARIAL
89
+ else:
90
+ primary = ThreatModel.PASSIVE
91
+ else:
92
+ primary = ThreatModel.ADVERSARIAL
93
+
94
+ # Apply operator
95
+ if primary == ThreatModel.ADVERSARIAL:
96
+ operator = "min"
97
+ determining = min(layers, key=lambda l: l.score)
98
+ total = determining.score
99
+ det_name = determining.name
100
+ rationale = (
101
+ "adversarial threat model — attacker selects weakest layer. "
102
+ "Composition is the minimum score across layers."
103
+ )
104
+ elif primary == ThreatModel.PASSIVE:
105
+ if target_unit == ScoreUnit.DB:
106
+ operator = "sum-in-dB"
107
+ total = sum(l.score for l in layers)
108
+ else:
109
+ operator = "sum-in-dB (raw)"
110
+ total = sum(l.score for l in layers)
111
+ warnings.append(
112
+ f"passive composition on non-dB unit ({target_unit.value}) is unusual; "
113
+ "consider recasting layers in dB — result reported as raw sum"
114
+ )
115
+ det_name = None
116
+ rationale = (
117
+ "passive threat model with independent layers — attenuations add. "
118
+ "In dB this is sum; in linear it would be product of factors."
119
+ )
120
+ else: # CONSENSUS
121
+ operator = "max"
122
+ determining = max(layers, key=lambda l: l.score)
123
+ total = determining.score
124
+ det_name = determining.name
125
+ rationale = (
126
+ "consensus / honest-majority model — system fails only when majority fails. "
127
+ "Composition is the maximum score across layers (strongest quorum member)."
128
+ )
129
+
130
+ per_layer = {
131
+ l.name: {
132
+ "score": l.score,
133
+ "unit": l.unit.value,
134
+ "threat_model": l.threat_model.value,
135
+ "independent": l.independent,
136
+ "notes": l.notes,
137
+ }
138
+ for l in layers
139
+ }
140
+
141
+ return CompositionResult(
142
+ total=total,
143
+ unit=target_unit,
144
+ operator_used=operator,
145
+ layer_that_determined=det_name,
146
+ per_layer=per_layer,
147
+ warnings=warnings,
148
+ rationale=rationale,
149
+ )
@@ -0,0 +1,116 @@
1
+ """Tests for layercomp composition algebra."""
2
+ import pytest
3
+
4
+ from layercomp import Layer, ThreatModel, ScoreUnit, compose
5
+
6
+
7
+ class TestAdversarialMin:
8
+ def test_zk_stack_returns_min(self):
9
+ layers = [
10
+ Layer("alg", 128, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
11
+ Layer("FRI", 100, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
12
+ Layer("hash", 95, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
13
+ Layer("FS", 128, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
14
+ Layer("impl", 120, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
15
+ ]
16
+ r = compose(layers)
17
+ assert r.operator_used == "min"
18
+ assert r.total == 95
19
+ assert r.layer_that_determined == "hash"
20
+
21
+ def test_single_layer(self):
22
+ r = compose([Layer("only", 128)])
23
+ assert r.total == 128
24
+ assert r.operator_used == "min"
25
+
26
+ def test_mixed_threat_falls_back_to_min(self):
27
+ layers = [
28
+ Layer("crypto", 128, threat_model=ThreatModel.ADVERSARIAL),
29
+ Layer("shield", 40, unit=ScoreUnit.DB, threat_model=ThreatModel.PASSIVE),
30
+ ]
31
+ r = compose(layers)
32
+ assert r.operator_used == "min"
33
+ assert r.total == 40
34
+ assert any("mixed threat models" in w for w in r.warnings)
35
+
36
+
37
+ class TestPassiveSumInDB:
38
+ def test_em_shielding_stack_returns_sum(self):
39
+ layers = [
40
+ Layer("ns", 20, ScoreUnit.DB, ThreatModel.PASSIVE, independent=True),
41
+ Layer("cgroup", 15, ScoreUnit.DB, ThreatModel.PASSIVE, independent=True),
42
+ Layer("seccomp", 25, ScoreUnit.DB, ThreatModel.PASSIVE, independent=True),
43
+ Layer("net", 30, ScoreUnit.DB, ThreatModel.PASSIVE, independent=True),
44
+ ]
45
+ r = compose(layers)
46
+ assert r.operator_used == "sum-in-dB"
47
+ assert r.total == 90 # 20+15+25+30
48
+ assert r.layer_that_determined is None
49
+
50
+ def test_passive_dependent_downgrades_to_min(self):
51
+ layers = [
52
+ Layer("l1", 20, ScoreUnit.DB, ThreatModel.PASSIVE, independent=False),
53
+ Layer("l2", 15, ScoreUnit.DB, ThreatModel.PASSIVE, independent=True),
54
+ ]
55
+ r = compose(layers)
56
+ assert r.operator_used == "min"
57
+ assert r.total == 15
58
+ assert any("dependent" in w for w in r.warnings)
59
+
60
+
61
+ class TestConsensusMax:
62
+ def test_bft_stack_returns_max(self):
63
+ layers = [
64
+ Layer("node1", 100, threat_model=ThreatModel.CONSENSUS),
65
+ Layer("node2", 90, threat_model=ThreatModel.CONSENSUS),
66
+ Layer("node3", 110, threat_model=ThreatModel.CONSENSUS),
67
+ ]
68
+ r = compose(layers)
69
+ assert r.operator_used == "max"
70
+ assert r.total == 110
71
+ assert r.layer_that_determined == "node3"
72
+
73
+
74
+ class TestRealWorldContradiction:
75
+ """Test the exact contradiction that motivated layercomp:
76
+ - Junior's PLONK Manifest says min for defense-in-depth
77
+ - Junior's Faraday-Digital Report says sum-in-dB
78
+ Both cannot be right for the same threat model.
79
+ """
80
+
81
+ def test_zk_manifest_correct_min(self):
82
+ """PLONK/Poseidon2 λ_efetiva = min{...}"""
83
+ layers = [
84
+ Layer("lambda_alg", 128, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
85
+ Layer("lambda_FRI", 100, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
86
+ Layer("lambda_hash", 95, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
87
+ ]
88
+ r = compose(layers)
89
+ assert r.total == 95
90
+ assert r.operator_used == "min"
91
+
92
+ def test_container_adversarial_should_be_min_not_sum(self):
93
+ """Corrects the Faraday-Digital error: container escape is adversarial, not passive."""
94
+ layers = [
95
+ Layer("ns", 20, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
96
+ Layer("seccomp", 15, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
97
+ Layer("cap", 25, ScoreUnit.BITS, ThreatModel.ADVERSARIAL),
98
+ ]
99
+ r = compose(layers)
100
+ # This is 15, NOT 60 as sum-dB would suggest
101
+ assert r.total == 15
102
+ assert r.operator_used == "min"
103
+
104
+
105
+ class TestValidation:
106
+ def test_empty_stack_raises(self):
107
+ with pytest.raises(ValueError):
108
+ compose([])
109
+
110
+ def test_mixed_units_warns(self):
111
+ layers = [
112
+ Layer("bits", 128, ScoreUnit.BITS),
113
+ Layer("dB", 40, ScoreUnit.DB),
114
+ ]
115
+ r = compose(layers)
116
+ assert any("mixed units" in w for w in r.warnings)