Signing a packbeam, and checking one.
Packbeam has no signature of its own: atomvm_packbeam writes none and
avmpack_is_valid compares the 24 byte magic and nothing else. This adds one
as a convention, in the shape fwup uses -- the signature travels inside the
archive, as a sibling of what it signs.
magic entry, entry, ... the archive as built nerves_hub/signature (data) appended terminator
The signed range is every byte before the signature entry begins. That is the one rule a signature scheme has to get right: the signed bytes must exclude the signature itself, or it cannot be checked without knowing what it was.
Signing is therefore a pure append. Nothing before the signature moves, so both ends compute the same range without needing to agree on anything else.
The signature is a data entry, the same class as priv/application.bin,
which AtomVM already skips when it looks for code. A signed archive boots on
a stock AtomVM that knows nothing about signing, and on a device that does
not verify. Signing can only ever add a check, never take a device away.
Reading it is unaffected too: nh_packbeam:byte_length/1 walks past the
entry to the terminator like any other.
AtomVM may define its own signing one day. The entry is named under
nerves_hub/ so it cannot collide with whatever that turns out to be, and
the payload leads with a magic and a version so a second scheme is additive
rather than a breaking change.
<<"NH1", Version:8, Signature:64/binary>>Ed25519, because that is what an organization's fwup keys already are -- a NervesHub organization can sign a packbeam with the key it already has.
| available/0 | Whether this VM can check an Ed25519 signature at all. |
| entry_name/0 | The entry a signature is carried in. |
| private_key/1 | Read a signing key, as the seed sign/2 wants. |
| public_key/1 | Normalise a configured public key to the 32 raw bytes. |
| sign/2 | Sign an archive with an Ed25519 private key, returning a signed archive. |
| signed_range/1 | The bytes a signature covers, and the signature payload itself. |
| strip/1 | The archive without its signature, as it was before signing. |
| verify/2 | Check an archive against a list of Ed25519 public keys. |
| verify_parts/3 | Check a signature against a range that has already been located. |
| version/0 |
available() -> boolean()
Whether this VM can check an Ed25519 signature at all.
AtomVM's Ed25519 lives behind AVM_USE_LIBSODIUM, which is **off** in a
default build: crypto:verify(eddsa, ...) then raises rather than returning
false. Firmware built for signature checking has to be running an AtomVM
compiled with -DAVM_USE_LIBSODIUM=ON.
Asked before checking, so that "this VM cannot verify" is never reported as "this firmware is not what it claims to be". They are different problems and they need different fixes, and confusing them sends someone hunting for a tampered archive that does not exist.
Probed with a signature that cannot pass, so a supported VM answers false without raising and an unsupported one raises.entry_name() -> binary()
The entry a signature is carried in.
private_key(Key::binary() | string()) -> {ok, binary()} | {error, term()}
Read a signing key, as the seed sign/2 wants.
Takes fwup's 64 byte secret key or a bare 32 byte seed, base64 or raw. An fwup private key is a seed followed by its public key, which is libsodium's layout, and only the seed is used to sign.
The counterpart ofpublic_key/1, and deliberately not the same function.
That one refuses a 64 byte key, because what it parses ends up compiled into
firmware and quietly accepting a private key there would put a signing key
on every device in the fleet. This one is for a build machine, where a
private key is the point.
public_key(Key::binary() | string()) -> {ok, binary()} | {error, term()}
Normalise a configured public key to the 32 raw bytes.
Accepts the base64 an fwup .pub file holds, or the raw bytes.
sign(Archive::binary(), PrivateKey::binary()) -> {ok, binary()} | {error, term()}
Sign an archive with an Ed25519 private key, returning a signed archive.
Signing an already signed archive replaces the signature rather than nesting one inside another, so re-signing after a key rotation is the same operation.signed_range(Archive::binary()) -> {ok, binary(), binary()} | none | {error, term()}
The bytes a signature covers, and the signature payload itself.
none when the archive carries no signature entry.
strip(Archive::binary()) -> {ok, binary()} | {error, term()}
The archive without its signature, as it was before signing.
verify(Archive::binary(), PublicKeys::[binary()]) -> {ok, binary()} | {error, term()}
Check an archive against a list of Ed25519 public keys.
Returns the key that verified, so a caller can record which one it was.{error, unsigned} and {error, invalid_signature} are deliberately
different: an archive nobody signed and an archive signed by someone else are
not the same problem.
verify_parts(Signed::binary(), Payload::binary(), PublicKeys::[binary()]) -> {ok, binary()} | {error, term()}
Check a signature against a range that has already been located.
Separate from verify/2 because a device does not have the archive in
memory. It maps the signed range out of flash with esp:partition_mmap/3,
which costs a few words of heap rather than the size of the archive, and
hands the mapped binary straight to this.
version() -> pos_integer()
Generated by EDoc