pamoja.update
Idiomatic signed-update facade.
A device that cannot be fixed in the field is a device that has to be visited, and some of them are a day's travel away. Signed updates make that a network operation instead: a release carries a manifest naming who it is for and what it hashes to, a device refuses anything not signed by the key it trusts, and an image that fails to confirm itself is rolled back to the one that worked.
1"""Idiomatic signed-update facade. 2 3A device that cannot be fixed in the field is a device that has to be visited, 4and some of them are a day's travel away. Signed updates make that a network 5operation instead: a release carries a manifest naming who it is for and what it 6hashes to, a device refuses anything not signed by the key it trusts, and an 7image that fails to confirm itself is rolled back to the one that worked. 8""" 9 10from __future__ import annotations 11 12import enum 13 14from 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) 28from pamoja._native import frame_update_block as _frame_block 29from pamoja._native import sign_delegation as _sign_delegation 30from pamoja._native import split_update_block as _split_block 31from pamoja._native import update_block_descriptor as _block_descriptor 32from pamoja._native import image_digest as _image_digest 33from pamoja._native import sign_manifest as _sign_manifest 34from pamoja._native import update_format_raw as _format_raw 35from pamoja._native import update_structure_version as _structure_version 36from pamoja.security import DeviceIdentity 37 38__all__ = [ 39 "BLOCK_DESCRIPTOR", 40 "FORMAT_RAW", 41 "STRUCTURE_VERSION", 42 "BootAction", 43 "BootDecision", 44 "Delegation", 45 "ImageVerifier", 46 "Manifest", 47 "Progress", 48 "SlotRecord", 49 "SlotState", 50 "Updater", 51 "decode_manifest", 52 "encode_manifest", 53 "envelope_body", 54 "open_delegation", 55 "sign_delegation", 56 "frame_block", 57 "image_digest", 58 "sign_manifest", 59 "split_block", 60 "verify_envelope", 61] 62 63#: The manifest structure version this build writes. 64STRUCTURE_VERSION = _structure_version() 65 66#: The payload format meaning the payload is the image itself, byte for byte. 67FORMAT_RAW = _format_raw() 68 69 70class SlotState(str, enum.Enum): 71 """What a device believes about one slot.""" 72 73 #: Nothing has been written here. 74 EMPTY = "Empty" 75 #: An image is arriving, and ``written`` says how much of it has. 76 RECEIVING = "Receiving" 77 #: A complete image that matched its manifest, not yet tried. 78 STAGED = "Staged" 79 #: Being tried for the first time; it reverts unless it confirms. 80 PENDING = "Pending" 81 #: Tried and confirmed working. 82 CONFIRMED = "Confirmed" 83 #: Tried and did not confirm, so it will not be tried again. 84 FAILED = "Failed" 85 86 87class BootAction(str, enum.Enum): 88 """What a bootloader should do with what it found.""" 89 90 #: Nothing new to try; run the confirmed image. 91 CONFIRMED = "Confirmed" 92 #: A staged image is being tried for the first time. 93 TRYING = "Trying" 94 #: A pending image never confirmed, so it was failed. 95 REVERTED = "Reverted" 96 97 98def image_digest(image: bytes) -> bytes: 99 """Hash a complete image, for a publisher filling in a manifest. 100 101 The manifest commits to a SHA-256 over the image, and this is that hash, so a 102 publisher does not need a hashing library of its own just to name the image it 103 is releasing. 104 105 :param image: The complete image the release carries. 106 :returns: The 32-byte digest to put in a :class:`Manifest`. 107 """ 108 return _image_digest(bytes(image)) 109 110 111def sign_manifest(manifest: Manifest, author: DeviceIdentity) -> bytes: 112 """Sign a manifest into the envelope that is offered to a device. 113 114 :param manifest: What the release says about itself. 115 :param author: The identity signing the release. 116 :returns: The signed envelope. 117 """ 118 return _sign_manifest(manifest, DeviceIdentity.native(author)) 119 120 121def sign_delegation(delegation: Delegation, anchor: DeviceIdentity) -> bytes: 122 """Sign a delegation, naming a release key the anchor stands behind. 123 124 Keeping the anchor offline and rotating a release key under it is the 125 arrangement to prefer, because the key that signs day to day is the one most 126 likely to be stolen. 127 128 :param delegation: The statement to sign. 129 :param anchor: The anchor identity, which is the root of the trust. 130 :returns: The signed delegation envelope. 131 """ 132 return _sign_delegation(delegation, DeviceIdentity.native(anchor)) 133 134 135#: What a transport calls a block carrying a signed update, for a field that names the 136#: convention. 137BLOCK_DESCRIPTOR = _block_descriptor() 138 139 140def frame_block(envelope: bytes, image: bytes) -> bytes: 141 """Write a signed update into one block, for a transport that moves blocks. 142 143 The block is the signed manifest and the image behind a header that says where each 144 begins. Nothing in the header is trusted: every rule that decides whether the image 145 runs is still the manifest's. 146 147 :param envelope: The signed manifest. 148 :param image: The image it describes. 149 :returns: The block to hand to a transport. 150 """ 151 return _frame_block(bytes(envelope), bytes(image)) 152 153 154def split_block(block: bytes) -> tuple[bytes, bytes]: 155 """Read a block back into the signed manifest and the image. 156 157 :param block: The block as it arrived. 158 :returns: The signed manifest and the image. 159 :raises Refusal: If the block does not carry this convention's header. 160 """ 161 return _split_block(bytes(block))
88class BootAction(str, enum.Enum): 89 """What a bootloader should do with what it found.""" 90 91 #: Nothing new to try; run the confirmed image. 92 CONFIRMED = "Confirmed" 93 #: A staged image is being tried for the first time. 94 TRYING = "Trying" 95 #: A pending image never confirmed, so it was failed. 96 REVERTED = "Reverted"
What a bootloader should do with what it found.
The decision a device made at boot, already recorded before it was returned.
A statement, signed by the anchor, that a second key may sign releases.
When the delegation stops being honored, in seconds since the Unix
epoch, or 0 to never expire.
Hashes an image as it arrives and settles it against its manifest.
What a release says about itself, and what a device checks it against.
How much of an image has arrived.
The record a device keeps about one slot, durable across a reboot.
71class SlotState(str, enum.Enum): 72 """What a device believes about one slot.""" 73 74 #: Nothing has been written here. 75 EMPTY = "Empty" 76 #: An image is arriving, and ``written`` says how much of it has. 77 RECEIVING = "Receiving" 78 #: A complete image that matched its manifest, not yet tried. 79 STAGED = "Staged" 80 #: Being tried for the first time; it reverts unless it confirms. 81 PENDING = "Pending" 82 #: Tried and confirmed working. 83 CONFIRMED = "Confirmed" 84 #: Tried and did not confirm, so it will not be tried again. 85 FAILED = "Failed"
What a device believes about one slot.
A device's slots, and the rules applied to what is offered for them.
Records that a slot already holds a confirmed image at a sequence number.
This is how a device that shipped with firmware says what it is running, so the rollback rule has something to compare against.
Adopts a delegation, so releases signed by the key it names are accepted.
Checks a manifest and stages an image that is already held whole, returning the slot it went into.
Checks a manifest and opens the slot it names for a transfer in pieces.
Every check that can be made without the image runs here, so a release
that is not for this device, would roll it back, or does not fit is
refused before a byte of it is accepted. The envelope is remembered until
finish, and every call after this one checks it again, so a release
overtaken or expired while it arrives stops arriving. The hash of what has
arrived is carried from one call to the next, so an image taken in many
small pieces costs no more than one taken whole.
Finishes an opened image and marks the slot bootable if it matched, returning the slot now holding it.
Decides what to run, and records that decision before returning it.
Call this once per boot, before jumping to an image. A staged image becomes pending here, so a device that resets before confirming reverts on the next call rather than trying a broken image forever.
Reads a manifest body back from its bytes.
This reads what a manifest claims; it proves nothing about who wrote it. Use
verify_envelope to read one whose signature has been checked.
Encodes the body of a manifest, which is the part a signature covers.
Copies out the signed body of an envelope, without checking the signature.
This is what a gateway relays onward unchanged.
Opens a signed delegation against the anchor that should have signed it.
122def sign_delegation(delegation: Delegation, anchor: DeviceIdentity) -> bytes: 123 """Sign a delegation, naming a release key the anchor stands behind. 124 125 Keeping the anchor offline and rotating a release key under it is the 126 arrangement to prefer, because the key that signs day to day is the one most 127 likely to be stolen. 128 129 :param delegation: The statement to sign. 130 :param anchor: The anchor identity, which is the root of the trust. 131 :returns: The signed delegation envelope. 132 """ 133 return _sign_delegation(delegation, DeviceIdentity.native(anchor))
Sign a delegation, naming a release key the anchor stands behind.
Keeping the anchor offline and rotating a release key under it is the arrangement to prefer, because the key that signs day to day is the one most likely to be stolen.
Parameters
- delegation: The statement to sign.
- anchor: The anchor identity, which is the root of the trust. :returns: The signed delegation envelope.
141def frame_block(envelope: bytes, image: bytes) -> bytes: 142 """Write a signed update into one block, for a transport that moves blocks. 143 144 The block is the signed manifest and the image behind a header that says where each 145 begins. Nothing in the header is trusted: every rule that decides whether the image 146 runs is still the manifest's. 147 148 :param envelope: The signed manifest. 149 :param image: The image it describes. 150 :returns: The block to hand to a transport. 151 """ 152 return _frame_block(bytes(envelope), bytes(image))
Write a signed update into one block, for a transport that moves blocks.
The block is the signed manifest and the image behind a header that says where each begins. Nothing in the header is trusted: every rule that decides whether the image runs is still the manifest's.
Parameters
- envelope: The signed manifest.
- image: The image it describes. :returns: The block to hand to a transport.
99def image_digest(image: bytes) -> bytes: 100 """Hash a complete image, for a publisher filling in a manifest. 101 102 The manifest commits to a SHA-256 over the image, and this is that hash, so a 103 publisher does not need a hashing library of its own just to name the image it 104 is releasing. 105 106 :param image: The complete image the release carries. 107 :returns: The 32-byte digest to put in a :class:`Manifest`. 108 """ 109 return _image_digest(bytes(image))
Hash a complete image, for a publisher filling in a manifest.
The manifest commits to a SHA-256 over the image, and this is that hash, so a publisher does not need a hashing library of its own just to name the image it is releasing.
Parameters
- image: The complete image the release carries.
:returns: The 32-byte digest to put in a
Manifest.
112def sign_manifest(manifest: Manifest, author: DeviceIdentity) -> bytes: 113 """Sign a manifest into the envelope that is offered to a device. 114 115 :param manifest: What the release says about itself. 116 :param author: The identity signing the release. 117 :returns: The signed envelope. 118 """ 119 return _sign_manifest(manifest, DeviceIdentity.native(author))
Sign a manifest into the envelope that is offered to a device.
Parameters
- manifest: What the release says about itself.
- author: The identity signing the release. :returns: The signed envelope.
155def split_block(block: bytes) -> tuple[bytes, bytes]: 156 """Read a block back into the signed manifest and the image. 157 158 :param block: The block as it arrived. 159 :returns: The signed manifest and the image. 160 :raises Refusal: If the block does not carry this convention's header. 161 """ 162 return _split_block(bytes(block))
Read a block back into the signed manifest and the image.
Parameters
- block: The block as it arrived. :returns: The signed manifest and the image.
Raises
- Refusal: If the block does not carry this convention's header.
Verifies an envelope against a key and reads the manifest inside it.
Raises when the signature is not from that key.