sovereign-sdk-sensor 1.3.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_sdk_sensor-1.3.0/PKG-INFO +156 -0
- sovereign_sdk_sensor-1.3.0/README.md +136 -0
- sovereign_sdk_sensor-1.3.0/pyproject.toml +31 -0
- sovereign_sdk_sensor-1.3.0/setup.cfg +4 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sdk_sensor.egg-info/PKG-INFO +156 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sdk_sensor.egg-info/SOURCES.txt +13 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sdk_sensor.egg-info/dependency_links.txt +1 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sdk_sensor.egg-info/top_level.txt +1 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sensor/__init__.py +83 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sensor/drivers/__init__.py +2 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sensor/drivers/esp32_hardware.py +67 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sensor/drivers/software_fallback.py +110 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sensor/envelope.py +165 -0
- sovereign_sdk_sensor-1.3.0/src/sovereign_sensor/interface.py +63 -0
- sovereign_sdk_sensor-1.3.0/tests/test_sensor.py +692 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sovereign-sdk-sensor
|
|
3
|
+
Version: 1.3.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-sdk-sensor"
|
|
7
|
+
version = "1.3.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,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sovereign-sdk-sensor
|
|
3
|
+
Version: 1.3.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,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/sovereign_sdk_sensor.egg-info/PKG-INFO
|
|
4
|
+
src/sovereign_sdk_sensor.egg-info/SOURCES.txt
|
|
5
|
+
src/sovereign_sdk_sensor.egg-info/dependency_links.txt
|
|
6
|
+
src/sovereign_sdk_sensor.egg-info/top_level.txt
|
|
7
|
+
src/sovereign_sensor/__init__.py
|
|
8
|
+
src/sovereign_sensor/envelope.py
|
|
9
|
+
src/sovereign_sensor/interface.py
|
|
10
|
+
src/sovereign_sensor/drivers/__init__.py
|
|
11
|
+
src/sovereign_sensor/drivers/esp32_hardware.py
|
|
12
|
+
src/sovereign_sensor/drivers/software_fallback.py
|
|
13
|
+
tests/test_sensor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sovereign_sensor
|
|
@@ -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__ = "1.3.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
|
+
)
|