James Bottomley recently posted an item regarding problems he’s seen involving UEFI binaries’ signature alignment, and I’d like to fill in a bit more detail.
UEFI systems use Microsoft’s PE/COFF binary format, with a modification in the headers to reflect the status as UEFI binaries rather than Windows Executables or DSOs. When the UEFI Spec Working Group (USWG) began considering how to specify Secure Boot, the obvious route was to use Microsoft’s binary signing system, Authenticode. Authenticode appears, at first glance, to be a straightforward signing system. Specific parts of a binary are digested with a cryptographic hash — in Secure Boot’s case, generally SHA-256 — and that hash is used in a PKCS#7 signature. The result is embedded in a special section of the binary, the “Certificate Table”. Neither the pointer to the Certificate Table, nor the table itself are included in the hashing, and so the same algorithm can be used during verification. Hash the binary, find a signature in the Certificate Table, and compare the stored hash from that signature to the one you’ve computed. If they match, the signature is for this binary, if they don’t, the binary has been modified. Once the hash has been verified, it’s a simple matter of checking the authorization of the signer against your system’s certificate databases, and you know whether to allow execution.
Except it’s not that simple.
In a PE/COFF binary, the Certificate Table is found by looking at a particular entry in an array known as the Data Directory. This entry contains a file offset for the table, as well as the size that the table takes up in the binary. Inside the table itself, there’s a header with a size, a version code, and a GUID to show that the body is an PKCS#7 signature. Given that it’s described as a table and clearly set up so the container can be larger than a given signature, the natural conclusion is that this is an array, and multiple signatures are allowed.
When I implemented pesign, I erroneously assumed that this was a packed array of signatures. When multiple signature support was added to Tiano, the UEFI reference implementation, the same assumption was made. Testing commenced, and the two worked together just fine.
Some time later, somebody noticed that the Tiano code was not in compliance with the specification. PE/COFF version 6.0 states:
Notice that certificates always start on an octaword boundary. If a certificate is not an even number of octawords long, it is zero padded to the next octaword boundary. However, the length of the certificate does not include this padding and so any certificate navigation software must be sure to round up to the next octaword to locate another certificate.
Aside from the constant use of the word “certificate” to refer to a signature, or the use of “octaword”, which has never confused anybody, I’m sure, what this says is that if you compute the address to put the Certificate Table, and that address isn’t on an 8 byte boundary — that is, the address modulo 8 is not zero — then you should round it up to the next 8 byte boundary. But it doesn’t really cover the array-like nature of the table — in fact, this version of the specification doesn’t really treat it as more than a single entry, though the data structure to allow you to treat it as an array is present.
In version 8.0, which UEFI binaries were standardized against, the format slightly changed, as did the language. The only mention of this alignment requirement is now (in 8.0 and 8.3):
The attribute certificate table is composed of a set of contiguous, octaword-aligned attribute certificate entries. Notice that certificates always start on an octaword boundary. If a certificate is not an even number of octawords long, it is zero padded to the next octaword boundary. However, the length of the certificate does not include this padding and so any certificate navigation software must be sure to round up to the next octaword to locate another certificate.
Ignoring for the moment that “contiguous” and “octaword-aligned” stand in opposition to each other, this spells out how to structure the list in more detail than older versions of the specification.
This all brings us to Tiano revisision 14141:
Update the DxeImageVerificationLib to support for Authenticode-signed UEFI images with multiple signatures. Signed-off-by: Fu Siyuan firstname.lastname@example.org Reviewed-by: Ye Ting email@example.com Reviewed-by: Dong Guo firstname.lastname@example.org
Among other changes is this:
This, for the first time in UEFI code, enforce alignment requirements on the signatures embedded in the binary.
As a result, signers have needed to be fixed to properly reflect this requirement. In pesign, that work was done in March of 2012. But this, once again, isn’t as simple as it seems.
Typically the Certificate Table is the very last thing in the binary — after all, it’s the last change you’re going to make, or else it’s going to be full of invalid signatures. In UEFI, you can either enroll a certificate into the security databases, or you can enroll the hash of a specific binary. When you have an unsigned binary, there’s no guarantee of what the size of the binary will be. If the last section happens to be full of structured data, it will probably end on an alignment that matches that of that data. If the last thing is a string table or executable code for an 8086 CPU, it could end at any offset. So there’s basically only a one in eight chance that it’s aligned correctly for appending the Certificate table. If it isn’t, then you pad to the right alignment before appending the table.
When the signature is calculated on a binary with a Certificate Table, that padding is included in the data being digested. The inevitable conclusion is that the Authenticode hash of a signed binary is not guaranteed to be the same as the hash of the exact same binary if it has never been signed.
So now in addition to “pesign –sign”, the command to sign a binary, and “pesign –hash”, which outputs the hash of a binary so you can enroll it specifically, pesign now also supports “pesign –hash –pad”, which you can use to find out what the hash of an unsigned binary would be, were it signed.
This all went in to pesign 0.104, released Wed Mar 27 09:41:52 2013 -0400 .
Just in case anybody is interested, in Fedora, we tracked this issue with bug 963361.