3.2 Signature Format

3.2.1 The blob #

A signature is a fixed 3310-byte blob, identical wherever it is stored:

OffsetSizeField
01Version. MUST be 0x01.
13309Raw 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.

  1. 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.
  2. 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 is e_shnum == 0, which does not commit.
  3. Read security.peios.sig. If present and exactly 3310 bytes, use it.
  4. 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.

Edit this page