Viewing public RFDs.
RFD
605
Updated

VM instances currently have no way to cryptographically prove to external services what software they’re running or that they’re executing on a trustworthy Oxide platform. This RFD proposes a method for establishing a cryptographic identity for an instance of a virtual machine (an "instance") that is bound to an attestation from the Oxide platform RoT. This identity is intended to be useful in establishing trust between instances in the rack and services possibly executing outside of the rack.

Note
This RFD attempts to use language from [rfd152] as much as possible. When discussing virtual machines (VMs) we use 'instance' to mean a VM in the running state.

Goals

This RFD has three primary goals:

  • Extend the measurement chain to include instance-specific software and configuration

    The control plane identifies instances by UUID and silo membership, but trust requires knowing the software executing within them.

  • Expose attestation primitives to guest instances

    To get an attestation from the platform an instance must have a means to challenge it for an attestation. This requires both a mechanism through which an instance can obtain an attestation from the platform as well as an API for doing so.

  • Provide a mechanism to bind an instance-held key to an attestation

    Cryptography is often used to establish a communication channel between two entities as a means to provide secrecy and integrity guarantees. Implementers of attestation protocols may wish to use these guarantees as a means to mitigate threats like interposition and impersonation attacks. This requires the instance be able to prove a binding between a key held by an instance and an attestation from the platform.

The remainder of this document begins with some background before discussing these goals, and how we accomplish them.

Qualifying Data

Qualifying data (qdata for short) is a term we borrow from the TCG [tcg-qdata]. qdata is input provided to an attestation function like TPM2_Quote, or the attest function from [rfd497]. It is not interpreted by the RoT that generates the attestation. Instead the RoT includes the provided qdata in the attestations that it produces:

attest = sign(serialize(log) | qdata)

where:

  • sign is the cryptographic signature function used by the RoT to generate attestations

  • log is a structured collection of measurements recorded by the RoT

  • serialize is the serialization function used by the RoT

  • qdata is a sequence of bytes provided by the challenger

Replay attacks against attestation protocols can be mitigated by binding a challenger supplied nonce to the attestation. So long as the caller provides a unique nonce with each challenge it can correlate each response to the request. We’ve selected 32 bytes as the size of our qdata to allow for random nonce generation with statistically improbable reuse provided a good RNG is used. In the general case, the challenger will supply a random 32 byte nonce as input to the attestation function:

qdata = nonce = rand(32)

We can also use qualifying_data in a more dynamic way as a means to bind additional data to the attestation. This is accomplished by combining the nonce with this data through a hash function:

qdata = digest(nonce | data)

where:

  • data is some data that we want to bind to the attestation

  • digest is a cryptographic digest function

This allows consumers of the attestation function to bind some data to the attestation in a way that is verifiable by the challenger.

Attestation & Appraisal

An [attestation] is an assertion of some set of system properties by an authority. In the context of computing the attestor is asserting the set of software that was loaded and executed. Appraisal is the process by which relying parties evaluate attested information to determine its trustworthiness. Attestations are produced by an [RoT-R] and signed by a key certified by some PKI.

The appraisal process happens in two phases: First the relying party must determine the trustworthiness of the attestation itself. Once this has been accomplished the measurements that make up the attestation are evaluated.

Verifying Attestations

Before an attestation can be appraised, its integrity, authenticity, and freshness must be verified. Attestations must be accompanied by a certificate chain that links the attestation signing key to a trusted PKI root. Establishing the authenticity of an attestation requires that we first verify that this cert chain is valid [rfc5280]. In most cases this cert chain will end with the last intermediate before the root signing key. The Oxide RoT signs attestations with a key certified by the Oxide platform identity PKI. RoT-Rs controlled by some other authority will use keys certified by their own PKI.

Once an attestor’s cert chain has been verified back to a trusted root we appraise the attestation’s integrity by verifying the signature over it. The leaf certificate in the chain is the attestation signing key (often called the [alias] key) and so we extract its public part to verify this signature. Attestations produced by the Oxide Platform RoT are signatures over the serialized measurement log concatenated with a caller provided nonce. Verifying this signature requires that the relying party reconstruct the message signed. By entangling a random nonce through the Qualifying Data, verifying the signature over the reconstructed message establishes both the freshness, and the integrity of the attested data.

Appraising Measurements

After verifying the signature over the attestation, the measurements represented in the attestation must be appraised. To aid in the appraisal process for the Oxide platform RoT we generate [CoRIM] documents as part of our release process. A [CoRIM] document contains a collection of reference measurements that an appraiser can use to identify the measurements produced by an RoT.

Appraisal is the process used to ensure that the measurements from an attestation produced by a particular RoT-R meet some policy. How this policy manifests is at the discretion of the appraiser. At a minimum a reasonable appraiser will reject an attestation that includes measurements not present in the set of reference integrity measurements trusted by the appraiser.

We view appraisals based solely on reference measurements from a trusted authority as a minimum bar. With this data alone we can only deduce whether or not a platform is running the software intended by the manufacturer. When the source code for the relevant software is not available for review, this is often the most thorough analysis that we can provide.

If the source code is available (publicly or otherwise) and the build is [reproducible], a much deeper analysis is possible. Reproducible builds are intended to allow a relying party to reconstruct a release artifact exactly from source code. This crates a link associating a release of the source code with the artifact and the reference measurements from the [CoRIM]. A relying party that can analyze the source code to determine its trustworthiness is making a significantly more informed decision than one that must simply trust the assertions from a [CoRIM].

Reproducible builds can be difficult to get right for large / complex software. We have done significant work to make reproducing Hubris (RoT and SP) images trivial but additional work will be required for up-stack components. Attestations that include measurements of 3rd party software on the platform may not however provide the same level of transparency or reproducibility. We anticipate having to place significant trust in the reference measurements distributed for any proprietary software artifacts.

Identity

Identity is defined by context and in the context of measurement and attestation, the identity of a piece of software is defined by a cryptographic hash over the executable image. Knowing this digest allows us to build an association between the executable and the source code used to generate it. This association is strongest when software builds are [reproducible].

We can extend this notion of identity to one of trust through analysis of the relevant source code. If we’re able to read the code and determine that an executable produced from it will behave in the way that we expect we can classify it as trustworthy.

It’s important to note that this does not guarantee a lack of bugs or vulnerabilities. The discovery of bugs or vulnerabilities is inevitable, and when such an event occurs our process remains unchanged: We fix the bug, we resolve the vulnerability, and we produce a new release. We cannot guarantee the absence of bugs in software, but this definition of identity allows us to differentiate between the software that we do not trust, and the software that we believe that we can.

The remainder of this section describes:

  • how we calculate the cryptographic identity of an instance

  • how we build an association between this cryptographic identity and other identities for the instance in the rack

boot disk

Capturing the digest of an executable is trivial when the executable is a file on disk. An instance in the rack is composed of many executable and configuration files. All of this data is contained within the boot disk image and so the digest of the boot disk image fits our definition. This is not a particularly elegant, or efficient solution, but it is does get us the information that we need.

UUID

Any number of instances can be launched from a single image so from the perspective of measured boot they are identical. While the digest of the boot disk acts as an identity for measured boot & attestation, the larger rack system has it’s own context. Instances are uniquely identified in the [Oxide-API] by UUIDs. When an instance is provisioned it is assigned a UUID that subsequent API calls use to address the resource. Our notion of identity must be linked to the instance UUID as a means to allow relying parties driving the rack through the API to associate the two.

Silo, Project etc

Additional configuration information associated with an instance may be relevant to its identity as well. We will consider these on a case-by-case basis.

Extending the Measurement Chain

The measured boot implementation in the rack captures the identities of the software components loaded & executed as the rack boots. Instances rely on these components for its correct execution since they control the various devices (including memory) that the instances consumes. These components make up the trusted computing base ([TCB]) for the instance, and are an integral part of it’s identity.

Our existing implementation is minimal, currently covering only the RoT and SP — a subset of the TCB for an instance. As we extend coverage up the stack to include the instance TCB in it’s entirety, we must integrate attestations from additional RoTs, each with its own measurement chain.

The following diagram shows the chain of measurements that flow from the RoTs on a sled. This includes both those currently implemented, and others that are still notional.

The following subsections describe the RoTs represented in this diagram, and the measurements that they collect (current & notional).

Oxide Platform RoT

Each sled in an Oxide rack has an RoT implemented on an NXP LPC55 [rfd0007]. This MCU is programmed with an instance of Hubris [rfd0041] configured to act as both the root of trust for measurement (RoT-M / RTM) and the root of trust for reporting (RoT-R / RTR) [TCG-Glossary]. When a sled is manufactured it generates a platform identity (PlatformId) key internally. This key is certified by our platform identity PKI [rfd0303][rfd0387].

This RoT performs a DICE-like boot leveraging the LPC55 ROM’s DICE implementation. The RoT bootloader and early hubris bringup code capture measurements of the RoT software image. This is used to construct a key hierarchy certified by the PlatformId. Certificates for leaf nodes in this PKI include the digest of the RoT software image. This is the first measurement captured by the Oxide Platform RoT measured boot.

Once this RoT has booted fully the Attest task is launched. This task receives a key pair (the alias key in [DICE Layering Architecture]) and certificate chain rooted in the platform identity PKI. This task is responsible for measuring the SP [rfd0529] and providing an interface for the SP to report additional measurements (similar to TPM2_PCR_Extend). Future work will use this mechanism in the SP as a means to report a measurement of the [HBS] image.

The Attest task provides an attestation API not unlike the TPM2 [rfd497]. Through this API a challenger obtains the serialized measurement log, a certificate chain, and an attestation signed by the RoT. The certificate chain is used to verify the signature over the attestation which includes a digest of the serialized measurement log and a caller supplied nonce (to guarantee freshness).

AMD RoT

In the new Turin line, AMD is providing roots of trust for both measurement and reporting in the PSP. The PSP provides two attestation mechanisms. The first is a Dice Protection Environment ([DPE]) that’s exposed to the host. This DPE collects a measurement of the first non-AMD / host software executed. This software is our [HBS], the same [HBS] we’re actively working to measure in the SP. Getting this measurement from two sources will allow us to correlate the information across two attestations. Enabling access to this DPE and integrating it into our measured boot implementation is a high priority and currently under development.

The PSP also hosts an [SPDM] responder that is intended to provide BMCs a mechanism for appraising the (AMD proprietary) code executed on the PSP. Due to this RoT reporting only measurements from AMD components and our inability to verify these measurements back to source code we believe integrating attestations from this RoT to be a lower priority.

OS RoT & Measurements

The [helios] operating system (OS) is launched from the Host Phase 1 & 2 images. This OS supervises the execution of the Oxide control plane and is thus best suited to extend the measurement chain to include OS components like processes and zones. These measurements must be recorded by either an existing RoT or a new one dedicated to the task. The AMD DPE is intended as a building block for such an RoT and is much more flexible as a result. The currently recommended approach is to use the AMD DPE as the basis for a new RoT dedicated to the OS and residing within the Illumos kernel.

VM Instance RoT

A subset of the measurements identified in Measurement Overview are marked as "dynamic". This is intended to convey per-instance configuration data that, if changed, will affect instance behavior. These must be handled with care such that an attestation describing the instance’s [TCB] is accurate and complete.

The propolis process is responsible for instance life cycle management and is best suited to collect a log describing the relevant configuration (boot disk, CPUID / device model etc, device state etc). Above all else, propolis must ensure that the measurement log it produces accurately describes the instance it manages.

The data that will be included in the measurement log produced by propolis is discussed in Identity. Elements like the instance UUID are communicated to propolis by the control plane as part of deploying instance. Capturing a digest of the boot disk is less trivial.

propolis will only report a digest for an instance boot disk if the disk is:

These restrictions are necessary in order for propolis to reliably report the boot disk digest with reasonable performance. The read-only requirement here enables propolis to calculate the digest of the boot disk in parallel to the instance booting. Until propolis has finished calculating this digest, it must respond to requests from the instance with an error indicating that the caller should try again.

The threats that necessitate these restrictions are discussed in Security Considerations.

When propolis boots an instance with a mutable boot disk, it can still report the UUID and other metadata accurately. Omitting the digest of the boot disk reduces the information available to appraisers, but the other metadata may still be useful. The implications of this choice for appraisers is discussed further in Security Considerations.

Unlike the Oxide or AMD RoTs, propolis does not have access to a key that it can used to sign attestations. Future work building on top of the AMD [DPE] may make this possible, but the urgency of our current situation requires a more immediate solution.

Our initial implementation is limited to attestations from the Oxide platform RoT. This RoT produces attestations that are a signature over the hubpack serialized measurement log concatenated with a caller supplied nonce as the Qualifying Data:

attest = sign(hubpack(log_rot) | qdata)]

where hubpack(log_rot) is a function that produces the hubpack serialization of the RoT measurement log.

Acting as an RoT-R for each VM instance, propolis must record the digest of the instance boot disk & UUID. When the VM requests an attestation, propolis will serialize a JSON representation of the instance UUID & [boot disk] digest: JSON(log_vm). To bind these additional measurements to an attestation from the Oxide platform RoT, propolis will combine JSON(log_vm) with the qdata provided by the caller within the instance.

Assuming that the instance provides propolis with a 32 byte nonce then:

qdata_i = nonce = rand(32)

where: qdata_i is the qualifying data provided by the instance

propolis will then add its serialized measurement log to the qualifying data from the instance :

qdata_p = digest(JSON(log_vm) | qdata_i)

where:

  • digest is a hash function that produces 32 bytes of output

  • adata_p is the qualifying data created by propolis

propolis will then request an attestation from the Oxide RoT providing qdata_p as input. The Oxide platform RoT will respond to this challenge with an attestation:

attest = sign(hubpack(log_rot) | qdata_p)

The verification formula would then become:

verify(attest, hubpack(log_rot) | digest(JSON(log_vm) | nonce))

Additional RoTs

Measured boot implementations are slowly gaining traction across the hardware ecosystem and our goal is to enable as many as is practical. We’ve already spoken to SSD vendors that claim to provide an SPDM responder as a means to enable attestation, but we’ve not yet been able to get details on how to enable or use this feature. As these products mature we will evaluate them for inclusion in our attestation and appraisal implementation.

Instance-facing Attestation Interface

To expose attestation primitives to guest instances we need a few things:

  • a communication channel between the hypervisor / propolis and the instance

  • an API & serialization scheme that accepts Qualifying Data from the instance and returns an attestation from the platform

VM Communication

How the VM communicates with the host to make these requests has significant ramifications for the scope and shape of this API and related ones. To date, the only communication between the hypervisor and the instance has been the existing NoCloud cloud-init static data ([rfd186]). Several use cases, including this one, motivate a richer channel:

  • Virtual machines want to retrieve metadata at runtime, which is useful for cases such as Kubernetes CSIs ([rfd595]). Many related use cases involve dynamically reading metadata that reflects things that can change, such as floating IPs ([rfd186]).

  • In many cases we’d like to notify the instance about something. For example, an instance using local disk cannot be live migrated and would benefit from advance notice before it’s shutdown ([rfd584]).

  • We’ve had several ideas for services that would let an instance sign data / requests with private keys held by the platform on its behalf. This could be similar to AWS KMS, a virtual TPM, or a cryptographic token that implements a more common interface (PKCS11, CCID/PIV etc). Because of it’s speciality nature, this use case is less important than the first two.

These use cases all involve direct communication between the instance and the hypervisor. A second category of communication has the instance reaching network services that we may want to be accessible independent of the external network. These include NTP, DNS ([rfd21]), and even the Oxide API itself.

Finally, when looking at interface options we must also consider:

  • Does the mechanism allow an instance to constrain who can use this interface? For example, does every user access to the interface out of the box, or can some permissions or privilege model limit access to some subset of users? This is not a hard constraint, but it would be nice if it fell out naturally.

  • How much concurrency does the interface provide?

  • What is the relative work involved in implementing support for a particular interface for Oxide, and what are the relative costs for consumers?

We consider three primary interfaces:

  • A serial device such as a virtualized UART.

  • IP-based sockets to an endpoint on the network (link-local or a reserved address on a Subnet).

  • Virtio-based sockets [VirtIO-Socket] (aka vsock) targeting the well-known hypervisor address.

We start with the virtualized UART or other serial device. Whether this is the classic 16550-style UART or a virtio-console, the ultimate user interface is the same. The ability to communicate with a UART exists on most every operating system. In addition, because the UART exposes a device path (say in /dev), the instance can control which users and containers inside of it potentially have access.

The UART has two notable downsides. First, UARTs have no inherent framing: if you disconnect mid-operation and then reconnect, the other side cannot tell, and may still be processing an earlier request or response. This requires every client to implement the same framing mechanism. This downside is surmountable. Second, by default a UART can only be opened by a single process at a given time. While the framing format could allow for multiple in-flight requests, this increases the complexity of the framing mechanism.

From here, we turn to two related mechanisms: using IP based communication or vsocks. vsocks are similar to IP sockets: they have a source and destination port as well as source and destination client IDs, though for most use cases one can use the well known hypervisor client ID. By using connection-oriented sockets (SOCK_STREAM), we can easily handle multiple requests and deal with resumption. This does not eliminate the need for framing within the socket itself.

Next is the the question of how to deal with the guest’s ability to lock down the interface. In general there is no great way to do this, but there are two techniques, both requiring guest cooperation. The first is to require that requests can only be initiated from privileged ports. This requires that an application call bind() to bind to a local address. This is doable, but can quickly get complicated, as one can no longer rely on the operating system to assign ports automatically. That said, having the option is nice. The second approach is to inject a per-boot shared secret into the instance in a privileged form that is then required in these requests.

We don’t consider any of these approaches a requirement for our initial goal. The use of privileged ports is useful when we consider future cases such as an agent running in the instance that wants to receive event notifications.

Comparing the IP and vsock interfaces surfaces a few notable tradeoffs. IP sockets are much more common and have far more tooling, and other cloud providers have used IP-based sockets for similar purposes. These have led to numerous server-side request forgery issues ([AWS-SSRF]), though such challenges are not unique to IP-based sockets: if we opt for vsock, we still need to evaluate our APIs and designs against this class of threat, even if it may be considered harder to exploit.

The major difference between the IP and vsock interfaces is what they require from the guest and how network configuration fits in. Guests may be configured with IP interfaces supporting only IPv4, only IPv6, both, or neither. While many guests will end up with IP interfaces, it is possible to create a guest interface with none. It would be nice if access to metadata services and similar features still worked in that environment.

Today our environments have no notion of link-local address usage. Because public clouds don’t support broadcast or multicast traffic ([rfd9]), they never need true link-local semantics. Oxide, however, does have customers who care about multicast support ([rfd488]). It is unlikely that we’ll ever need a real use for link-local addresses in the product; a distinct advantage of vsocks, though, is that they do not interfere with traditional host-based networking or anything else one might want to do. Similarly, IPv6 defaults to assigning a link-local address to an interface while IPv4 does not. Because some of our longer-term goals want a guest to listen for events from the control plane, this makes the vsock interface slightly easier to use. We do not want to require a guest to listen on a specific address assigned from its VPC Subnet for this purpose; the guest should be free to use all of its ports however it desires.

Conversely, a major advantage of IP-based sockets is that they are easier to use and make requests against. Common utilities like curl and nc were designed for IP-based sockets and have no support for vsocks, which can make development and introspection harder. Windows also lacks an out-of-the-box driver for vsocks, though they are covered by the same driver package used for all virtio devices. Not all operating systems support this interface, which is a disadvantage.

The last question is ease of implementation. Both paths require a chunk of work. The IP route means developing a way to identify all of these requests in OPTE and route them to a zone for processing. While aspects of this logic are required to implement some of the services we’ve envisioned, such as internal NTP and DNS ([rfd21]), none of it exists yet. The vsock route requires adding a good deal of support to propolis. It’s hard to estimate the exact magnitude of either effort, but the propolis support seems likely to be the lower lift, and propolis requires this work regardless.

There are a lot of tradeoffs here. Our proposal is to go with vsocks, in part because of our time-to-market needs, because it stays out of the way of whatever the guest might want to do with networking, and because it enables future-looking paths that likewise avoid interfering with guest networking.

One important observation is that even if we select this path today, it can be part of the virtual machine definition in some form (e.g., part of the VM virtual platform definition [rfd505] or similar), allowing it to be changed and evolved over time.

Protocol & Message Format

Our choice of virtio-sockets (vsock) as the communication channel between the hypervisor and the instance means the protocol that we implement must work over a standard stream socket. The protocol that we build on top of this socket should be as simple as is practical. Efficiency is also important but the artifacts exchanged over this channel are relatively small.

To obtain an attestation the caller / instance must provide Qualifying Data to the VM Instance RoT. The data that it receives in response will include attestations, measurement logs, and cert chains from each relevant RoT on the platform. Our initial implementation will include the Oxide Platform RoT and the VM Instance RoT. The VM Instance RoT will not have a signing key and will instead entangle the measurements it collects with the attestation from the Oxide Platform RoT. The artifacts returned from the [VM Instance RoT] be limited to one cert chain, two measurement logs, and one attestation. The size of the cert chain will be an order of magnitude larger than the other artifacts, though it is still only single digit KiBs in size.

Binary protocols are generally more efficient than text but given the relatively small size of the data exchanged through this protocol efficiency is a secondary goal. Simplicity is far more important on this front and so we will encode all messages as JSON. Each message exchanged will be serialized as JSON with a new line terminating the message. This format is known as JSONL / [JSON-Lines].

API

The API exposed to an instance by the host must be able to capture attestations from the relevant RoTs. Initially this will include a single attestation from the existing Oxide platform RoT. Future work to add attestations from other RoTs (AMD, host OS etc) would ideally be backward compatible. Our initial release however will provide no such guarantees. Customers must be made aware of this limitation in our initial work and should plan for future changes.

Note
This features is being developed in coordination with a customer that has agreed to take API breaking changes in future releases.

Each RoT on the oxide platform must expose some interface for the platform to drive interactions. The API / interaction model that we layer over these interfaces and expose to VM instances must be compatible with these underlying APIs. As we build out this architecture over time it is possible that we’ll need to integrate an RoT with an incompatible / difficult API. At the time of writing however the RoTs that we know we will need to integrate with either support [SPDM] or the simple API exposed by the Oxide platform RoT.

Oxide Platform RoT API

The API exposed by the Oxide platform RoT has only 3 commands each performing a related function. The log command is used to retrieve the measurements collected by the RoT. The attest command takes 32 bytes of qualifying data from the caller and returns a signature over a digest of the hubpack encoded measurement log and the caller provided qualifying data. Finally the cert_chain command retrieves the certificate chain from the leaf cert for the attestation signing key back to the first intermediate before the Oxide platform identity PKI. The data produced by these 3 commands, when combined with the appropriate trust anchor and reference measurements, is sufficient to verify and appraise an attestation from the Oxide platform RoT.

This minimal interface is nice for a few reasons. Implementation was relatively easy and fast. This allowed rapid prototyping, testing and enabled implementation of client software it drive the API from various parts of the platform.

This simplicity does however come at a cost. The popular attestation APIs (generally limited to the TPM & SPDM) include provisions to protect the secrecy and integrity of the communication channel between the requester & responder. This increases our reliance on the security properties of the path between propolis and the various RoTs. Additional details are discussed in Security Considerations.

SPDM

SPDM is a DMTF standard intended to provide a protocol with bindings to various transports for reporting measurements of firmware identities for devices. This standard is intended to meet the needs of many different types of devices that are integrated into traditional PCs / servers. This results in a very flexible and unfortunately complex protocol. The details of the SPDM protocol can be found in [SPDM] but readers are encouraged to start with the [SPDM-architecture] overview.

Despite the complexity of SPDM the general shape of the protocol is similar to the TPM: Commands exist to get a certificate chain from the responder suitable to verify the signatures that it produces. If the device hosting the responder is sufficiently simple we may be able to get all measurements and a signature (attestation) over them in a single command. If the device is more complex the VM Instance RoT or some down-stack component may need to execute a sequence of GET_MEASUREMENTS commands in order to retrieve the full log.

Exposing this complexity to instances should only be done if absolutely necessary and so the VM Instance RoT should attempt to collect all measurement blocks from the responder and return these to the requester in a single message along with the associated signature.

VM Instance RoT API

In our efforts to keep our initial implementation as simple as possible we do not intend to expose each attestation responder directly to the instance. Instead we want to expose an interface that maps directly to the needs of the caller: They want to know what software is executing on the platform that they are running on or communicating with, and a means to establish trust in these artifacts. Given what we know now (the information contained within this document) we believe this can be accomplished with a single command that we call attest.

Input to this command will be 32 bytes of Qualifying Data. This gives the caller maximum flexibility to extend this architecture as the qualifying data may be a nonce if the VM instance is challenging the platform directly, or a combination of data determined by the instance. [Binding Keys to Attestations] discusses one such use case in depth.

In response to this command the VM Instance RoT will: * generate the JSON encoded structure with the elements describing the VM from Identity * use a hash function to combine this string with the qualifying data provided by the caller * get an attestation from the Oxide Platform RoT providing it with this qualifying data

The measurement log and cert chain required to verify and appraise this attestation must be obtained and returned in the same message. These artifacts may be retrieved in response to the challenge or may be cached (see Optimization). A mock implementation of the API discussed in this section can be found in [vm-attest-proto].

Binding Keys to Attestations

Including information about a VM instance in our attestation information gets us mostly to realizing our Goals. We must still however provide a means for the attestation challenger to construct a secure communication channel with the attested instance.

Baking keys into disk images is a practice that we should discourage. Doing so allows for trust relationships between keys and the services that rely on them to be created in advance of their deployment. The downsides outweigh the benefits however as this practice make the disk images that contain the keying material as sensitive as the keys that they hold. Instead we prefer ephemeral keys created by the instance.

Ephemeral keys cannot be known to the challenger in advance so mutual TLS authentication is out of reach. A PKI root can be baked into the instance image though and the instance can use this as a means to authenticate the challenger as part of setting up a TLS connection. Our goal then is to bootstrap trust in this initially untrusted key.

A mechanism to accomplish this has already been discussed in [VM Instance RoT] as a means to bind information about the instance to an attestation from the Oxide platform RoT. We expand the definition for the qdata to include an arbitrary public key:

Assuming the instance is provided a nonce as the qualifying data from the challenger:

qdata_c = nonce = rand(32)

The instance would then calculate a new qualifying data from this nonce & the public part of an ephemeral key:

qdata_i = digest(qdata_c | key_pub)`

qdata_i is then provided to propolis over the vsock through the [VM Instance RoT API].

The challenger must then verify and appraise the attestation from the platform. This must include verifying the link between the key used by the instance to establish the secure channel with the challenger and the public key bound by the instance to the attestation. Additional risks associated with constructing attested channels between an instance and a challenger are in [Security Considerations].

Key Protection

Creating a binding between an instance’s UUID and a cryptographic key requires that each guest possess a key. This key will be as sensitive as the operations or access it authorizes. Misuse or loss of such a key can be damaging on a number of vectors, many of which depend on the resources protected by a relying party / access control mechanism. Cryptographic tokens like [CCID] devices and TPMs are commonly recommended as mitigations.

This problem is increasingly relevant for virtual machines, and virtual cryptographic tokens (vsmartcard , vTPM etc) are often provided in cloud environments as a mitigation. Oxide has not yet implemented such a mechanism for VMs due to relatively low customer interest. Our direction currently is to provide a binding between an instance’s identifying features (image digest, UUID) and a key generated and held by the client.

Performance

The mechanism proposed in Binding Keys to Attestations seem sufficient to meet our stated Goals. The downside of this approach is that the [Oxide Platform RoT] becomes a bottle neck as it binds all attestations & VM instance data together. The platform RoT is a microcontroller with limited resources and the communication channel from the host OS and it is narrow. By allowing VM instances to call into the platform RoT we create potential resource contention that will be visible to instances.

This is further complicated by the expected usage pattern: This API provides a means to bootstrap trust between the VM instance and some external authority. The authority is expected to use the attestation appraisal process to gate access to some resource. This implies that the VM instance will need to provide an attestation to the attestation challenger before it can do any useful work. Since this API is expected to be on the critical path for customer workloads we need to be able to describe the performance characteristics that they should expect.

Test Strategy

Though this is still a notional API we believe that the Oxide platform RoT will be the bottle neck in our initial implementation. As part of this work we’ve developed a mock implementation [vm-attest-proto] of the proposed [VM Instance RoT]. The tests in this implementation rely on an existing mock of the Oxide platform RoT. To support the collection of initial performance characteristics, the [attest-time] tool was developed to configure the mock VM instance RoT to talk to the Oxide platform RoT through the IPCC interface. This tool then makes repeated calls to one of the Oxide platform RoT commands emitting the duration as text.

Tests were carried out on gimlet hardware in one of our test environments. Data sets of 100000 samples were collected for each of the 3 functions in the API, on 4 different systems for comparison. The [m3vs] utility consumes these samples and calculates the min, max, mean, variance, and standard deviation from each. The results from each test follows:

Test Results

The attest command was tested in the 4 gimelets attached to the dublin environment. The raw data can be obtained from [attest-time-dublin-data]. The following table provides a summary (values in microseconds / µs & rounded to the nerest whole unit):

SNcommandminmaxmeanstd deviation

BRM23230010

attest

99449

309989

104540

5060

BRM23230010

get_cert_chains

102462

180704

120597

23501

BRM23230010

get_measurement_logs

29450

60019

33265

7047

BRM23230018

attest

98924

310066

104541

5204

BRM23230018

get_cert_chains

102786

180950

120526

23275

BRM23230018

get_measurement_logs

28990

59976

33276

7047

BRM27230037

attest

98899

309993

104540

5129

BRM27230037

get_cert_chains

102513

180163

120597

23479

BRM27230037

get_measurement_logs

29461

59979

33251

7047

BRM42220026

attest

91496

300134

104546

5008

BRM42220026

get_cert_chains

102796

181083

120648

23333

BRM42220026

get_measurement_logs

28869

60057

33223

7032

Analysis

The data above verifies a few expected characteristics:

The get_measurement_logs function is the fastest of the commands. This is expected for a few reasons:

  • this command requires no work in the RoT beyond serializing a single, relatively small data structure

  • this call results in a single message both two and from the RoT

The get_cert_chains function is the slowest of the commands. This is expected because cert chains aren’t small and it takes several messages to get the whole cert chain out of the RoT. This command is a good candidate for Optimization, likely though Caching.

Some of the less expected characteristics:

The extreme min & max for the attest function can most likely be attributed to the IPCC channel being busy. If we carve this data up into z-score bands we can see that all samples fall within 1 or 2 standard deviations from the mean. There are however a handfull of outliers in the 37th and 39th standard deviations from the mean. The code to do these calculations can be found in [sds-from-mean].

This appears to be an artifact of my test setup and the fact that remote access to these systems uses pilot to access the system console through the SP. My test script collected samples from the attest command first and so I believe several early samples are competing with my console traffic. I’m working to confirm this now.

The relatively high standard deviation in the get_cert_chains function is a bit surprising. This can probably be attributed to it being the only call that requires multiple messages exchanged with the RoT.

Optimization

Our initial implementation will include no optimizations for calls through this API. Each call will cause the VM instance RoT to do some work and then make the required calls into the Oxide platform RoT. We do however believe that some optimizations may be necessary and so we explore some options here.

Caching

Possible optimizations fall into two categories that are determined by whether or not the call provides guarantees of freshness. Both the get_cert_chains and the get_measurement_logs commands require no freshness guarantees. This presents the opportunity for the VM instance RoT to cache this data on behalf of the RoTs.

If a caching scheme is deployed for these API calls the VM instance RoT will need some mechanism to determine when the cache is out of date. When cert chains or measurement logs are out of date attestations from the associated RoT will fail. A naive scheme then could have the VM instance RoT verify each attestation before returning it to the VM. If an attestation fails to verify then it would refresh its cache and try again.

Batched Signing

The attest command takes a nonce from the challenger and produces a response entagled with this same nonce. Generally this requires that each call from the VM instance result in a call to each of the underlying RoTs. This rules out caching of responses but leaves the potential for batching requests.

To batch these requests we’d need to collect them and construct a merkle tree of the associated nonces. After some threshold the is passed the root digest from the merkle tree would be used to collect attestations in place of the nonce value. In this scheme additional work would be required from each appraiser as they would need to be provided with their branch of the merkle tree to verify the attestations.

Unfortunately this scheme breaks down significantly given our intention for propolis to act as the VM instance RoT. Each VM instance has a dedicated propolis instance and this would limit this batching scheme to requests from a single VM. The work and complexity of constructing a system that can better take advantage of such a mechanism is deferred until it can be justified.

Faster Algorithms

The implementation of the attest task in the Hubris RoT image has not, till this point, been benchmarked or optimized. The attest command is the only one where we perform cryptographic operations. Now that we have a ready means to collect performance data there are likely opportunities for improvement through the use of faster algorithms (sha3 → sha2) etc.

Determinations

  • vsock will be used as the transport for an attestation API exposed to instances by propolis

  • propolis will implement this API as described in API

  • Binding Keys to Attestations describes the method recommended for bootstrapping trust in ephemeral keys held by an instance

Open Questions

  • How to grow this architecture over time / add support for additional RoTs

  • The initial API is very simple but not flexible: do we need flexibility on any dimensions?

    • limit the subset of RoTs included in the attestation?

  • How to handle API breaking changes / upgrades.

    • Initial customer has agreed to accept breaking changes & coordinating accordingly.

    • Versioning this API is left as future work.

  • The process described in Binding Keys to Attestations needs a reference implementation.

Security Considerations

This feature is significant in that we are exposing a new interface to guest VM instances. While we trust our customers in many contexts, we must treat the software that they run in a rack as untrusted. This means we have created a new interface between the host OS and instances that must be defended.

A less direct collection of threats that we must consider are those that may undermine the veracity of the claims we’re making about this new API. These threats present a risk to ourselves and customers as users of this API: if it doesn’t have the properties that we believe it does then the trust we build between systems using this API may not be as strong as we think / claim. Such threats also present a risk of reputational damage for Oxide because we’re building and recommending the use of this API. We will look very foolish / incompetent if it doesn’t provide the properties that we claim. Such threats must be understood, mitigated to a satisfactory degree, and communicated to customers for use in their own risk analysis.

The remainder of this section will discuss these threats and the associated mitigations.

Defensive Programming

propolis must be capable of defending itself against misbehaving (intentionally or otherwise) VM instances. It must gracefully reject malformed requests from the associated instance while returning a sensible error message.

Denial of service through resource contention / exhaustion is another threat that we must consider. This can be mitigated through rate limiting use of the interface between propolis and the instance. propolis may rate limit this API in the future though our initial implementation will not. Customers should plan their use of this API to minimize contention (rate limiting within instances).

Incomplete Measurement Chain

Our initial implementation will include an attestation from the Oxide Platform RoT. The measurements reflected in this attestation are limited to the RoT and SP software images. With this information we can identify when a platform has booted legitimate Oxide RoT and SP software.

There is however a lot of software executing between these components and the VM Instance RoT. Until our measurement chain contains the [TCB] for the instance, consumers of this API cannot reasonably asses their trustworthiness. This implies that these components must be trusted implicitly which is contrary to our goals. Between the initial release of this feature and when our measured boot implementation reflects all [TCB] elements the risk associated with this implicit trust must be assumed.

An example of such a risk is present in the VM Instance RoT. This RFD proposes that the propolis software take on the additional responsibility of generating a log structure describing the instance boot_disk (if it’s read-only), and its UUID, then binding this data to an attestation from the Oxide Platform RoT.

Assume for a moment that in doing this work we introduce a bug in propolis that causes it to misreport this information. Assume further that some time after release this bug is discovered, fixed, and a new release is made. Appraisers should, through the attestation appraisal process, be able to differentiate between old vulnerable releases and newer ones where the bug has been fixed. Without a measurement of propolis in the attestation, appraisers will be unable to do so. Threats like this can only be mitigated by continuing to fill in our measured boot implementation until it is complete.

boot disk digest calculation

For this system to behave correctly propolis must reliably report the data that it observes. There are a number of ways to configure which disk is used to boot an instance in the rack however and from the perspective of propolis, some of these configurations are less deterministic than others. To ensure that propolis VM Instance RoT is able to reliably report the digest of the disk booted by an instance we must impose some restrictions.

boot disk & boot order

The oxide API does not require that an attached disk be explicitly identified as the boot disk. In this case, the boot order will be determined by the OVMF firmware on first boot. The instance may then modify the boot order through the UEFI interface [oxapi-boot-order]. In this configuration propolis cannot know which disk will be booted by the instance and so it cannot report a digest for the boot disk in an attestation.

When the oxide API is used to explicitly identify a boot disk [oxapi-boot-images] for an instance propolis configures OVMF to have a boot order that includes only this disk. If additional disks are assigned to the instance, and the boot disk fails to boot, OVMF will not attempt to boot from the other disks. This property is essential for our goals, though there is a caveat.

If OVMF fails to boot an OS from the boot disk, it will execute the UEFI shell and wait for user input. Someone with access to the console can at this point boot from any bootable media attached to the VM. This includes:

  • possibly another bootable block device if one is attached to the instance

  • PXE boot from the attached network device

In this situation the values reported by propolis are undefined. Customers must mitigate this risk by limiting access to the instance console.

read-only boot disk

A read only boot disk is not strictly required for propolis to accurately report it’s digest. A mutable disk can be changed arbitrarily by the instance after boot though. This requires that propolis delay booting the instance until the digest calculation is complete. This restriction would slow boot time for the instance proportional to the size of the disk.

This delay alone is probably unacceptable but even if it were, a mutable disk can be changed by the instance once it’s running. On subsequent boots the digest reported by propolis will change if the instance modifies its boot disk. While it’s possible for propolis to report this measurement accurately even if it changes across boots, managing and making sense of these changes is not something we encourage customers to do. Instead, their efforts would be better spent implementing an [immutable] image.

Code on Non-bootable Disk

After boot an instance may mount any number of disks that have been assigned to it. This could allow the execution of code not captured by a measurement of the boot disk. Code from the boot disk would have to mount the disk and execute code from it so a measurement of the boot disk will capture this behavior. Before placing trust in an instance the software executing within it must be analyzed to ensure it it trustworthy.

Atestation API as Building Block

The API described in this RFD allows instances to obtain an attestation from the platform while binding some data held by the instance to it. The instance can use this API to challenge the platform for an attestation & appraise the results directly but the output of this process is of little use. By deploying an instance on to the rack, the instance has already been configured to place a high level of trust in the sleds that make up the rack (propolis controls it’s memory & execution).

This API can however be a building block that an instance can use to convince a remove challenger that it can be trusted. The construction of such a protocol is left as an exercise to the customer. Incorrect use of this API in building such a protocol is a risk for us and customers. This could result in the compromise of customer data, and reputational damage for Oxide.

Mitigating this threat requires that we describe, and possibly provide a reference implementation of a secure protocol that uses this API and the attestation data correctly. The customer driving this initial implementation has implemented a protocol that they believe is sufficiently secure for their use-case. Future work by Oxide should provide a reference implementation demonstrating proper use of this API in a remote attestation protocol.

Of specific concern are instances with mutable boot disks. The attestation produced by such instances will fail to report a digest of their boot disk. Without this information, the threat of impersonation and interposition attacks will always exist as the challenger cannot know wither or not some intermediate is proxying commands through a vulnerable instance. This can be partially mitigated if the challenger knows the expected UUID of the instance being appraised. We say this is a partial mitigation because this mitigation will fail if the instance assigned said UUID is in collusion with or can be manipulated into attesting to arbitrary data.

External References