sovereign-sensor 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.
- sovereign_sensor-0.1.0/PKG-INFO +156 -0
- sovereign_sensor-0.1.0/README.md +136 -0
- sovereign_sensor-0.1.0/pyproject.toml +31 -0
- sovereign_sensor-0.1.0/setup.cfg +4 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor/__init__.py +83 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor/drivers/__init__.py +2 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor/drivers/esp32_hardware.py +67 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor/drivers/software_fallback.py +110 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor/envelope.py +165 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor/interface.py +63 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor.egg-info/PKG-INFO +156 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor.egg-info/SOURCES.txt +13 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor.egg-info/dependency_links.txt +1 -0
- sovereign_sensor-0.1.0/src/sovereign_sensor.egg-info/top_level.txt +1 -0
- sovereign_sensor-0.1.0/tests/test_sensor.py +692 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sovereign-sensor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bare-metal Write-Side Custody enforcement for MicroPython sensor nodes with hardware crypto abstraction
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/kenwalger/sovereign-sdk
|
|
7
|
+
Project-URL: Repository, https://github.com/kenwalger/sovereign-sdk
|
|
8
|
+
Project-URL: Changelog, https://github.com/kenwalger/sovereign-sdk/blob/main/CHANGELOG.md
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: System :: Hardware
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# sovereign-sensor — Phase 9
|
|
22
|
+
|
|
23
|
+
A lightweight, MicroPython-compatible Hardware Abstraction Layer (HAL) that enforces
|
|
24
|
+
data custody at the **Point of Genesis** by sealing each sensor observation into a
|
|
25
|
+
versioned, tamper-evident, replay-protected JSON transmission envelope before any
|
|
26
|
+
network transit or cloud ingestion occurs.
|
|
27
|
+
|
|
28
|
+
Zero external dependencies. Internal library code is restricted to standard MicroPython
|
|
29
|
+
built-ins (`json`, `sys`, `hashlib`, `hmac`, `binascii`). Targets ESP32 and Raspberry Pi
|
|
30
|
+
Pico via MicroPython; fully exercisable on CPython 3.12 for desktop CI.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
### Hardware Abstraction Layer
|
|
37
|
+
|
|
38
|
+
`SovereignCryptoDriver` (`interface.py`) defines a three-method contract:
|
|
39
|
+
|
|
40
|
+
| Method | Contract |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `initialize_hardware() -> None` | Load key material; configure accelerator subsystem. |
|
|
43
|
+
| `sign(payload: bytes) -> bytes` | Return raw binary signature bytes (no encoding). |
|
|
44
|
+
| `algorithm() -> str` | Return a canonical algorithm identifier string. |
|
|
45
|
+
|
|
46
|
+
Two concrete drivers are provided:
|
|
47
|
+
|
|
48
|
+
- **`SoftwareFallbackDriver`** — HMAC-SHA256 over a VFS-resident binary key file.
|
|
49
|
+
Used on all non-ESP32 targets (desktop CI, Raspberry Pi Pico, etc.).
|
|
50
|
+
The class-level `MOCK_KEY_SENTINEL = "/mock/test_gateway.key"` opts into a
|
|
51
|
+
deterministic stub key for desktop testing; every other path that cannot be opened
|
|
52
|
+
raises `RuntimeError` immediately, eliminating silent key substitution. A zero-byte
|
|
53
|
+
key file raises `ValueError` to prevent HMAC keyed with `b""`.
|
|
54
|
+
|
|
55
|
+
- **`ESP32HardwareDriver`** — Skeleton placeholder for the on-chip ECC accelerator.
|
|
56
|
+
`initialize_hardware()` raises `NotImplementedError` until register-level engineering
|
|
57
|
+
is complete; `bootstrap_sensor_node()` catches this and falls back to
|
|
58
|
+
`SoftwareFallbackDriver` automatically so the sealing and VFS layers remain
|
|
59
|
+
exercisable on the workbench.
|
|
60
|
+
|
|
61
|
+
### Seven-Step Sealing Pipeline (`SovereignEnvelope.seal()`)
|
|
62
|
+
|
|
63
|
+
1. **Monotonic sequence counter** — computed transiently as `_sequence + 1` and bound
|
|
64
|
+
into the preimage. The in-memory counter and VFS file (default `.sovereign_sequence`)
|
|
65
|
+
are advanced only after signing succeeds, so a `sign()` failure never consumes a
|
|
66
|
+
sequence position or introduces a gap in the on-disk custody timeline. A truncated
|
|
67
|
+
(0-byte) file resets to 0; a negative stored value is clamped to 0. VFS write
|
|
68
|
+
failures degrade gracefully to RAM-only tracking.
|
|
69
|
+
|
|
70
|
+
2. **Algorithm identifier** — queried from the active driver via `algorithm()` and
|
|
71
|
+
embedded in the authenticated preimage, providing protocol agility without
|
|
72
|
+
a schema change.
|
|
73
|
+
|
|
74
|
+
3. **Canonical payload serialization** — `json.dumps(payload, separators=(",", ":"), sort_keys=True, ensure_ascii=False)`
|
|
75
|
+
guarantees a byte-identical preimage for semantically equivalent payloads
|
|
76
|
+
regardless of key insertion order. `ensure_ascii=False` forces raw UTF-8 output
|
|
77
|
+
for all characters, eliminating the `\uXXXX`-vs-raw-UTF-8 split-brain divergence
|
|
78
|
+
that would cause cross-platform HMAC verification to fail silently on any payload
|
|
79
|
+
containing characters outside U+007F.
|
|
80
|
+
|
|
81
|
+
4. **UTF-8 byte-count-prefixed preimage assembly** — `node_id`, `timestamp`, and the
|
|
82
|
+
`algorithm` identifier are each encoded to UTF-8 independently; the prefix for each
|
|
83
|
+
field is the UTF-8 byte count (not the Unicode character count). The preimage is
|
|
84
|
+
assembled from raw byte slices:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
1|{len(node_bytes)}:{node_id}|{len(time_bytes)}:{timestamp}|{seq}|{len(algo_bytes)}:{algo}|{canonical}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Byte-count prefixes close all variable-length field injection surfaces: delimiter
|
|
91
|
+
injection (any two inputs that differ only in where a `|` character falls produce
|
|
92
|
+
identical naive pipe-joined preimage bytes without prefixes) and multi-byte encoding
|
|
93
|
+
ambiguity (a receiver using character-count semantics parses field boundaries at the
|
|
94
|
+
wrong byte offset for any non-ASCII field value).
|
|
95
|
+
|
|
96
|
+
5. **Driver signing** — raw preimage bytes traverse `driver.sign()`, returning raw
|
|
97
|
+
binary output from the underlying cryptographic primitive.
|
|
98
|
+
|
|
99
|
+
6. **Hex encoding** — `binascii.hexlify()` maps all byte values `0x00–0xFF` to
|
|
100
|
+
the lowercase alphanumeric characters `0–9`, `a–f`, preventing
|
|
101
|
+
`UnicodeDecodeError` on constrained MicroPython silicon.
|
|
102
|
+
|
|
103
|
+
7. **Wire frame serialization** — all seven envelope fields (`v`, `n`, `t`, `q`,
|
|
104
|
+
`alg`, `d`, `s`) are packed into a dict and serialized with
|
|
105
|
+
`json.dumps(..., separators=(",", ":"), sort_keys=True, ensure_ascii=False)`,
|
|
106
|
+
freezing the alphabetical key sequence and enforcing raw UTF-8 wire encoding
|
|
107
|
+
independently of MicroPython allocator-driven insertion order.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Quick Start
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sovereign_sensor import bootstrap_sensor_node
|
|
115
|
+
|
|
116
|
+
# Auto-selects ESP32HardwareDriver or SoftwareFallbackDriver at runtime.
|
|
117
|
+
# Falls back to SoftwareFallbackDriver with a warning if hardware crypto
|
|
118
|
+
# is not yet implemented on the target.
|
|
119
|
+
envelope = bootstrap_sensor_node(
|
|
120
|
+
node_id="node-temperature-01",
|
|
121
|
+
private_key_path="/flash/keys/node.key",
|
|
122
|
+
sequence_file="/flash/.sovereign_sequence",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
observation = {"sensor": "temperature", "value": 21.4, "unit": "C"}
|
|
126
|
+
wire_bytes = envelope.seal("2026-06-16T12:00:00Z", observation)
|
|
127
|
+
# → b'{"alg":"hmac-sha256","d":{"sensor":"temperature","unit":"C","value":21.4},'
|
|
128
|
+
# '"n":"node-temperature-01","q":1,"s":"<64-char hex>","t":"2026-06-16T12:00:00Z","v":1}'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Desktop / CI (mock key sentinel)
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from sovereign_sensor import bootstrap_sensor_node
|
|
135
|
+
from sovereign_sensor.drivers.software_fallback import SoftwareFallbackDriver
|
|
136
|
+
|
|
137
|
+
envelope = bootstrap_sensor_node(
|
|
138
|
+
node_id="ci-node-001",
|
|
139
|
+
private_key_path=SoftwareFallbackDriver.MOCK_KEY_SENTINEL,
|
|
140
|
+
)
|
|
141
|
+
wire = envelope.seal("2026-06-16T00:00:00Z", {"ping": True})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Invariants
|
|
147
|
+
|
|
148
|
+
| Property | Guarantee |
|
|
149
|
+
|---|---|
|
|
150
|
+
| **Replay protection** | Monotonic `q` counter persisted to VFS; resumes across reboots. |
|
|
151
|
+
| **Sequence atomicity** | Counter advanced only after `sign()` succeeds; a signing failure leaves the on-disk counter unchanged with no gap. |
|
|
152
|
+
| **Key material safety** | Missing or empty key file raises immediately; no silent substitution. |
|
|
153
|
+
| **Preimage determinism** | `sort_keys=True` and `ensure_ascii=False` on payload; byte-count length prefixes on all three variable-length fields. |
|
|
154
|
+
| **Wire frame determinism** | `sort_keys=True` and `ensure_ascii=False` on the outer frame; byte-identical UTF-8 output across CPython and MicroPython builds. |
|
|
155
|
+
| **Encoding safety** | `binascii.hexlify` prevents `UnicodeDecodeError` on raw binary digest bytes. |
|
|
156
|
+
| **Zero dependencies** | No network calls, no PyTorch, no external packages at runtime. |
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# sovereign-sensor — Phase 9
|
|
2
|
+
|
|
3
|
+
A lightweight, MicroPython-compatible Hardware Abstraction Layer (HAL) that enforces
|
|
4
|
+
data custody at the **Point of Genesis** by sealing each sensor observation into a
|
|
5
|
+
versioned, tamper-evident, replay-protected JSON transmission envelope before any
|
|
6
|
+
network transit or cloud ingestion occurs.
|
|
7
|
+
|
|
8
|
+
Zero external dependencies. Internal library code is restricted to standard MicroPython
|
|
9
|
+
built-ins (`json`, `sys`, `hashlib`, `hmac`, `binascii`). Targets ESP32 and Raspberry Pi
|
|
10
|
+
Pico via MicroPython; fully exercisable on CPython 3.12 for desktop CI.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
### Hardware Abstraction Layer
|
|
17
|
+
|
|
18
|
+
`SovereignCryptoDriver` (`interface.py`) defines a three-method contract:
|
|
19
|
+
|
|
20
|
+
| Method | Contract |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `initialize_hardware() -> None` | Load key material; configure accelerator subsystem. |
|
|
23
|
+
| `sign(payload: bytes) -> bytes` | Return raw binary signature bytes (no encoding). |
|
|
24
|
+
| `algorithm() -> str` | Return a canonical algorithm identifier string. |
|
|
25
|
+
|
|
26
|
+
Two concrete drivers are provided:
|
|
27
|
+
|
|
28
|
+
- **`SoftwareFallbackDriver`** — HMAC-SHA256 over a VFS-resident binary key file.
|
|
29
|
+
Used on all non-ESP32 targets (desktop CI, Raspberry Pi Pico, etc.).
|
|
30
|
+
The class-level `MOCK_KEY_SENTINEL = "/mock/test_gateway.key"` opts into a
|
|
31
|
+
deterministic stub key for desktop testing; every other path that cannot be opened
|
|
32
|
+
raises `RuntimeError` immediately, eliminating silent key substitution. A zero-byte
|
|
33
|
+
key file raises `ValueError` to prevent HMAC keyed with `b""`.
|
|
34
|
+
|
|
35
|
+
- **`ESP32HardwareDriver`** — Skeleton placeholder for the on-chip ECC accelerator.
|
|
36
|
+
`initialize_hardware()` raises `NotImplementedError` until register-level engineering
|
|
37
|
+
is complete; `bootstrap_sensor_node()` catches this and falls back to
|
|
38
|
+
`SoftwareFallbackDriver` automatically so the sealing and VFS layers remain
|
|
39
|
+
exercisable on the workbench.
|
|
40
|
+
|
|
41
|
+
### Seven-Step Sealing Pipeline (`SovereignEnvelope.seal()`)
|
|
42
|
+
|
|
43
|
+
1. **Monotonic sequence counter** — computed transiently as `_sequence + 1` and bound
|
|
44
|
+
into the preimage. The in-memory counter and VFS file (default `.sovereign_sequence`)
|
|
45
|
+
are advanced only after signing succeeds, so a `sign()` failure never consumes a
|
|
46
|
+
sequence position or introduces a gap in the on-disk custody timeline. A truncated
|
|
47
|
+
(0-byte) file resets to 0; a negative stored value is clamped to 0. VFS write
|
|
48
|
+
failures degrade gracefully to RAM-only tracking.
|
|
49
|
+
|
|
50
|
+
2. **Algorithm identifier** — queried from the active driver via `algorithm()` and
|
|
51
|
+
embedded in the authenticated preimage, providing protocol agility without
|
|
52
|
+
a schema change.
|
|
53
|
+
|
|
54
|
+
3. **Canonical payload serialization** — `json.dumps(payload, separators=(",", ":"), sort_keys=True, ensure_ascii=False)`
|
|
55
|
+
guarantees a byte-identical preimage for semantically equivalent payloads
|
|
56
|
+
regardless of key insertion order. `ensure_ascii=False` forces raw UTF-8 output
|
|
57
|
+
for all characters, eliminating the `\uXXXX`-vs-raw-UTF-8 split-brain divergence
|
|
58
|
+
that would cause cross-platform HMAC verification to fail silently on any payload
|
|
59
|
+
containing characters outside U+007F.
|
|
60
|
+
|
|
61
|
+
4. **UTF-8 byte-count-prefixed preimage assembly** — `node_id`, `timestamp`, and the
|
|
62
|
+
`algorithm` identifier are each encoded to UTF-8 independently; the prefix for each
|
|
63
|
+
field is the UTF-8 byte count (not the Unicode character count). The preimage is
|
|
64
|
+
assembled from raw byte slices:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
1|{len(node_bytes)}:{node_id}|{len(time_bytes)}:{timestamp}|{seq}|{len(algo_bytes)}:{algo}|{canonical}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Byte-count prefixes close all variable-length field injection surfaces: delimiter
|
|
71
|
+
injection (any two inputs that differ only in where a `|` character falls produce
|
|
72
|
+
identical naive pipe-joined preimage bytes without prefixes) and multi-byte encoding
|
|
73
|
+
ambiguity (a receiver using character-count semantics parses field boundaries at the
|
|
74
|
+
wrong byte offset for any non-ASCII field value).
|
|
75
|
+
|
|
76
|
+
5. **Driver signing** — raw preimage bytes traverse `driver.sign()`, returning raw
|
|
77
|
+
binary output from the underlying cryptographic primitive.
|
|
78
|
+
|
|
79
|
+
6. **Hex encoding** — `binascii.hexlify()` maps all byte values `0x00–0xFF` to
|
|
80
|
+
the lowercase alphanumeric characters `0–9`, `a–f`, preventing
|
|
81
|
+
`UnicodeDecodeError` on constrained MicroPython silicon.
|
|
82
|
+
|
|
83
|
+
7. **Wire frame serialization** — all seven envelope fields (`v`, `n`, `t`, `q`,
|
|
84
|
+
`alg`, `d`, `s`) are packed into a dict and serialized with
|
|
85
|
+
`json.dumps(..., separators=(",", ":"), sort_keys=True, ensure_ascii=False)`,
|
|
86
|
+
freezing the alphabetical key sequence and enforcing raw UTF-8 wire encoding
|
|
87
|
+
independently of MicroPython allocator-driven insertion order.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Quick Start
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from sovereign_sensor import bootstrap_sensor_node
|
|
95
|
+
|
|
96
|
+
# Auto-selects ESP32HardwareDriver or SoftwareFallbackDriver at runtime.
|
|
97
|
+
# Falls back to SoftwareFallbackDriver with a warning if hardware crypto
|
|
98
|
+
# is not yet implemented on the target.
|
|
99
|
+
envelope = bootstrap_sensor_node(
|
|
100
|
+
node_id="node-temperature-01",
|
|
101
|
+
private_key_path="/flash/keys/node.key",
|
|
102
|
+
sequence_file="/flash/.sovereign_sequence",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
observation = {"sensor": "temperature", "value": 21.4, "unit": "C"}
|
|
106
|
+
wire_bytes = envelope.seal("2026-06-16T12:00:00Z", observation)
|
|
107
|
+
# → b'{"alg":"hmac-sha256","d":{"sensor":"temperature","unit":"C","value":21.4},'
|
|
108
|
+
# '"n":"node-temperature-01","q":1,"s":"<64-char hex>","t":"2026-06-16T12:00:00Z","v":1}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Desktop / CI (mock key sentinel)
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sovereign_sensor import bootstrap_sensor_node
|
|
115
|
+
from sovereign_sensor.drivers.software_fallback import SoftwareFallbackDriver
|
|
116
|
+
|
|
117
|
+
envelope = bootstrap_sensor_node(
|
|
118
|
+
node_id="ci-node-001",
|
|
119
|
+
private_key_path=SoftwareFallbackDriver.MOCK_KEY_SENTINEL,
|
|
120
|
+
)
|
|
121
|
+
wire = envelope.seal("2026-06-16T00:00:00Z", {"ping": True})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Invariants
|
|
127
|
+
|
|
128
|
+
| Property | Guarantee |
|
|
129
|
+
|---|---|
|
|
130
|
+
| **Replay protection** | Monotonic `q` counter persisted to VFS; resumes across reboots. |
|
|
131
|
+
| **Sequence atomicity** | Counter advanced only after `sign()` succeeds; a signing failure leaves the on-disk counter unchanged with no gap. |
|
|
132
|
+
| **Key material safety** | Missing or empty key file raises immediately; no silent substitution. |
|
|
133
|
+
| **Preimage determinism** | `sort_keys=True` and `ensure_ascii=False` on payload; byte-count length prefixes on all three variable-length fields. |
|
|
134
|
+
| **Wire frame determinism** | `sort_keys=True` and `ensure_ascii=False` on the outer frame; byte-identical UTF-8 output across CPython and MicroPython builds. |
|
|
135
|
+
| **Encoding safety** | `binascii.hexlify` prevents `UnicodeDecodeError` on raw binary digest bytes. |
|
|
136
|
+
| **Zero dependencies** | No network calls, no PyTorch, no external packages at runtime. |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sovereign-sensor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Bare-metal Write-Side Custody enforcement for MicroPython sensor nodes with hardware crypto abstraction"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12" # Enforces CPython 3.12+ for desktop development/CI test suites; package library code remains strictly bare-metal MicroPython compatible.
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = []
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: Implementation :: MicroPython",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
"Topic :: System :: Hardware",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/kenwalger/sovereign-sdk"
|
|
27
|
+
Repository = "https://github.com/kenwalger/sovereign-sdk"
|
|
28
|
+
Changelog = "https://github.com/kenwalger/sovereign-sdk/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# packages/sovereign-sensor/src/sovereign_sensor/__init__.py
|
|
2
|
+
"""sovereign-sensor — Bare-metal Write-Side Custody enforcement for MicroPython sensor nodes.
|
|
3
|
+
|
|
4
|
+
Provides a Hardware Abstraction Layer that selects between on-chip hardware
|
|
5
|
+
cryptography and pure-Python software fallbacks at runtime, then seals each
|
|
6
|
+
sensor observation into a tamper-evident, versioned, minified JSON transmission
|
|
7
|
+
envelope with monotonic replay protection that survives hardware reboots.
|
|
8
|
+
Zero external dependencies; targets ESP32 and Raspberry Pi Pico via MicroPython.
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from .envelope import SovereignEnvelope
|
|
13
|
+
from .interface import SovereignCryptoDriver
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"SovereignCryptoDriver",
|
|
19
|
+
"SovereignEnvelope",
|
|
20
|
+
"bootstrap_sensor_node",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bootstrap_sensor_node(
|
|
25
|
+
node_id: str,
|
|
26
|
+
private_key_path: str,
|
|
27
|
+
sequence_file: str = ".sovereign_sequence",
|
|
28
|
+
) -> SovereignEnvelope:
|
|
29
|
+
"""Instantiate and configure a platform-appropriate sensor envelope factory.
|
|
30
|
+
|
|
31
|
+
Inspects ``sys.platform`` to route between the ESP32 hardware-accelerated
|
|
32
|
+
driver and the pure-Python software fallback. The selected driver is
|
|
33
|
+
initialized (loading key material from ``private_key_path``) before the
|
|
34
|
+
envelope is constructed, guaranteeing the signing subsystem is ready on
|
|
35
|
+
the first ``seal()`` call.
|
|
36
|
+
|
|
37
|
+
If the selected driver's ``initialize_hardware()`` raises
|
|
38
|
+
``NotImplementedError`` — indicating that hardware crypto acceleration is
|
|
39
|
+
still a skeleton placeholder — a warning is printed and the node falls back
|
|
40
|
+
to ``SoftwareFallbackDriver`` so that the sealing, sequencing, and VFS
|
|
41
|
+
serialization layers remain exercisable on the workbench before
|
|
42
|
+
register-level engineering is complete.
|
|
43
|
+
|
|
44
|
+
:param node_id: Immutable identifier string for this sensor node.
|
|
45
|
+
:type node_id: str
|
|
46
|
+
:param private_key_path: Filesystem path to the node's private signing key,
|
|
47
|
+
or ``SoftwareFallbackDriver.MOCK_KEY_SENTINEL`` for desktop testing.
|
|
48
|
+
:type private_key_path: str
|
|
49
|
+
:param sequence_file: VFS path used to persist the monotonic sequence
|
|
50
|
+
counter across reboots. Defaults to ``".sovereign_sequence"`` in the
|
|
51
|
+
current working directory.
|
|
52
|
+
:type sequence_file: str
|
|
53
|
+
:return: A fully configured ``SovereignEnvelope`` bound to the active driver.
|
|
54
|
+
:rtype: SovereignEnvelope
|
|
55
|
+
"""
|
|
56
|
+
platform: str = sys.platform.lower()
|
|
57
|
+
if "esp32" in platform:
|
|
58
|
+
from .drivers.esp32_hardware import ESP32HardwareDriver
|
|
59
|
+
driver: SovereignCryptoDriver = ESP32HardwareDriver(private_key_path)
|
|
60
|
+
else:
|
|
61
|
+
from .drivers.software_fallback import SoftwareFallbackDriver
|
|
62
|
+
driver = SoftwareFallbackDriver(private_key_path)
|
|
63
|
+
_hw_not_implemented: bool = False
|
|
64
|
+
try:
|
|
65
|
+
driver.initialize_hardware()
|
|
66
|
+
except NotImplementedError:
|
|
67
|
+
_hw_not_implemented = True
|
|
68
|
+
|
|
69
|
+
if _hw_not_implemented:
|
|
70
|
+
# Emit the warning outside the except block so the fallback initialization
|
|
71
|
+
# path carries no implicit exception context. Any exception raised by the
|
|
72
|
+
# SoftwareFallbackDriver below will surface with a clean traceback rather
|
|
73
|
+
# than chaining the original NotImplementedError, preventing doubled
|
|
74
|
+
# tracebacks from flooding the serial monitor on constrained MicroPython targets.
|
|
75
|
+
print(
|
|
76
|
+
"WARNING: Hardware crypto accelerator is not yet implemented "
|
|
77
|
+
"(pending low-level register engineering in the next sprint). "
|
|
78
|
+
"Falling back to SoftwareFallbackDriver for this node instance."
|
|
79
|
+
)
|
|
80
|
+
from .drivers.software_fallback import SoftwareFallbackDriver
|
|
81
|
+
driver = SoftwareFallbackDriver(private_key_path)
|
|
82
|
+
driver.initialize_hardware()
|
|
83
|
+
return SovereignEnvelope(node_id, driver, sequence_file=sequence_file)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# packages/sovereign-sensor/src/sovereign_sensor/drivers/esp32_hardware.py
|
|
2
|
+
"""ESP32 hardware-accelerated cryptographic signing driver.
|
|
3
|
+
|
|
4
|
+
Shell implementation targeting the ESP32's on-chip SHA and RSA/ECC
|
|
5
|
+
accelerator peripherals via the MicroPython ``machine`` and ``hashlib``
|
|
6
|
+
HAL bindings. Full low-level register engineering is deferred to the
|
|
7
|
+
next sprint; this module establishes the class contract and import
|
|
8
|
+
surface so the bootstrap router can bind it without modification.
|
|
9
|
+
"""
|
|
10
|
+
from ..interface import SovereignCryptoDriver
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ESP32HardwareDriver(SovereignCryptoDriver):
|
|
14
|
+
"""Hardware-accelerated signing driver for ESP32 targets.
|
|
15
|
+
|
|
16
|
+
On initialization, this driver will configure the on-chip crypto
|
|
17
|
+
accelerator and load key material from the eFuse or external flash
|
|
18
|
+
partition pointed to by ``private_key_path``.
|
|
19
|
+
|
|
20
|
+
:param private_key_path: Path to the private key in the ESP32 VFS
|
|
21
|
+
(e.g. ``/flash/keys/node.key``).
|
|
22
|
+
:type private_key_path: str
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, private_key_path: str) -> None:
|
|
26
|
+
self._key_path: str = private_key_path
|
|
27
|
+
self._initialized: bool = False
|
|
28
|
+
|
|
29
|
+
def initialize_hardware(self) -> None:
|
|
30
|
+
"""Configure the ESP32 hardware crypto accelerator subsystem.
|
|
31
|
+
|
|
32
|
+
:rtype: None
|
|
33
|
+
:raises NotImplementedError: Until low-level register engineering is complete.
|
|
34
|
+
"""
|
|
35
|
+
raise NotImplementedError(
|
|
36
|
+
"ESP32 hardware crypto drivers are pending low-level register engineering "
|
|
37
|
+
"in the next sprint."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def algorithm(self) -> str:
|
|
41
|
+
"""Return the canonical algorithm identifier for this driver.
|
|
42
|
+
|
|
43
|
+
Next sprint: finalize the identifier once the hardware signing
|
|
44
|
+
primitive (ECDSA P-256 via the ESP32 ECC accelerator) is confirmed.
|
|
45
|
+
|
|
46
|
+
:return: ``"ecdsa-p256"`` — forward-looking placeholder for the
|
|
47
|
+
ESP32 on-chip ECC accelerator signing primitive.
|
|
48
|
+
:rtype: str
|
|
49
|
+
"""
|
|
50
|
+
return "ecdsa-p256"
|
|
51
|
+
|
|
52
|
+
def sign(self, payload: bytes) -> bytes:
|
|
53
|
+
"""Produce a hardware-accelerated signature over ``payload``.
|
|
54
|
+
|
|
55
|
+
Next sprint: delegate to the ESP32 SHA/ECC hardware engine via
|
|
56
|
+
MicroPython ``hashlib`` acceleration bindings. Returns raw binary
|
|
57
|
+
signature bytes; hex encoding is the envelope layer's responsibility.
|
|
58
|
+
|
|
59
|
+
:param payload: Raw preimage bytes to authenticate.
|
|
60
|
+
:type payload: bytes
|
|
61
|
+
:return: Raw binary signature bytes (no encoding applied).
|
|
62
|
+
:rtype: bytes
|
|
63
|
+
:raises NotImplementedError: Until hardware signing is implemented.
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError(
|
|
66
|
+
"ESP32 hardware crypto signing is pending next-sprint implementation."
|
|
67
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# packages/sovereign-sensor/src/sovereign_sensor/drivers/software_fallback.py
|
|
2
|
+
"""Pure-Python software fallback signing driver.
|
|
3
|
+
|
|
4
|
+
Used on any platform that does not expose on-chip hardware crypto
|
|
5
|
+
acceleration (desktop CI, Raspberry Pi Pico without accelerator, etc.).
|
|
6
|
+
Produces a keyed HMAC-SHA256 digest of the preimage using key material
|
|
7
|
+
read from the VFS at initialization time. Raw bytes are returned;
|
|
8
|
+
hex encoding is the responsibility of the envelope serialization layer.
|
|
9
|
+
Not suitable for production custody chains; intended for structural
|
|
10
|
+
validation and desktop integration testing only.
|
|
11
|
+
"""
|
|
12
|
+
import hashlib
|
|
13
|
+
import hmac
|
|
14
|
+
|
|
15
|
+
from ..interface import SovereignCryptoDriver
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SoftwareFallbackDriver(SovereignCryptoDriver):
|
|
19
|
+
"""HMAC-SHA256–based software signing driver for non-accelerated platforms.
|
|
20
|
+
|
|
21
|
+
Key material is loaded from ``private_key_path`` during ``initialize_hardware()``.
|
|
22
|
+
If ``private_key_path`` equals ``MOCK_KEY_SENTINEL``, the fixed deterministic
|
|
23
|
+
``_MOCK_KEY`` stub is substituted so lightweight desktop tests can operate without
|
|
24
|
+
a provisioned key store. Any other path that cannot be opened raises
|
|
25
|
+
``RuntimeError`` immediately — there is no silent key substitution for
|
|
26
|
+
non-sentinel paths.
|
|
27
|
+
|
|
28
|
+
:param private_key_path: Filesystem path to the binary HMAC key material, or
|
|
29
|
+
``SoftwareFallbackDriver.MOCK_KEY_SENTINEL`` to opt into the deterministic
|
|
30
|
+
mock key for desktop testing.
|
|
31
|
+
:type private_key_path: str
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Explicit opt-in sentinel for desktop testing without a provisioned key store.
|
|
35
|
+
# Any path other than this that cannot be opened raises RuntimeError immediately.
|
|
36
|
+
MOCK_KEY_SENTINEL: str = "/mock/test_gateway.key"
|
|
37
|
+
|
|
38
|
+
# Fixed stub keyed exclusively to MOCK_KEY_SENTINEL paths. Never deploy in
|
|
39
|
+
# production — this value provides zero cryptographic uniqueness guarantees.
|
|
40
|
+
_MOCK_KEY: bytes = b"sovereign-sensor-mock-key-v1-do-not-use-in-production"
|
|
41
|
+
|
|
42
|
+
def __init__(self, private_key_path: str) -> None:
|
|
43
|
+
self._key_path: str = private_key_path
|
|
44
|
+
self._initialized: bool = False
|
|
45
|
+
self._secret_key: bytes = b""
|
|
46
|
+
|
|
47
|
+
def initialize_hardware(self) -> None:
|
|
48
|
+
"""Load HMAC key material from the VFS and mark the driver ready.
|
|
49
|
+
|
|
50
|
+
If ``private_key_path`` equals ``MOCK_KEY_SENTINEL``, substitutes the
|
|
51
|
+
fixed deterministic ``_MOCK_KEY`` stub so lightweight desktop tests operate
|
|
52
|
+
without a provisioned key store. For all other paths, opens the file in
|
|
53
|
+
read-binary mode, re-raises any ``OSError`` as ``RuntimeError``, and raises
|
|
54
|
+
``ValueError`` if the file exists but contains zero bytes — a zero-length
|
|
55
|
+
key produces an HMAC keyed with ``b""``, which is deterministic across all
|
|
56
|
+
nodes that share the same empty-file failure and provides no cryptographic
|
|
57
|
+
uniqueness.
|
|
58
|
+
|
|
59
|
+
:rtype: None
|
|
60
|
+
:raises RuntimeError: If ``private_key_path`` is not the mock sentinel and
|
|
61
|
+
the key file cannot be opened or read.
|
|
62
|
+
:raises ValueError: If the key file exists but contains zero bytes.
|
|
63
|
+
"""
|
|
64
|
+
if self._key_path == self.MOCK_KEY_SENTINEL:
|
|
65
|
+
self._secret_key = self._MOCK_KEY
|
|
66
|
+
self._initialized = True
|
|
67
|
+
return
|
|
68
|
+
try:
|
|
69
|
+
with open(self._key_path, "rb") as f:
|
|
70
|
+
key_bytes: bytes = f.read()
|
|
71
|
+
except OSError as exc:
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
f"Key material loading failed: '{self._key_path}' could not be read: {exc}"
|
|
74
|
+
) from exc
|
|
75
|
+
if not key_bytes:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Key material at '{self._key_path}' is empty; a non-zero-byte key is "
|
|
78
|
+
"strictly required for secure HMAC authentication."
|
|
79
|
+
)
|
|
80
|
+
self._secret_key = key_bytes
|
|
81
|
+
self._initialized = True
|
|
82
|
+
|
|
83
|
+
def algorithm(self) -> str:
|
|
84
|
+
"""Return the canonical algorithm identifier for this driver.
|
|
85
|
+
|
|
86
|
+
:return: ``"hmac-sha256"`` — the HMAC-SHA256 keyed signing primitive.
|
|
87
|
+
:rtype: str
|
|
88
|
+
"""
|
|
89
|
+
return "hmac-sha256"
|
|
90
|
+
|
|
91
|
+
def sign(self, payload: bytes) -> bytes:
|
|
92
|
+
"""Return the raw 32-byte HMAC-SHA256 digest of ``payload``.
|
|
93
|
+
|
|
94
|
+
The digest is keyed with the secret material loaded during
|
|
95
|
+
``initialize_hardware()``. Hex encoding is intentionally deferred to
|
|
96
|
+
the envelope serialization layer so that this driver's return type is
|
|
97
|
+
identical to the raw binary output expected from hardware accelerators.
|
|
98
|
+
|
|
99
|
+
:param payload: Raw preimage bytes to authenticate.
|
|
100
|
+
:type payload: bytes
|
|
101
|
+
:return: Raw 32-byte HMAC-SHA256 digest.
|
|
102
|
+
:rtype: bytes
|
|
103
|
+
:raises RuntimeError: If ``initialize_hardware()`` has not been called
|
|
104
|
+
prior to this invocation.
|
|
105
|
+
"""
|
|
106
|
+
if not self._initialized:
|
|
107
|
+
raise RuntimeError(
|
|
108
|
+
"Driver must be initialized via initialize_hardware() before generating signatures."
|
|
109
|
+
)
|
|
110
|
+
return hmac.new(self._secret_key, payload, hashlib.sha256).digest()
|