pamoja-update 0.1.18__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,13 @@
1
+ # Native extensions are built per platform, not committed.
2
+ *.so
3
+ *.pyd
4
+ *.dylib
5
+
6
+ # Build and packaging output.
7
+ dist/
8
+ wheels/
9
+ target/
10
+
11
+ # Local virtual environments.
12
+ .venv/
13
+ venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthony Wiedman
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,127 @@
1
+ Metadata-Version: 2.5
2
+ Name: pamoja-update
3
+ Version: 0.1.18
4
+ Summary: Signed firmware manifests, streaming image verification, and A/B slots that fall back on their own.
5
+ Project-URL: Repository, https://github.com/molexxxx/pamoja
6
+ Project-URL: Documentation, https://pamoja.molex.cloud/docs/guides/update.html
7
+ Author: molexxxx
8
+ License: MIT
9
+ License-File: LICENSE-MIT
10
+ Keywords: iot,pamoja,robotics,update
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: pamoja-native==0.1.18
17
+ Requires-Dist: pamoja-security==0.1.18
18
+ Description-Content-Type: text/markdown
19
+
20
+ # pamoja-update
21
+
22
+ Signed firmware manifests, streaming image verification, and A/B slots that fall back on their own. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
23
+
24
+ [![read the guide](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-guide.svg)](https://pamoja.molex.cloud/docs/guides/update.html)
25
+ [![documentation](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-docs.svg)](https://pamoja.molex.cloud/docs/)
26
+ [![API reference](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-api.svg)](https://pamoja.molex.cloud/docs/reference/python/pamoja/update.html)
27
+
28
+ ## Install
29
+
30
+ ```sh
31
+ pip install pamoja-update
32
+ ```
33
+
34
+ ```python
35
+ from pamoja import update
36
+ ```
37
+
38
+ This pulls in `pamoja-native`, the compiled engine, and `pamoja-security`. `pip install pamoja` is the whole framework in one package.
39
+
40
+ ## Example
41
+
42
+ The script the test suite runs, spliced here as it ran.
43
+
44
+ From [`bindings/python/guides/update.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/update.py):
45
+
46
+ ```python
47
+ from pamoja.core import PamojaError
48
+ from pamoja.security import DeviceIdentity
49
+ from pamoja.update import (
50
+ BootAction,
51
+ Manifest,
52
+ SlotState,
53
+ Updater,
54
+ image_digest,
55
+ sign_manifest,
56
+ verify_envelope,
57
+ )
58
+
59
+ # The publisher's key signs releases; devices in the field are anchored to its public half
60
+ # and will take firmware from nobody else.
61
+ publisher = DeviceIdentity.from_seed(bytes([7]) * 32)
62
+ vendor = bytes([0x0A]) * 16
63
+ device_class = bytes([0x0B]) * 16
64
+
65
+ # The release. A manifest says who the image is for, which slot it belongs in, how big it
66
+ # is and what it hashes to; nothing about the image itself is taken on trust.
67
+ image = b"firmware for a flow meter, version two"
68
+ manifest = Manifest(
69
+ sequence=2,
70
+ vendor_id=vendor,
71
+ class_id=device_class,
72
+ storage=1,
73
+ digest=image_digest(image),
74
+ size=len(image),
75
+ )
76
+ envelope = sign_manifest(manifest, publisher)
77
+ print(f"published sequence {manifest.sequence} in a {len(envelope)}-byte envelope")
78
+
79
+ # On the device. It checks the envelope against the key it was anchored to before it
80
+ # accepts a single byte of the image.
81
+ opened = verify_envelope(envelope, publisher.public_key)
82
+ print(f"accepted a release for slot {opened.storage}")
83
+
84
+ # It left the factory running sequence 1 from slot 0, so the release goes to the spare slot
85
+ # and the image it is running stays where it is.
86
+ fleet = Updater(vendor, device_class, publisher.public_key, 2, 4096)
87
+ fleet.provision(0, 1)
88
+ fleet.begin(envelope)
89
+ for at in range(0, len(image), 16):
90
+ fleet.write(image[at : at + 16])
91
+ print(f"staged {fleet.progress().written} of {len(image)} bytes")
92
+ slot = fleet.finish()
93
+ print(f"written to slot {slot}, leaving the running image alone")
94
+
95
+ # The first boot into a new image is a trial. It reverts on the next boot unless the device
96
+ # confirms that it came up, which is what makes a bad release survivable.
97
+ print(f"booting {fleet.on_boot().action}")
98
+ fleet.confirm()
99
+ print(f"confirmed slot {slot} is now {fleet.slot_record(slot).state}")
100
+
101
+ # The same release signed by a key this device is not anchored to gets nowhere.
102
+ impostor = DeviceIdentity.from_seed(bytes([90]) * 32)
103
+ try:
104
+ fleet.stage(sign_manifest(manifest, impostor), image)
105
+ print("a forged release was accepted, which should never happen")
106
+ except PamojaError as error:
107
+ print(f"forged refused: {error}")
108
+ ```
109
+
110
+ ## The same capability in every language
111
+
112
+ | Language | Package | Reference |
113
+ | --- | --- | --- |
114
+ | Rust | [`pamoja-update`](https://crates.io/crates/pamoja-update) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_update/index.html), [docs.rs](https://docs.rs/pamoja-update), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-update) |
115
+ | TypeScript | [`@pamoja/update`](https://www.npmjs.com/package/@pamoja/update) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_update.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-update) |
116
+ | Python | [`pamoja-update`](https://pypi.org/project/pamoja-update/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/update.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-update) |
117
+ | C# | [`Pamoja.Update`](https://www.nuget.org/packages/Pamoja.Update) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Update.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-update) |
118
+
119
+ ## Documentation
120
+
121
+ - [`pamoja.update` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/update.html), every class and function in this module.
122
+ - [The Signed updates guide](https://pamoja.molex.cloud/docs/guides/update.html), with the same example in Rust, TypeScript, and C#.
123
+ - [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,108 @@
1
+ # pamoja-update
2
+
3
+ Signed firmware manifests, streaming image verification, and A/B slots that fall back on their own. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
4
+
5
+ [![read the guide](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-guide.svg)](https://pamoja.molex.cloud/docs/guides/update.html)
6
+ [![documentation](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-docs.svg)](https://pamoja.molex.cloud/docs/)
7
+ [![API reference](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-api.svg)](https://pamoja.molex.cloud/docs/reference/python/pamoja/update.html)
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pip install pamoja-update
13
+ ```
14
+
15
+ ```python
16
+ from pamoja import update
17
+ ```
18
+
19
+ This pulls in `pamoja-native`, the compiled engine, and `pamoja-security`. `pip install pamoja` is the whole framework in one package.
20
+
21
+ ## Example
22
+
23
+ The script the test suite runs, spliced here as it ran.
24
+
25
+ From [`bindings/python/guides/update.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/update.py):
26
+
27
+ ```python
28
+ from pamoja.core import PamojaError
29
+ from pamoja.security import DeviceIdentity
30
+ from pamoja.update import (
31
+ BootAction,
32
+ Manifest,
33
+ SlotState,
34
+ Updater,
35
+ image_digest,
36
+ sign_manifest,
37
+ verify_envelope,
38
+ )
39
+
40
+ # The publisher's key signs releases; devices in the field are anchored to its public half
41
+ # and will take firmware from nobody else.
42
+ publisher = DeviceIdentity.from_seed(bytes([7]) * 32)
43
+ vendor = bytes([0x0A]) * 16
44
+ device_class = bytes([0x0B]) * 16
45
+
46
+ # The release. A manifest says who the image is for, which slot it belongs in, how big it
47
+ # is and what it hashes to; nothing about the image itself is taken on trust.
48
+ image = b"firmware for a flow meter, version two"
49
+ manifest = Manifest(
50
+ sequence=2,
51
+ vendor_id=vendor,
52
+ class_id=device_class,
53
+ storage=1,
54
+ digest=image_digest(image),
55
+ size=len(image),
56
+ )
57
+ envelope = sign_manifest(manifest, publisher)
58
+ print(f"published sequence {manifest.sequence} in a {len(envelope)}-byte envelope")
59
+
60
+ # On the device. It checks the envelope against the key it was anchored to before it
61
+ # accepts a single byte of the image.
62
+ opened = verify_envelope(envelope, publisher.public_key)
63
+ print(f"accepted a release for slot {opened.storage}")
64
+
65
+ # It left the factory running sequence 1 from slot 0, so the release goes to the spare slot
66
+ # and the image it is running stays where it is.
67
+ fleet = Updater(vendor, device_class, publisher.public_key, 2, 4096)
68
+ fleet.provision(0, 1)
69
+ fleet.begin(envelope)
70
+ for at in range(0, len(image), 16):
71
+ fleet.write(image[at : at + 16])
72
+ print(f"staged {fleet.progress().written} of {len(image)} bytes")
73
+ slot = fleet.finish()
74
+ print(f"written to slot {slot}, leaving the running image alone")
75
+
76
+ # The first boot into a new image is a trial. It reverts on the next boot unless the device
77
+ # confirms that it came up, which is what makes a bad release survivable.
78
+ print(f"booting {fleet.on_boot().action}")
79
+ fleet.confirm()
80
+ print(f"confirmed slot {slot} is now {fleet.slot_record(slot).state}")
81
+
82
+ # The same release signed by a key this device is not anchored to gets nowhere.
83
+ impostor = DeviceIdentity.from_seed(bytes([90]) * 32)
84
+ try:
85
+ fleet.stage(sign_manifest(manifest, impostor), image)
86
+ print("a forged release was accepted, which should never happen")
87
+ except PamojaError as error:
88
+ print(f"forged refused: {error}")
89
+ ```
90
+
91
+ ## The same capability in every language
92
+
93
+ | Language | Package | Reference |
94
+ | --- | --- | --- |
95
+ | Rust | [`pamoja-update`](https://crates.io/crates/pamoja-update) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_update/index.html), [docs.rs](https://docs.rs/pamoja-update), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-update) |
96
+ | TypeScript | [`@pamoja/update`](https://www.npmjs.com/package/@pamoja/update) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_update.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-update) |
97
+ | Python | [`pamoja-update`](https://pypi.org/project/pamoja-update/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/update.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-update) |
98
+ | C# | [`Pamoja.Update`](https://www.nuget.org/packages/Pamoja.Update) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Update.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-update) |
99
+
100
+ ## Documentation
101
+
102
+ - [`pamoja.update` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/update.html), every class and function in this module.
103
+ - [The Signed updates guide](https://pamoja.molex.cloud/docs/guides/update.html), with the same example in Rust, TypeScript, and C#.
104
+ - [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,126 @@
1
+ """Idiomatic signed-update facade.
2
+
3
+ A device that cannot be fixed in the field is a device that has to be visited,
4
+ and some of them are a day's travel away. Signed updates make that a network
5
+ operation instead: a release carries a manifest naming who it is for and what it
6
+ hashes to, a device refuses anything not signed by the key it trusts, and an
7
+ image that fails to confirm itself is rolled back to the one that worked.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import enum
13
+
14
+ from pamoja._native import (
15
+ BootDecision,
16
+ Delegation,
17
+ ImageVerifier,
18
+ Manifest,
19
+ Progress,
20
+ SlotRecord,
21
+ Updater,
22
+ decode_manifest,
23
+ encode_manifest,
24
+ envelope_body,
25
+ open_delegation,
26
+ verify_envelope,
27
+ )
28
+ from pamoja._native import sign_delegation as _sign_delegation
29
+ from pamoja._native import image_digest as _image_digest
30
+ from pamoja._native import sign_manifest as _sign_manifest
31
+ from pamoja._native import update_format_raw as _format_raw
32
+ from pamoja._native import update_structure_version as _structure_version
33
+ from pamoja.security import DeviceIdentity
34
+
35
+ __all__ = [
36
+ "FORMAT_RAW",
37
+ "STRUCTURE_VERSION",
38
+ "BootAction",
39
+ "BootDecision",
40
+ "Delegation",
41
+ "ImageVerifier",
42
+ "Manifest",
43
+ "Progress",
44
+ "SlotRecord",
45
+ "SlotState",
46
+ "Updater",
47
+ "decode_manifest",
48
+ "encode_manifest",
49
+ "envelope_body",
50
+ "open_delegation",
51
+ "sign_delegation",
52
+ "image_digest",
53
+ "sign_manifest",
54
+ "verify_envelope",
55
+ ]
56
+
57
+ #: The manifest structure version this build writes.
58
+ STRUCTURE_VERSION = _structure_version()
59
+
60
+ #: The payload format meaning the payload is the image itself, byte for byte.
61
+ FORMAT_RAW = _format_raw()
62
+
63
+
64
+ class SlotState(str, enum.Enum):
65
+ """What a device believes about one slot."""
66
+
67
+ #: Nothing has been written here.
68
+ EMPTY = "Empty"
69
+ #: An image is arriving, and ``written`` says how much of it has.
70
+ RECEIVING = "Receiving"
71
+ #: A complete image that matched its manifest, not yet tried.
72
+ STAGED = "Staged"
73
+ #: Being tried for the first time; it reverts unless it confirms.
74
+ PENDING = "Pending"
75
+ #: Tried and confirmed working.
76
+ CONFIRMED = "Confirmed"
77
+ #: Tried and did not confirm, so it will not be tried again.
78
+ FAILED = "Failed"
79
+
80
+
81
+ class BootAction(str, enum.Enum):
82
+ """What a bootloader should do with what it found."""
83
+
84
+ #: Nothing new to try; run the confirmed image.
85
+ CONFIRMED = "Confirmed"
86
+ #: A staged image is being tried for the first time.
87
+ TRYING = "Trying"
88
+ #: A pending image never confirmed, so it was failed.
89
+ REVERTED = "Reverted"
90
+
91
+
92
+ def image_digest(image: bytes) -> bytes:
93
+ """Hash a complete image, for a publisher filling in a manifest.
94
+
95
+ The manifest commits to a SHA-256 over the image, and this is that hash, so a
96
+ publisher does not need a hashing library of its own just to name the image it
97
+ is releasing.
98
+
99
+ :param image: The complete image the release carries.
100
+ :returns: The 32-byte digest to put in a :class:`Manifest`.
101
+ """
102
+ return _image_digest(bytes(image))
103
+
104
+
105
+ def sign_manifest(manifest: Manifest, author: DeviceIdentity) -> bytes:
106
+ """Sign a manifest into the envelope that is offered to a device.
107
+
108
+ :param manifest: What the release says about itself.
109
+ :param author: The identity signing the release.
110
+ :returns: The signed envelope.
111
+ """
112
+ return _sign_manifest(manifest, DeviceIdentity.native(author))
113
+
114
+
115
+ def sign_delegation(delegation: Delegation, anchor: DeviceIdentity) -> bytes:
116
+ """Sign a delegation, naming a release key the anchor stands behind.
117
+
118
+ Keeping the anchor offline and rotating a release key under it is the
119
+ arrangement to prefer, because the key that signs day to day is the one most
120
+ likely to be stolen.
121
+
122
+ :param delegation: The statement to sign.
123
+ :param anchor: The anchor identity, which is the root of the trust.
124
+ :returns: The signed delegation envelope.
125
+ """
126
+ return _sign_delegation(delegation, DeviceIdentity.native(anchor))
File without changes
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pamoja-update"
7
+ version = "0.1.18"
8
+ description = "Signed firmware manifests, streaming image verification, and A/B slots that fall back on their own."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ license-files = ["LICENSE-MIT"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "molexxxx" }]
14
+ keywords = ["pamoja", "iot", "robotics", "update"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "pamoja-native==0.1.18",
23
+ "pamoja-security==0.1.18",
24
+ ]
25
+
26
+ [project.urls]
27
+ Repository = "https://github.com/molexxxx/pamoja"
28
+ Documentation = "https://pamoja.molex.cloud/docs/guides/update.html"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["pamoja"]