Skip to content

Commit

Permalink
Merge pull request #885 from Dasharo/capsule-keygen-kb
Browse files Browse the repository at this point in the history
Add details about key generation for update capsules
  • Loading branch information
BeataZdunczyk authored Sep 2, 2024
2 parents f8b04b1 + ccd16f8 commit 3b8aed5
Showing 1 changed file with 346 additions and 8 deletions.
354 changes: 346 additions & 8 deletions docs/kb/edk2-capsule-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ Signing takes three keys (root, its subkey and subkey's subkey for signing):
This structure of signing keys is presupposed by `GenerateCapsule` and
`Pkcs7Sign` but it seems to be a design decision of the tooling rather than a
strict requirement on the length of a signing key chain by UEFI or cryptographic
implementation in EDK2.
implementation in EDK2. In particular, a chain of length 2 (without a subkey)
can be used by specifying any certificate in place of the subkey (root, signing
certificate or literally any other; it won't be used by the code but still needs
to be a valid certificate so the tools don't complain).

### Optional payload metadata

Expand Down Expand Up @@ -186,12 +189,15 @@ EDK2 payload:
cd payloads/external/edk2/workspace/Dasharo/
BaseTools/BinWrappers/PosixLike/GenerateCapsule --encode \
--capflag PersistAcrossReset \
--capflag InitiateReset \
--json-file dasharo.json \
--output dasharo.cap
```

Values from multiple `--capflag` options are combined together.
In case more than one flag needs to be specified, values from multiple
`--capflag` options are combined together. There is no `InitiateReset` because
Linux rejects capsules with this flag and requires a manual soft reset for
persistent capsules (`CapsuleApp.efi` does reset for persistent capsules by
default, so no difference in behaviour for it).

## Capsule introspection

Expand Down Expand Up @@ -325,6 +331,333 @@ interferes).

[wiki-pkc]: https://en.wikipedia.org/wiki/Public-key_cryptography

## Generating signing keys with OpenSSL

The process involves creation of 3 certificates, a local certificate
authority (CA) in a directory and 14 files. This warrants some overview of
what's going to be done.

Keys making up the chain will assume the following roles:

- _root_ — self-signed certificate acting as the basis of a CA
- _sub_ — second-level CA signed by _root_ key
- _sign_ — a signing key (not a CA) that is signed by _sub_ key

A digital signature algorithm (DSA) consists of a private key (PK) algorithm and
a digest algorithm. Keys in a chain don't have to use the same DSA. Also, most
DSAs allow use of an arbitrary digest (not Ed25519 or Ed448 which prescribe the
use of SHA-512 and SHAKE-256 respectively).

Keys exist independently of the chains in which they appear. When stored,
private keys are often encrypted.

### Key generation by example

It will be easier to go into finer details while looking at the actual commands.
The entire process can be broken into several stages:

1. Creation of 3 certificates
2. Construction of a CA
3. Preparation of _root_ certificate for inclusion into EDK
4. Preparation of _sign_ certificate for signing

#### Make certificates

Using 4096-bit RSA keys as an example:

```bash
openssl genrsa -aes256 -out root.p8e 4096
openssl genrsa -aes256 -out sub.p8e 4096
openssl genrsa -aes256 -out sign.p8e 4096
```

Each of them is encrypted with AES-256. The password is queried interactively
for creation and each time the certificate is used to sign something. Drop
`-aes256` to not use encryption (for a test or if you consider access to the
files a complete compromise of the security).

`.p8e` extension is for PKCS #8 format carrying an encrypted private key.

#### Make a CA

By default, directory for a CA is called `demoCA`, although it can be different
depending on the OS. It can be looked up in `/etc/ssl/openssl.cnf` as
`dir = ...` in `[ CA_default ]` section (and that section is in turn referenced
by `default_ca` in `[ ca ]` section). The directory needs to be set up prior
to using `openssl ca`:

```bash
mkdir -p demoCA/newcerts
touch demoCA/index.txt
echo 01 > demoCA/serial
```

Initialize it with self-signed root certificate (will ask for a password
and certificate fields; country, state and organization fields must match _root_
certificate, common name must be a unique non-empty value):

```bash
openssl req -new -x509 -days 3650 -key root.p8e -out root.pub.pem
```

Create certificate signing requests (CSRs) (don't bother with
entering a challenge for CSRs, you won't be asked for it):

```bash
openssl req -new -key sub.p8e -out sub.csr
openssl req -new -key sign.p8e -out sign.csr
```

Perform the signing (there will be password and confirmation prompts):

```bash
openssl ca -extensions v3_ca \
-in sub.csr \
-days 3650 \
-cert root.pub.pem \
-keyfile root.p8e \
-notext \
-out sub.pub.pem
openssl ca -in sign.csr \
-days 3650 \
-cert sub.pub.pem \
-keyfile sub.p8e \
-notext \
-out sign.crt
```

The `-days 3650` is something to be adjusted and is provided as an example that
certificate properties are set in a different command for _root_ compared to
other certificates.

`-notext` avoids dumping certificate details in text form to the output file
thus making all certificates look consistent. The details are easy to obtain
by running `openssl x509 -in {cert-file} -text -noout` when needed.

`*.csr` files aren't necessary after successful signing and can be removed.

`.pub.pem` and `.crt` files contain essentially the same X.509 certificates, but
the former is used for CAs. There is little consistency or sense in extension
for these types of files in general, so don't read too much meaning into them.

#### Prepare _root_ for EDK build system

EDK gets _root_ certificate(s) in a PCD. The PCD name differ and support one
or many certificates, in this case it's
`gFmpDevicePkgTokenSpaceGuid.PcdFmpDevicePkcs7CertBufferXdr` which expects one
or more certificates in DER (binary) form combined via XDR (simple format where
big-endian 32-bit length is followed by that number of bytes).

EDK provides `BinToPcd.py` that can generate a file for inclusion via `!include`
in some DSC-file of an EDK package.

```bash
openssl x509 -in root.pub.pem -out root.cer -outform DER
python payloads/external/edk2/workspace/Dasharo/BaseTools/Scripts/BinToPcd.py \
-p gFmpDevicePkgTokenSpaceGuid.PcdFmpDevicePkcs7CertBufferXdr \
-i root.cer \
-x \
-o CapsuleRootKey.inc
```

`root.cer` can be removed afterward.

#### Prepare _sign_ certificate

`GenerateCapsule` and EDK will only ever need public parts of _root_ and _sub_
certificates, but _sign_ certificate will have to be provided in combination
with the corresponding private key. This is achieved by packing the two parts
via PKCS #12 which is an archive file format.

`openssl pkcs12` either creates a PKCS #12 file or converts it, thus requiring
two invocations for obtaining the result in PEM format.

First, create binary PKCS #12 (certificate and corresponding private key):

```bash
openssl pkcs12 -export -inkey sign.p8e -in sign.crt -out sign.pfx
```

Add `-passout pass:` to perform export (creation, that is) without encryption.

Now convert binary PKCS #12 into PEM form:

```bash
openssl pkcs12 -in sign.pfx -out sign.p12
```

`-passin pass:` can be added if `sign.pfx` was created without a password to
skip the prompt.

`-noenc` (`-nodec` is deprecated in OpenSSL v3) can be added to avoid
encrypting `sign.p12`. Without this option, there will be a password prompt
during the conversion and whenever a capsule is signed.

`sign.pfx` can now be removed.

### Supported algorithms and digests

EDK has 2 libraries implementing `AuthenticateFmpImage()` which is responsible
for verifying an FMP image:

- `SecurityPkg/Library/FmpAuthenticationLibRsa2048Sha256`
- `SecurityPkg/Library/FmpAuthenticationLibPkcs7`

The first one supports only one kind of a key as indicated by its name.
Nowadays the second library is much more likely to be used. PKCS #7 is a
generic container for signature/signed data and the set of permitted algorithms
and digests is defined by the implementation handling it. There are two
options in Dasharo EDK based on top of `edk2-stable202402` upstream release:

- OpenSSL v3.0.9
- MbedTLS v3.3.0

However, because OpenSSL was never designed for embedded environments, it became
prohibitively large in v3 which introduced providers feature. This is why
Dasharo uses MbedTLS and it will be the focus of the below discussion.

Because MbedTLS deals with certificates prepared by OpenSSL tools, both
constrain the set of what can be used.

#### Build-time configuration

What's actually supported by MbedTLS depends on a specific build and which
version of an EDK wrapper is being used.

The library is configured via a set of defines in
`CryptoPkg/Library/MbedTlsLib/Include/mbedtls/mbedtls_config.h`. One of the
most important ones is:

```c
#define MBEDTLS_MPI_MAX_SIZE 1024
```
It specifies the maximum number of bytes available for multiple precision
integers and puts a limit on private key algorithms. `1024` is the default
value that prevents the use of RSA keys longer than 8192 bits. Elliptic
curve (EC) keys are much shorter in size, so the default doesn't affect them in
any way.
Another important set of defines is the list of enabled curves:
```c
/* Short Weierstrass curves (supporting ECP, ECDH, ECDSA) */
// #define MBEDTLS_ECP_DP_SECP192R1_ENABLED
// #define MBEDTLS_ECP_DP_SECP224R1_ENABLED
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_DP_SECP384R1_ENABLED
#define MBEDTLS_ECP_DP_SECP521R1_ENABLED
// #define MBEDTLS_ECP_DP_SECP192K1_ENABLED
// #define MBEDTLS_ECP_DP_SECP224K1_ENABLED
// #define MBEDTLS_ECP_DP_SECP256K1_ENABLED
// #define MBEDTLS_ECP_DP_BP256R1_ENABLED
// #define MBEDTLS_ECP_DP_BP384R1_ENABLED
// #define MBEDTLS_ECP_DP_BP512R1_ENABLED
/* Montgomery curves (supporting ECP) */
#define MBEDTLS_ECP_DP_CURVE25519_ENABLED
#define MBEDTLS_ECP_DP_CURVE448_ENABLED
```

Mind that using EC requires the use of `MbedTlsLibFull` instead of
`MbedTlsLib`, that's really the difference between the two wrappers.
This define is supposed to enable RSASSA-PSS (see below though):
```c
#define MBEDTLS_X509_RSASSA_PSS_SUPPORT
```
#### Digest types
MbedTLS has the list of supported digests in `include/mbedtls/md.h`:
```c
typedef enum {
MBEDTLS_MD_MD5, /**< The MD5 message digest. */
MBEDTLS_MD_SHA1, /**< The SHA-1 message digest. */
MBEDTLS_MD_SHA224, /**< The SHA-224 message digest. */
MBEDTLS_MD_SHA256, /**< The SHA-256 message digest. */
MBEDTLS_MD_SHA384, /**< The SHA-384 message digest. */
MBEDTLS_MD_SHA512, /**< The SHA-512 message digest. */
MBEDTLS_MD_RIPEMD160, /**< The RIPEMD-160 message digest. */
};
```
OpenSSL supports all of them (see `openssl list -digest-algorithms`, but not all
listed there work with X.509 certificates, not clear why).
#### Private key algorithms
These are listed in `include/mbedtls/pk.h` of MbedTLS:
```c
typedef enum {
MBEDTLS_PK_NONE=0,
MBEDTLS_PK_RSA,
MBEDTLS_PK_ECKEY,
MBEDTLS_PK_ECKEY_DH,
MBEDTLS_PK_ECDSA,
MBEDTLS_PK_RSA_ALT,
MBEDTLS_PK_RSASSA_PSS,
MBEDTLS_PK_OPAQUE,
} mbedtls_pk_type_t;
```
OpenSSL supports the following ones (see `openssl list -signature-algorithms`
but only those which were accepted by OpenSSL in the commands above are listed
here):
- `rsa`
- `rsa-pss`
- `ed25519`
- `ed448`
- `ec`
`rsa` and `ec` are the only ones that actually worked. While Ed25519 and Ed448
curves are there, that is far from a complete implementation of a corresponding
DSAs.
`rsa-pss` is also explicitly enabled, but the following error reported at
run-time:
```c
/** Key algorithm is unsupported (only RSA and EC are supported). */
#define MBEDTLS_ERR_PK_UNKNOWN_PK_ALG -0x3C80
```
Suggesting that it's not actually supported even when enabled. The situation
might be similar to the elliptic curves above.

#### What's actually supported and how to use it
Valid digest names for OpenSSL:
- `md5`
- `ripemd160`
- `sha1`
- `sha224`
- `sha256`
- `sha384`
- `sha512`
How a digest is to be specified depends on a particular subcommand:
- `openssl ca` takes `-md {digest-name}`
- `openssl req` takes `-{digest-name}` (confusingly documented as `-digest`)
`openssl genpkey` accepts `-algorithm {alg}` parameter and `rsa` or `ec` works.
RSA key length's lower bound is 512 in OpenSSL and upper bound is 8192 in
MbedTLS by default. Key size can be configured with
`-pkeyopt rsa_keygen_bits:numbits` if `openssl genpkey` is used.

Given that MbedTLS has only 3 standard curves enabled, there are only 3
possibilities for an EC algorithm (use of `MbedTlsLibFull` is required):

- `-pkeyopt ec_paramgen_curve:P-256`
- `-pkeyopt ec_paramgen_curve:P-384`
- `-pkeyopt ec_paramgen_curve:P-521`

## `capsule.sh` script

### Building a capsule
Expand Down Expand Up @@ -362,12 +695,17 @@ suitable set of keys which can be done like this:
./capsule.sh keygen my-test-keys
```

At the end the script prints a command to use the keys:
At the end the script prints commands to use the keys (wrapped manually here):

```bash
./capsule.sh make -t my-test-keys/root.pub.pem \
-o my-test-keys/sub.pub.pem \
-s my-test-keys/sign.p12
Installing root certificate (before build):
cp my-test-keys/CapsuleRootKey.inc \
payloads/external/edk2/workspace/Dasharo/DasharoPayloadPkg/

Signing a capsule (after build):
./capsule.sh make -t my-test-keys/root.pub.pem \
-o my-test-keys/sub.pub.pem \
-s my-test-keys/sign.p12
```

!!! warning
Expand All @@ -384,7 +722,7 @@ the EDK2-way of doing things via `SignedCapsulePkg` ([PDF][edk2-capsules]) but
it appears to be less flexible due to relying on the build system of EDK2 which
is quite rigid for a large firmware variance that can be found in Dasharo.

Because `GenerateCapsules` is in Python and at least part of the functionality
Because `GenerateCapsule` is in Python and at least part of the functionality
is abstracted in form of modules, it's also possible to build custom tools on
top of that.

Expand Down

0 comments on commit 3b8aed5

Please sign in to comment.