/aws-nitro-enclaves-image-format

This library provides the definition of the enclave image format (EIF) file used in AWS Nitro Enclaves.

Primary LanguageRustApache License 2.0Apache-2.0

aws-nitro-enclaves-image-format

status version docs msrv

This library provides the definition of the enclave image format (EIF) file.

Security

See CONTRIBUTING for more information.

License

This project is licensed under the Apache-2.0 License.

Building

To compile the eif_build tool, run

$ cargo build --all --release

The resulting binary will be under ./target/release/eif_build.

Usage

This package is mostly intended as a library. However, it also contains the eif_build tool which you can use to create AWS Nitro Enclave image files:

Enclave image format builder
Builds an eif file

USAGE:
    eif_build [OPTIONS] --kernel <FILE> --cmdline <String> --output <FILE> --ramdisk <FILE>

OPTIONS:
        --arch <(x86_64|aarch64)>
            Sets image architecture [default: x86_64]

        --build-time <build_time>
            Overrides image build time. [default: 2024-07-09T17:16:38.424202433+00:00]

        --build-tool <build_tool>
            Image build tool name. [default: eif_build]

        --build-tool-version <build_tool_version>
            Overrides image build tool version. [default: 0.2.0]

        --cmdline <String>
            Sets the cmdline

    -h, --help
            Print help information

        --img-kernel <img_kernel>
            Overrides image Operating System kernel version. [default: "Unknown version"]

        --img-os <img_os>
            Overrides image Operating System name. [default: "Generic Linux"]

        --kernel <FILE>
            Sets path to a bzImage/Image file for x86_64/aarch64 architecture

        --kernel_config <FILE>
            Sets path to a bzImage.config/Image.config file for x86_64/aarch64 architecture

        --metadata <metadata>
            Path to JSON containing the custom metadata provided by the user.

        --name <image_name>
            Name for enclave image

        --output <FILE>
            Specify output file path

        --private-key <private-key>
            Specify the path to the private-key

        --ramdisk <FILE>
            Sets path to a ramdisk file representing a cpio.gz archive

        --signing-certificate <signing-certificate>
            Specify the path to the signing certificate

        --version <image_version>
            Version of the enclave image

Enclave Image File (EIF) Specification

Date: 2024-06-21

Background

AWS Nitro Enclaves (Official documentation) is an Amazon EC2 feature that allows you to create isolated compute environments, called enclaves, from Amazon EC2 instances. Enclaves are separate, hardened, and highly-constrained virtual machines. They provide only secure local socket connectivity with their parent instance. They have no persistent storage, interactive access, or external networking.

To run your application in an enclave, your application needs to be packaged into an Enclave Image File (EIF). An EIF is self contained - everything your application needs to run within an enclave is part of the file (e.g. operating system, your application, root file system).

The File Format

High Level Structure

On a high level an Enclave Image File consists of a general header and multiple data sections, each with their local header:

+-------------------------+
|        EifHeader        |
+-------------------------+
|   EifSectionHeader 0    |
+-------------------------+
|      Data Section 0     |
+-------------------------+
|   EifSectionHeader 1    |
+-------------------------+
|      Data Section 1     |
+-------------------------+
>          ...            <
+-------------------------+
|   EifSectionHeader n    |
+-------------------------+
|      Data Section n     |
+-------------------------+

The Enclave Image File format supports a variety of data section types. The data section types can be mandatory or optional. Each section contains a specific type of data needed to run your application within a Nitro Enclave, the specifics of which are specified below in Data sections.

EifHeader

The EifHeader is a general description of an enclave image file and provides metadata on the file as a whole. It has a fixed size of 548 bytes and the byte-order for all multi-byte fields is big-endian. The EifHeader is structured as follows:

0x0000  +--------+--------+--------+--------+
        |               magic               |
0x0004  +--------+--------+--------+--------+
        |     version     |      flags      |
0x0008  +--------+--------+--------+--------+
        |                                   |
        +            default_mem            +
        |                                   |
0x0010  +--------+--------+--------+--------+
        |                                   |
        +            default_cpus           +
        |                                   |
0x0018  +--------+--------+--------+--------+
        |    reserved     |   num_sections  |
0x001c  +--------+--------+--------+--------+
        |                                   |
        +          section_offset 0         +
        |                                   |
        +--------+--------+--------+--------+
        >                ...                <
        +--------+--------+--------+--------+
        |                                   |
        +          section_offset 31        +
        |                                   |
0x011c  +--------+--------+--------+--------+
        |                                   |
        +           section_size 0          +
        |                                   |
        +--------+--------+--------+--------+
        >                ...                <
        +--------+--------+--------+--------+
        |                                   |
        +           section_size 31         +
        |                                   |
0x021c  +--------+--------+--------+--------+
        +             reserved              |
0x0220  +--------+--------+--------+--------+
        |              crc_32               |
0x0224  +--------+--------+--------+--------+

All reserved fields are ignored by the virtualization stack.

magic

The magic field is a constant value chosen to easily identify enclave image files. The value equates to the ASCII string .eif or [0x2e, 0x65, 0x69, 0x66] as byte array.

version

The version field encodes the specification version of the enclave image file. It is necessary to determine the file format version and chose the correct handling according to it.

The latest version of the EIF format is 4. The version gets incremented whenever a backwards incompatible change or addition to the file format is introduced.

EIF format version history:
  • Version 0: internal development version
  • Version 1: internal development version
  • Version 2: initial publicly released version as published in aws-nitro-enclaves-cli v0.1.0 (initial public pre-release) on 2020-08-13. This initial version set the basic file format structure and supported the base section types EifSectionKernel, EifSectionCmdline, and EifSectionRamdisk.
  • Version 3: published in aws-nitro-enclaves-cli v1.0.10 (initial public production release) on 2021-04-29. This version added optional support for image signing through section type EifSectionSignature.
  • Version 4: published in aws-nitro-enclaves-cli v1.2.0 on 2022-03-08. This version added a new mandatory section type EifSectionMetadata containing metadata about the environment the EIF was built in.

Versions 0 and 1 have not been published as part of any tooling release. They are not supported anymore and will fail at the crc check stage of loading the enclave image file.

Versions 2 and 3 both behave the same way. As the EifSectionSignature section type has not been defined in version 2 and is optional in version 3 they are effectively handled the same.

Version 4 introduced the EifSectionMetadata as a mandatory section and checks that it is part of an enclave image file. Apart from that it is handled the same way as Version 3.

Versions >4 are reserved for the future and yield undefined behavior.

flags

The flags bit-field encodes properties for the file and the environment the file is targeted for. The structure of the flags field is as follows:

 f e d c b a 9 8 7 6 5 4 3 2 1 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             |a|
|                             |r|
|          reserved           |c|
|                             |h|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • arch: determines the CPU architecture of the enclave this image file is for: 0 for x86_64, 1 for aarch64. This flag is mandatory and an EIF with an architecture other than the architecture of the enclave will be rejected.
default_mem

The default_mem field describes the default amount of main memory in bytes for the enclave this image is going to run on. Currently this field is unused by the virtualization stack and aws-nitro-enclaves-cli.

default_cpus

The default_cpus field describes the default number of vCPUs for the enclave this image is going to run on. Currently this field is unused by the virtualization stack and aws-nitro-enclaves-cli.

num_sections

The num_sections field describes how many data sections an enclave image file contains. The value range for this is between 2 and 32 (MAX_NUM_SECTIONS).

section_offsets

The section_offsets field is an array of 32 (MAX_NUM_SECTIONS) 8-byte values. The first num_secions entries in this array each describe the position of one data section within the file in bytes from the file start. All used entries have to be ordered in the same order as the corresponding data sections in the file, meaning section_offsets[0] describes the file offset for the first data section in the file, section_offset[1] describes the file offset for the second data section in the file and so on.

section_sizes

The section_sizes field is an array of 32 (MAX_NUM_SECTIONS) 8-byte values. The first num_sections entries in this array each describe the size of one data section within the file in bytes. All used entries have to be ordered in the same order as the corresponding data sections in the file, meaning section_sizes[0] describes the size of the first data section in the file, section_sizes[1] describes the size of the second data section in the file and so on. The section sizes as set in this array only cover the size of the data in a section and do not include the size of section headers.

eif_crc32

The eif_crc32 field contains the crc32 checksum over the whole file except this eif_crc32 field itself; this checksum includes EifHeader and all sections, including their respective section headers, in the order they appear in the file.

Data sections

An enclave image file contains multiple data sections of different types, each with a distinct purpose. The high level format is common between all section types, consisting of an EifSectionHeader and binary data. Sections cannot overlap each other and must not overflow out of 64-bit address space.

The ordering of the different section types within one enclave image file is mostly unconstrained. The only constraint on ordering the sections is that all EifSectionRamdisk sections must be located after the EifSectionKernel section.

EifSectionHeader

The section header is a basic description of a section:

0x0000  +--------+--------+--------+--------+
        |  section_type   |      flags      |
0x0004  +--------+--------+--------+--------+
        |                                   |
        +           section_size            +
        |                                   |
0x000c  +--------+--------+--------+--------+
section_type

The section_type field describes the kind of section. The following is a list of valid section types and their numeric value. A detailed description of each section type follows below.

  • EifSectionInvalid (0x00)
  • EifSectionKernel (0x01)
  • EifSectionCmdline (0x02)
  • EifSectionRamdisk (0x03)
  • EifSectionSignature (0x04) (introduced in version 3 of the EIF format)
  • EifSectionMetadata (0x05) (introduced in version 4 of the EIF format)

Enclave image files containing an EifSectionInvalid section or sections with a type outside of the above range (>= 6) will be rejected by the virtualization stack.

flags

The flags bit-field can be used to encode properties of the binary data in a section. It is currently not used by any section type and is reserved for future use.

section_size

The section_size field describes the size in bytes of the sections data. It must match the corresponding section_sizes entry in the global EifHeader structure.

EifSectionKernel

The EifSectionKernel section data contains a Linux kernel image to be run within the enclave. The file format for that depends on the CPU architecture of your instance and its enclave. For x86_64 instances the kernel section data has to be a bzImage (Refer to the x86_64 boot protocol for details). For aarch64 instances the kernel section data has to be an uncompressed kernel Image file (Refer to the arm64 boot protocol for details).

The aws-nitro-enclaves-cli provides pre-built kernel images for both architectures:

EifSectionKernel section is a mandatory section and every enclave image file must contain exactly one EifSectionKernel section.

EifSectionCmdline

The EifSectionCmdline section data contains a string with Linux kernel cmdline parameters for the enclave kernel. The kernel cmdline can be used to configure certain aspects of the kernel at boot time (Documentation of kernel-parameters).

The aws-nitro-enclaves-cli provides kernel cmdlines for both architectures (compatible with the pre-built kernel images in the same location):

EifSectionCmdline section is a mandatory section and every enclave image file must contain exactly one EifSectionCmdline section.

EifSectionRamdisk

The EifSectionRamdisk section contains data that is going to be part of the root file system of the enclave in cpio or cpio.gz (compressed) format. All data of EifSectionRamdisk sections are concatenated to act together as one initramfs (See Background on ramdisk composition and loading below).

All EifSectionRamdisk sections must be positioned after the EifSectionKernel section within an enclave image file.

Example: ramdisks created with aws-nitro-enclaves-cli

When creating an enclave image file through the aws-nitro-enclaves-cli, two EifSectionRamdisk sections are created. The first ramdisk is the same for all applications and contains two main parts:

  • An init executable: The init process is the first user-space process started by the kernel. The task of the init process is to bring up the systems user-space and start relevant services. For Nitro Enclaves, the init processes tasks are reduced to the bare minimum of mounting special filesystems and files (i.e. procfs, sysfs, /dev), initializing the console device, loading the driver to interact with the Nitro Security Module, and launching the user application. The code for the minimal init process can be found in aws-nitro-enclaves-sdk-bootstrap, and the aws-nitro-enclaves-cli provides pre-compiled executables of it for both architectures:
  • The nsm.ko driver: This is a loadable driver module for the Linux kernel which facilitates access to the Nitro Secure Module (NSM). The driver exposes a special device in the enclave to communicate with the hypervisor to retrieve an attestation document, which can be used to prove the identity of the enclave. The source code for the NSM driver can be found in aws-nitro-enclaves-sdk-bootstrap, starting with Linux kernel series v6.8 the driver is part of the upstream Linux kernel. The aws-nitro-enclaves-cli provides pre-compiled versions of this driver for both architectures (compatible with the pre-built kernel images in the same location):

The second ramdisk contains the application specific data and has three major parts:

  • The root file system: This is a filesystem providing all the software and runtime environment needed by the application as shipped in the applications docker image.
  • cmd file: The cmd file contains the default entry point of the application as specified in the Dockerfile through CMD (or ENTRYPOINT if CMD is not specified).
  • env file: The env file contains the environment variables of the application as specified in the Dockerfile through ENV.
Background on ramdisk composition and loading

The Linux kernel supports various models of booting a system and bringing up user-space. One mechanism is through the Linux kernel’s initramfs format (See Linux kernel documentation driver-api/early-userspace/buffer-format.rst). An initramfs consists of a collection of cpio files, either uncompressed (.cpio) or compressed (.cpio.gz). The Linux kernel contains a minimal interpreter for these files to load initramfs and construct the root file system from them. The initramfs usually contains a basic user-space to bootstrap a system and bring up additional devices like hard disks to switch to the final file system from disk. In the case of Nitro Enclaves, there is no support for persistent storage like hard disks, so the whole system is booted from and contained in the initramfs.

For Nitro Enclaves, no bootloader is employed to load the kernel and ramdisks into memory. That part is performed by the hypervisor, which loads the kernel and ramdisk data into the enclaves memory before starting the enclave. The resulting enclave memory on startup is populated as follows from the EIF (For an example EIF with three ramdisk sections):

                                                        Enclave Memory Layout
+-------------------------+                             +--------------------+
|        EifHeader        |                             |       zeroes       | 0x0
+-------------------------+                             >         ...        <
|   EifSectionHeader 0    |                             |                    |
+-------------------------+>--------------------------->+--------------------+
|      Kernel Image       |                             |    Kernel Image    |
+-------------------------+   +------------------------>+--------------------+ --+
|   EifSectionHeader 1    |   |                         |   Ramdisk (init)   |   |
+-------------------------+   |   +-------------------->+--------------------+   |
|      Kernel Cmdline     |   |   |                     |   Ramdisk (user0)  |    > initramfs
+-------------------------+   |   |   +---------------->+--------------------+   |
|   EifSectionHeader 2    |   |   |   |                 |   Ramdisk (user1)  |   |
+-------------------------+>--+   |   |                 +--------------------+ --
|      Ramdisk (init)     |       |   |                 |                    |
+-------------------------+       |   |                 |       zeroes       |
|   EifSectionHeader 3    |       |   |                 >        ...         <
+-------------------------+>------+   |                 |                    | 0xffffffffffffffff
|      Ramdisk (user0)    |           |                 +--------------------+
+-------------------------+           |
|   EifSectionHeader 4    |           |
+-------------------------+>----------+
|      Ramdisk (user1)    |
+-------------------------+
|   EifSectionHeader 5    |
+-------------------------+
|      Signature Data     |
+-------------------------+
|   EifSectionHeader 6    |
+-------------------------+
|         Metadata        |
+-------------------------+
EifSectionSignature

The EifSectionSignature section was introduced as an optional section in file format version 3. The section data has a maximum size of 32768 bytes (SIGNATURE_MAX_SIZE).

The data format for the EifSectionSignature section is Concise Binary Object Representation (CBOR) as introduced in RFC8949. The CBOR data contains an array of two-tuples, each containing a serialized signing certificate and a serialized CBOR Object Signing and Encryption (COSE) Sign1 object as described in RFC8152. Although the EifSectionSignature section allows for multiple such tuples, only the first of these objects is currently verified against PCR0. This means the only relevant data to sign and add to the EifSectionSignature section is the tuple (0, PCR0).

The overall structure of the CBOR data in EifSectionSignature can be described as follows, where >>>> and <<<< describe entry and exit boundaries of nested serialized CBOR data:

Array(1) {
    Map(2) {
        [0] {
            Text(19)                                                 // key = "signing_certificate"
            Array<Uint8>(len(cbor_serialize(cert)))                  // value = CBOR serialized certificate
        },
        [1] {
            Text(9)                                                  // key = "signature"
            Array<Uint8>(len(cbor_serialize(cose_sign1))             // value = CBOR serialized COSE_Sign1 object
            >>>>
                Array(4) {
                    [0] ByteString(len(cbor_serialize(protected))),  // CBOR serialized COSE protected header
                    >>>>
                        Map(1) {
                            unsigned(1)                              // key = 1 (alg)
                            negative(<val>)                          // value = Signing Algorithm (-7 for ES256, -35 for ES384, -36 for ES512)
                        }
                    <<<<
                    [1] Map(0),                                      // CBOR serialized COSE unprotected header (empty)
                    [2] BytesString(len(cbor_serialize(payload))),   // CBOR serialized COSE_Sign1 payload
                    >>>>
                        Map(2) {
                            [0] {
                                Text(14)                             // key = "register_index"
                                Unsigned(<idx>)                      // value = <idx> (Index of which PCR got signed)
                            },
                            [1] {
                                Text(14)                             // key = "register_value"
                                Array<Uint8>(48)                     // value = PCR<idx> value bytes
                            },
                        }
                    <<<<
                    [3] BytesString(len(<signature>))                // Signature bytes
                }
            <<<<
        }
    }
}
Background on COSE Sign1 and usage in EIF

COSE Sign1 provides a signature structure to have a message signed by a single signer:

+-------------------------+-----------------+-------------------+
|      COSE Headers       |     Payload     |     Signature     |
+- - - - - - - - - - - - -+- - - - - - - - -+- - - - - - - - - -+
| protected | unprotected |    plaintext    |  signature bytes  |
+-------------------------+-----------------+-------------------+

The COSE Headers are divided into two buckets, the protected bucket contains metadata for the signature layer that is part of the data being signed (covered/protected by the signature), while the unprotected bucket contains metadata that does not contribute towards the signature. Each bucket is a map of key-value pairs. The payload is the plaintext data that is being signed. The signature contains the signature bytes.

For the usage in EIF the data contained in the different parts of the COSE Sign1 object is as follows:

  • The protected bucket of COSE Headers only contains one key-value pair identifying the used signature algorithm.
  • The unprotected bucket of COSE Headers is empty.
  • The payload contains a tuple describing one platform configuration register (PCR) for the EIF file, specifically a two-tuple containing the PCRs index and it’s value. (More details on PCRs can be found below in section EIF Measurements).
  • The Signature contains an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of the protected headers and payload with one of the following ECDSA variants: ES256, ES384, and ES512.
EifSectionMetadata

The EifSectionMetadata section was introduced as a mandatory section in file format version 4. The section data contains JSON describing the build environment that produced the enclave image file according to the following JSON schema:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://github.com/aws/aws-nitro-enclaves-image-format",
    "title": "EIF Metadata Content",
    "description": "Format Content of EIFSection of type EifSectionMetadata",
    "type": "object",
    "properties": {
        "ImageName": {
            "type": "string",
            "description": "Name of the EIF image"
        },
        "ImageVersion": {
            "type": "string",
            "description": "EIF version for this image file"
        },
        "BuildMetadata": {
            "type": "object",
            "description": "Metadata on the build environment",
            "properties": {
                "BuildTime": {
                    "type": "string",
                    "description": "Time the image was build at"
                },
                "BuildTool": {
                    "type": "string",
                    "description": "Name of the tool that produced the image"
                },
                "BuildToolVersion": {
                    "type": "string",
                    "description": "Version of the tool that produced the image"
                },
                "OperatingSystem": {
                    "type": "string",
                    "description": "Name of the OS the image was build on"
                },
                "KernelVersion": {
                    "type": "string",
                    "description": "Kernel version of the build host"
                }
            },
            "required": [ "BuildTime", "BuildTool", "BuildToolVersion", "OperatingSystem", "KernelVersion" ]
        },
        "DockerInfo": {
            "type": "object",
            "description": "Metadata on the docker image this EIF was based on, as produced by `docker image inspect`"
        },
        "CustomMetadata": {
            "type": "object",
            "description": "Optional custom metadata to annotate the EIF with"
        }
    },
    "required": [ "ImageName", "ImageVersion", "BuildMetadata", "DockerInfo" ]
}

The EifSectionMetadata section is not part of any measurement for an enclave and does not get validated by the hypervisor, beyond checking for its existence with file format version 4 and above.

EIF Measurements

Nitro Enclaves includes attestation mechanisms to prove its identity and build trust with external services. As part of these measurements the enclave exposes a set of platform configuration registers (PCRs), each providing a set of hashes over some identifying data for the enclaves configuration and code. For the EIF files, there are four PCRs that describe it. They are PCR0, PCR1, PCR2, and PCR8:

PCR0                                  PCR1  PCR2  PCR8*
  |     +-------------------------+     |     |     |
  |     |        EifHeader        |     |     |     |
  |     +-------------------------+     |     |     |
  |     |   EifSectionHeader 0    |     |     |     |
  |     +-------------------------+     |     |     |
  +----<|      Kernel Image       |>----+     |     |
  |     +-------------------------+     |     |     |
  |     |   EifSectionHeader 1    |     |     |     |
  |     +-------------------------+     |     |     |
  +----<|      Kernel Cmdline     |>----+     |     |
  |     +-------------------------+     |     |     |
  |     |   EifSectionHeader 2    |     |     |     |
  |     +-------------------------+     |     |     |
  +----<|      Ramdisk (init)     |>----+     |     |
  |     +-------------------------+           |     |
  |     |   EifSectionHeader 3    |           |     |
  |     +-------------------------+           |     |
  +----<|     Ramdisk (user0)     |>----------+     |
  |     +-------------------------+           |     |
  |     |   EifSectionHeader 4    |           |     |
  |     +-------------------------+           |     |
  +----<|     Ramdisk (user1)     |>----------+     |
        +-------------------------+                 |
        |   EifSectionHeader 5    |                 |
        +-------------------------+                 |
        |      Signature Data     |>----------------+
        +-------------------------+
        |   EifSectionHeader 6    |
        +-------------------------+
        |         Metadata        |
        +-------------------------+

All EIF specific PCRs are calculated in a multi-level scheme containing a fixed initial state and a digest over specific parts of the EIF. They are calculated as the sha384 message digest (described in RFC6234) over the concatenation of the initial_digest and the content_digest:

PCRX = sha384sum( initial_digest.content_digest )

The initial_digest is the same for all PCRs and consists of 48 zero-bytes. The content_digest contains data on different parts of an EIF depending on the PCR.

PCR0

PCR0 contains a measurement of all the data influencing the runtime of code in an EIF. It includes a sha384 message digest over the contiguous data of the EifSectionKernel, EifSectionCmdline, and all EifSectionRamdisk sections in the order they are present in the enclave image file. For PCR0 this means content_digest is calculated as follows:

content_digest[PCR0] = sha384sum( data(EifSectionKernel).data(EifSectionCmdline).data(EifSectionRamdisk[..]) )

Note: The order of elements in that calculation depends on the order of sections in the EIF. Only the data for each section is part of the calculation, the headers are excluded.

PCR1

PCR1 contains a measurement of all the data influencing the bootstrap and kernel in an EIF. It includes a sha384 message digest over the contiguous data of the EifSectionKernel, EifSectionCmdline, and the first EifSectionRamdisk sections in the order they are present in the enclave image file. For PCR1 this means content_digest is calculated as follows:

content_digest[PCR1] = sha384sum( data(EifSectionKernel).data(EifSectionCmdline).data(EifSectionRamdiks[0]) )

Note: The order of elements in that calculation depends on the order of sections in the EIF. Only the data for each section is part of the calculation, the headers are excluded.

PCR2

PCR2 contains a measurement of the user application in an EIF. It includes a sha384 message digest over the contiguous data of all EifSectionRamdisk sections excluding the first EifSectionRamdisk section in the order they are present in the enclave image file. For PCR2 this means content_digest is calculated as follows:

content_digest[PCR2] = sha384sum( data(EifSectionRamdisk[1..]) )

Note: The order of elements in that calculation depends on the order of sections in the EIF. Only the data for each section is part of the calculation, the headers are excluded.

PCR8

PCR8 is populated only if an EifSectionSignature section is part of the enclave image file. In that case PCR8 contains a measurement over the DER representation of the certificate used to sign PCR0 and contained in EifSectionSignature. For PCR8 this means content_digest is calculated as follows:

content_digest[PCR8] = sha384sum( signing_certificate_in_DER )