3.2 Signature Format
3.2.1 The blob #
A signature is a fixed 3310-byte blob, identical wherever it is stored:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | Version. MUST be 0x01. |
| 1 | 3309 | Raw ML-DSA-65 signature. |
Total 3310 bytes exactly. There is no length field, no algorithm
identifier, no key identifier, no timestamp and no padding. A blob of
any other length MUST be rejected, and so MUST a version byte other
than 0x01.
Signers MUST NOT emit any other version. A verifier encountering one MUST treat the file as unsigned rather than attempting a fallback interpretation.
3.2.2 Storage #
Two locations are defined. A verifier MUST consult them in this order.
3.2.2.1 ELF section #
An ELF binary SHOULD carry its signature in a section named exactly
.peios.sig. The name comparison covers all eleven bytes including
the terminating NUL, so a longer name having .peios.sig as a prefix
MUST NOT match.
The section's type MUST be SHT_PROGBITS and its size MUST be exactly
3310. The range [sh_offset, sh_offset + sh_size) MUST lie entirely
within the file.
The containing file MUST be ELFCLASS64, MUST be ELFDATA2LSB, and
MUST carry EV_CURRENT in e_ident[EI_VERSION]. e_shentsize MUST
equal 64. e_shstrndx MUST NOT be SHN_UNDEF and MUST be less than
e_shnum. The section header table and the section-name string table
MUST both lie entirely within the file.
No alignment or flag requirement applies. sh_addralign, sh_flags,
sh_addr, sh_link and sh_info are not inspected, and the
section's index and position are unconstrained. Where several sections
share the name, the one at the lowest index is used.
A 32-bit or big-endian ELF cannot carry a signature at all: it fails the structural requirements above, and a verifier MUST NOT fall back to the extended attribute for it.
3.2.2.2 Extended attribute #
Any file MAY carry its signature in the extended attribute
security.peios.sig. The value MUST be exactly 3310 bytes; any other
size MUST be treated as unsigned.
This is the only location available to non-ELF files, and it is
available to ELF files that carry no .peios.sig section header.
3.2.2.3 Lookup order and commitment #
A verifier MUST determine the storage location as follows.
- Read the first four bytes. A file shorter than four bytes, or whose
first four bytes are not
\x7fELF, is not ELF: go to step 3. - Parse the ELF structures and scan for a section named
.peios.sig. Once such a section header is found, the ELF path is committed: the extended attribute MUST NOT be consulted, whatever happens next. A wrong type, a wrong size, an out-of-range offset, a read failure, a bad version byte or a failed verification all yield "unsigned". A structural failure encountered while parsing MUST commit the path in the same way. The single exception ise_shnum == 0, which does not commit. - Read
security.peios.sig. If present and exactly 3310 bytes, use it. - Otherwise the file is unsigned.
The commitment rule is a security requirement rather than an optimisation. Without it, an attacker able to write a malformed ELF section could force fallback to whichever location they more easily controlled.
Where both locations are populated, the ELF section wins. A signer SHOULD NOT populate both.
3.2.3 What is signed #
The message is a 32-byte SHA-256 content hash. Which bytes it covers depends on where the signature is stored, and a signer MUST use the form matching its chosen location.
ELF section source. The hash covers the file with the section's contents replaced by zeros:
SHA-256( file[0 .. sh_offset)
|| 0x00 × sh_size
|| file[sh_offset + sh_size .. file_size) )
Only the section contents are zeroed. The Elf64_Shdr entry
describing the section is hashed verbatim, as are the ELF header, the
program headers, the section-name string table and every other byte of
the file. The section header metadata is therefore integrity-protected
along with everything else, which is what stops an attacker relocating
or resizing the signature section without invalidating the signature.
This has a direct consequence for how a signer MUST work. The complete
file layout — including sh_offset, sh_size, sh_name and the
position of the section header table — MUST be final before the hash
is computed. The practical sequence is to reserve a 3310-byte
.peios.sig section filled with zeros, finalise the layout, hash the
file as it then stands, sign the hash, and write the blob into the
reserved bytes without touching anything else. Because the reserved
region is already zero, the file as hashed and the file as shipped
differ only in those 3310 bytes.
Extended attribute source. The hash covers the entire file with no exclusions:
SHA-256( file[0 .. file_size) )
This applies to ELF files reaching the attribute path as well as to non-ELF ones. The ELF-zeroed form is used only when the ELF section is the signature source.
In both cases file_size is a snapshot taken at the start of
verification. A verifier MUST re-check the size before returning and
MUST discard the result if it changed.
3.2.4 Algorithm #
The signature is ML-DSA-65 as specified in FIPS 204. The public key is 1952 bytes and the signature is 3309 bytes.
Signing is:
ML-DSA.Sign(private_key, content_hash, ctx = "")
and verification is the corresponding ML-DSA.Verify.
This is pure ML-DSA — FIPS 204 Algorithm 2 and Algorithm 3 — and not HashML-DSA, the pre-hashing variant. The message happens to be a 32-byte SHA-256 hash, which pure ML-DSA signs directly. A signer MUST NOT use the pre-hashing variant; a signature produced that way will not verify.
The context string MUST be empty. Signers MUST NOT set a context. Verifiers cannot express one, so a signature produced under a non-empty context simply fails to verify rather than being detected and reported.
It follows that ML-DSA's context field is not available for domain separation between binary signatures and any other Peios signature system. Separation MUST come from using distinct keys.
A signer MUST supply the public key as the raw 1952-byte key, not an SPKI-wrapped DER encoding. From OpenSSL 3.5 or later, the raw key is the trailing 1952 bytes of the DER public key.
3.2.5 Key selection and trust tiers #
The blob carries no key identifier, so a verifier selects a key by exhaustive trial: it tries each key in its table in order and takes the first that verifies.
The trust tier is a property of which key verified — a pip_type
and a pip_trust, both unsigned integers — and never of anything the
signer encoded. A signer cannot express intent about the tier it
wants. It obtains whichever tier the verifier associates with the key
it used, and if the verifier carries no matching key the file is
simply unsigned.
Consequently:
- A signer that wants software to run at a tier MUST have its public key present in the verifier's table. Getting a key into that table is a deployment question, not a format question.
- A signer MUST NOT assume its signature is portable across systems carrying different key tables. The same file may be trusted on one and untrusted on another, with no observable difference in the file.
- Verification cost is linear in the number of keys, so a verifier MAY reasonably carry few.