Signature-based Integrity

Draft Community Group Report,

This version:
https://wicg.github.io/signature-based-sri/
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google LLC.)
Toggle Diffs:

Abstract

A monkey-patch spec that enhances SRI with signature-based integrity checks. These are conceptually similar to the content-based checks currently defined, but have different properties that seem interesting to explore.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

Subresource Integrity [SRI] defines a mechanism by which developers can ensure that script or stylesheet loaded into their pages' contexts are exactly those scripts or stylesheets the developer expected. By specifying a SHA-256 hash of a resource’s content, any malicious or accidental deviation will be blocked before being executed. This is an excellent defense, but its deployment turns out to be brittle. If the resource living at a specific URL is dynamic, then content-based integrity checks require pages and the resources they depend upon to update in lockstep. This turns out to be ~impossible in practice, which makes SRI less usable than it could be.

Particularly as the industry becomes more interested in supply-chain integrity (see Shopify’s [PCIv4-SRI-Gaps], for instance), it seems reasonable to explore alternatives to static hashes that could allow wider deployment of these checks, and therefore better understanding of the application experiences that developers are actually composing.

This document outlines the changes that would be necessary to [Fetch], and [SRI] in order to support the simplest version of a signature-based check:

Pages will embed an Ed25519 public key assertion into integrity attributes:
<script src="https://my.cdn/script.js"
        crossorigin="anonymous"
        integrity="ed25519-[base64-encoded-public-key]"
        ...></script>

Servers will deliver a signature over the resource content using the corresponding private key along with the resource as an HTTP message signature [RFC9421]:

HTTP/1.1 200 OK
Accept-Ranges: none
Vary: Accept-Encoding
Content-Type: text/javascript; charset=UTF-8
Access-Control-Allow-Origin: *
Identity-Digest: sha-512=:[base64-encoded digest of `console.log("Hello, world!");`]:
Signature-Input: sig1=("identity-digest";sf); keyid="[base64-encoded public key]"; tag="sri"
Signature: sig1=:[base64-encoded result of Ed25519(signature base)]:

console.log("Hello, world!");

The user agent will validate the signature using the expected public key before executing the response.

That’s it!

The goal here is to flesh out the proposal for discussion, recognizing that it might be too simple to ship. Then again, it might be just simple enough...

1.1. Signatures are not Hashes

Subresource Integrity’s existing hash-based checks ensure that specific, known _content_ executes. It doesn’t care who made the file or from which server it was retrieved: as long as the content matches the expectation, we’re good to go. This gives developers the ability to ensure that a specific set of audited scripts are the only ones that can execute in their pages, providing a strong defense against some kinds of threats.

The signature-based checks described briefly above are different. Rather than validating that a specific script or stylesheet is known-good, they instead act as a proof of _provenance_ which ensures that scripts will only execute if they’re signed with a known private key. Assuming good key-management practices (easy, right?), this gives a guarantee which is different in kind, but similarly removes the necessity to trust intermediaries.

With these properties in mind, signature-based integrity checks aim to protect against attackers who might be able to manipulate the content of resources that a site depends upon, but who cannot gain access to the signing key.

1.2. High-Level Overview

The mechanism described in the remainder of this document can be broken down into a few independent parts, layered on top of one another to achieve the goals developers are aiming for.

  1. Server-initiated integrity checks: Servers can deliver an `Identity-Digest` header along with responses that contain one or more digests of the response’s content _after_ decoding any content codings (gzip, brotli, etc).

    If such a header is present, user agents can enforce it by synthesizing a network error if the delivered content does not match the asserted digest. See § 2.2 Patches to Fetch below for more details.

  2. Server-initiated signature checks: Servers can deliver HTTP Message Signature headers (`Signature` and `Signature-Input` from [RFC9421]) that allow the verification of request/response metadata. We can construct these headers in such a way that user agents can enforce them, and further ensure that the signed metadata includes the server-initiated integrity checks noted above. Enforcing signature verification, then, means ensuring that the private key’s possessor signed the specific content in question.

    See the verification requirements for SRI described below for more detail about these headers' construction.

  3. Client-initiated integrity checks: Pages need to be able to specify integrity metadata for script and link elements that can be matched against the server-initiated checks described above. The work necessary is described in § 2.1 Patches to SRI below.

  4. CSP-driven enforcement: As described in Content Security Policy 3 § 8.4 Allowing external JavaScript via hashes, it’s possible today to safely allow JavaScript execution by specifying integrity metadata on a given element, matching that metadata against a page’s active policies, and relying upon SRI to enforce the constraints the metadata declares. The same should be possible for signatures (and should fall out of CSP’s specification without much additional work).

    TODO(mkwst): Write up this integration, which requires at least a grammar update in CSP. [Issue #36]

Implementing the mechanism in this document therefore requires:

  1. Implementing `Identity-Digest` checks, at least for the subset of resource types upon which SRI can act: scripts and stylesheets.

  2. Implementing the subset of HTTP Message Signatures required to support the headers which meet the verification requirements for SRI.

  3. Implementing the patches against SRI necessary to support the new integrity types, described in § 2.1 Patches to SRI.

Revisiting the example above, the following things might happen to ensure that we’re only executing script correctly signed with a key we expect:

  1. Prior to sending the request, the page’s CSP will verify the content of the relevent script element’s integrity attribute, ensuring that any public keys asserted match the page’s requirements.

  2. The user agent receives response headers for https://my.cdn/script.js, parses the `Signature-Input` header, and uses it to verify the `Signature` header’s content, blocking the response if verification fails. This verification shows that we’ll only be dealing with responses for which we have proof that the private key’s possessor signed this response, including the integrity information.

  3. The user agent matches the public key contained in the `Signature-Input` header with the request’s integrity metadata, blocking the response if there’s a mismatch. This ensures that we’re meeting the page’s requirements for resource inclusion.

  4. Once the response has streamed in, we validate the integrity information contained in the `Identity-Digest` headers against the response body, refusing to execute any mismatched responses.

  5. We’re done, executing probably-safe JavaScript to our heart’s content.

2. Monkey Patches

Extending SRI to support signatures will require changes to three specifications, along with some additional infrastructure.

2.1. Patches to SRI

At a high level, we’ll make the following changes to SRI:

  1. We’ll define a profile of HTTP Message Signatures that meets the specific needs we have for this feature, specifying the requirements for signatures intended as proofs of integrity/provenance that can be enforced upon by clients without any pre-existing relationship to the server which delivered them. This requires locking down the components and properties of the signature itself, as well as some of the decision points available during the generation of the signature base

  2. We’ll define the accepted algorithm values. Currently, these are left up to user agents in order to allow for future flexibility: given that the years since SRI’s introduction have left the set of accepted algorithms and their practical ordering unchanged, we should define that explicitly.

  3. With known algorithms, we can adjust the prioritization model to return a set of the strongest content-based and signature-based algorithms specified in a given element. This would enable developers to specify both a hash and signature expectation for a resource, ensuring both that known resources load, and that they’re accepted by a trusted party.

  4. Finally, we’ll adjust the matching algorithm to correctly handle signatures by passing the public key in to the comparison operation.

The following sections add content and adjust algorithms accordingly.

2.1.1. The SRI HTTP Message Signature Profile

This document defines an HTTP Message Signature profile that specifies the requirements for signatures intended as proofs of integrity/provenance that can be enforced upon by clients without any pre-existing relationship to the server which delivered them. This requires locking down the components and properties of the signature itself, as well as some of the decision points available during the generation of the signature base (Section 2.5 of [RFC9421]).

At a high-level, the constraints are simple: this profile supports only Ed25519 signatures, requires that the public key portion of the verification key material be included in the signature’s input, and specifies the ordering of the components and properties to remove potential ambiguity about the signature’s construction. The rest of this section spells out those constraints more formally as the verification requirements for SRI, following the guidelines from Section 1.4 of [RFC9421]:

Components and Parameters:

The signature’s input MUST:

  1. Include the following component identifiers with their associated constraints:

    • identity-digest, which MUST include the sf parameter and no other parameters.

    Note: We’ll extend the set of allowed headers over time. The limitation to identity-digest is artificial, and aimed towards making a prototype of this approach as simple as possible to implement and evaluate as we decide what makes sense to ship at scale.

  2. Include the following signature parameters with their associated constraints:

    • keyid, whose value MUST be a string containing a base64 encoding of the public key portion of the signature’s verification key material.

    • tag, whose value MUST be the string sri

      Perhaps something more specific to make room for variants in the future that have different constraints? enforce-ed25519-provenance? ed25519-integrity? Etc? [Issue #34]

  3. Not include the alg signature parameter.

    Note: The algorithm can be determined unambigiously from the type, as this profile only supports Ed25519. Section 7.3.6 of RFC9421 suggests dropping the alg parameter in these cases, which is the recommendation we’re following here.

The signature’s input MAY include the following derived components as part of the list of component identifiers, each of which MUST include the req parameter and no other paramters:

The signature’s input MAY include the following signature parameters, with their associated constraints:

  • created, an integer whose value MUST represent a time in the past.

  • expires, an integer whose value MUST represent a time in the future.

  • nonce, which is a string whose value SHOULD be generated in a fashion which guarantees uniqueness.

Structured Field Types:
Retrieving the Key Material:

The public key of the verification key material can be directly extracted from the signature input’s keyid parameter, where it’s represented as a base64 encoded string.

Signature Algorithms:

The only signature algorithm identifier allowed is "ed25519", as defined in Section 3.3.6 of RFC9421.

Determine Key/Algorithm Appropriateness:

Since the only accepted algorithm is ed25519, it is appropriate for any context in which this profile will be used.

Derivation Context

The context for derivation of message components from an HTTP message and its application context is the HTTP message itself, encompassing the response with which the signature was delivered, and the request to which it responds.

Error Reporting from Verifier to Signer

No error reporting is required.

Clients MUST represent verification failures as network errors, consistent with [FETCH]'s handling of other server-specified constraints on the usage of response data.

Security Considerations

See § 5 Security Considerations.

Other

The HTTP Message Signature must be delivered with a response.

The `Identity-Digest` header must be valid for SRI.

When instructed to "determine an order" while constructing the signature base, clients and servers both MUST choose the same order as the `Signature-Input` header they consume or produce, respectively.

Valid `Signature-Input` header values would therefore include:
Note: These requirements are fairly draconian, allowing only a very small subset of the flexibility allowed by the HTTP Message Signature format. It is entirely probable that we can expand the scope of allowed signature inputs in the future, but as we’re figuring out how to do signature validation on the client it seems prudent to provide as much strict guidance as possible in order to keep the initial complexity under control.

For posterity, this set of requirements has a few helpful implications:

  1. Specifying the tag parameter as "sri" is a pretty clear signal that the developer is aiming to validate the integrity and/or provenance of a given subresource, and can therefore be reasonably expected to adhere to the set of constraints and processing instructions described in this document. Developers specifying that tag can be expected to be unsurprised when resources are blocked if their signatures don’t properly validate.

  2. Specifying the keyid parameter as a base64 encoding of the signer’s public key makes it possible for validation to be enforced whether or not the resource was requested from a page requiring integrity.

  3. Supporting only the "ed25519" algorithm is a good place to start as the keys are small and the algorithm is broadly supported. Choosing one algorithm simplifies initial implementations, and reduces the set of choices we ask developers to make about crypto primitives.

  4. The `Signature-Input` header is very flexible as specified, and most of the restrictions here aim to reduce its complexity as we gain implementation experience on both the client and server sides of the signature generation process. [RFC9421] leaves several important questions about the serialization of the "signature base" open to agreement between the signer and verifier: we’re locking most of those joints down here in order to ensure that we start with a simple story for both sides.

    To that end, we’re supporting signatures only over the one specific header necessary to meaningfully assert something about the resource’s body. We’re explicitly specifying strict serialization of that header, and we’re requiring it to be a header, not a trailer.

  5. In order to avoid potential disagreements between servers and clients about the serialization of a signature base for a given response, we’re specifying how both sides ought to "Determine an order for any signature parameters" through reference to the header as-delivered. Whatever order the server produces in the `Signature-Input` header is the order that the client will expect the signature base to represent.

2.1.1.1. Identity-Digest Validation for SRI

An `Identity-Digest` header (header) is valid for SRI if the following steps return "valid":

  1. Let parsed be the result of parsing structured fields with input_string set to header’s value, and header_type set to "dictionary".

  2. If parsing failed or if parsed is empty, return "invalid".

  3. For each keyvalue of parsed:

    1. If value is not a byte sequence, return "invalid".

    2. If key is not contained within the list « "sha-256", "sha-384", "sha-512" », return "invalid".

    3. If key is "sha-256", and value’s length is not 32, return "invalid".

    4. If key is "sha-384", and value’s length is not 48, return "invalid".

    5. If key is "sha-512", and value’s length is not 64, return "invalid".

  4. Return "valid".

2.1.2. Parse metadata.

First, we’ll define valid signature algorithms:

Then, we’ll adjust SRI’s Parse metadata. algorithm as follows:

This algorithm accepts a string, and returns a map containing one set of hash expressions whose hash functions are understood by the user agent, and one set of signature expressions which are likewise understood:

  1. Let result be the empty set the ordered map «[ "hashes" → « », "signatures" → « » ]».

  2. For each item returned by splitting metadata on spaces:

    1. Let expression-and-options be the result of splitting item on U+003F (?).

    2. Let algorithm-expression be expression-and-options[0].

    3. Let base64-value be the empty string.

    4. Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-).

    5. Let algorithm be algorithm-and-value[0].

    6. If algorithm-and-value[1] exists, set base64-value to algorithm-and-value[1].

    7. If algorithm is not a valid SRI hash algorithm token, then continue.
    8. Let metadata data be the ordered map «["alg" → algorithm, "val" → base64-value]».

    9. Append metadata to result.
    10. If algorithm is a valid SRI hash algorithm token, then append data to result["hashes"].
    11. Otherwise, if algorithm is a valid SRI signature algorithm token, then append data to result["signatures"].
  3. Return result.

2.1.3. Do bytes and response match metadataList?

Since we adjusted the result of § 2.1.2 Parse metadata. above, we need to adjust the matching algorithm to match. The core change will be processing both hashing and signature algorithms: if only one kind is present, the story will be similar to today, and multiple strong algorithms can be present, allowing multiple distinct resources. If both hashing and signature algorithms are present, both will be required to match. This is conceptually similar to the application of multiple Content Security Policies.

In order to validate signatures, we’ll need to change Fetch to pass in the relevant HTTP response header. For the moment, let’s simply pass in the entire response (response), as that makes the integration with [RFC9421] somewhat explicable.

To perform client-initiated integrity checks for a given byte sequence (bytes), request (request), and response (response), execute the following steps. They return "passed" or "failed":

  1. Let parsedMetadata be the result of executing Parse metadata on request’s integrity metadata.

  2. If both parsedMetadata["hashes"] and parsedMetadata["signatures"] are empty set, return "passed".

  3. Let hash-metadata be the result of executing SRI § 3.3.3 Get the strongest metadata from set on parsedMetadata["hashes"]..

  4. Let signature-metadata be the result of executing SRI § 3.3.3 Get the strongest metadata from set on parsedMetadata["signatures"].

  5. Let hash-match be true if hash-metadata is empty, and false otherwise.

  6. Let signature-match be true if signature-metadata is empty, and false otherwise.

  7. For each item in hash-metadata:

    1. Let algorithm be the item["alg"].

    2. Let expectedValue be the item["val"].

    3. Let actualValue be the result of SRI § 3.3.1 Apply algorithm to bytes on algorithm and bytes.

    4. If actualValue is a case-sensitive match for expectedValue, set hash-match to true and break.

  8. For each item in signature-metadata:

    1. Let algorithm be the item["alg"].

    2. Let public key be the item["val"].

    3. Let result be the result of validating an integrity signature over request and response using algorithm and public key.

    4. If result is "valid", set signature-match to true and break.

  9. Return "passed" if both hash-match and signature-match are true. Otherwise return "failed".

2.1.4. Validate a signature over response using algorithm and public key

The matching algorithm above calls into a new signature validation function. Let’s write that down. At core, it will execute the Ed25519 validation steps from [RFC8032] using signatures extracted from HTTP Message Signature headers defined in [RFC9421], then compare valid signatures against the expected public key.

To validate an integrity signature over a response response, string algorithm, and string public key, execute the following steps. They return valid if the signature is valid, or invalid otherwise.

  1. Let result be the result of verifying an HTTP Message Signature as defined in Section 3.2 of [RFC9421], given response as the signature context, the verification requirements for SRI, and the following processing instructions:

    1. When executing Step 1.1 of the verification algorithm referenced above, "determine which signature should be processed for this message" by evaluating all signatures whose input’s tag parameter is "sri".

    2. When executing Step 4 of the verification algorithm, use the verification requirements for SRI described above.

    3. When executing Step 5 of the verification algorithm:

      1. "Determine the verification key material" by base64 decoding the signature input’s keyid parameter.

      2. "Determine the trustworthiness of the key material" by comparing the signature input’s keyid parameter to public key. If the two do not match, fail validation for this signature.

    4. Assert: When executing Step 6, the result of "Determine the algorithm" is "ed25519" due to the verification requirements for SRI applied above.

  2. If result is failure, return "invalid".

  3. Otherwise, return "valid".

2.2. Patches to Fetch

Support for this feature would require changes to Fetch § 4.1 Main fetch to support enforcement of server-initiated integrity checks through `Identity-Digest`, `Signature`, and `Signature-Input`, and to pass the right set of information into the version of SRI § 3.3.4 Do bytes match metadataList? altered by this specification in order to enable signature-based checks that require information from the request (integrity metadata on the one hand, request headers and properties for signature components on the other) and the response (integrity headers and the body).

It would also require changes to Fetch § 4.5 HTTP-network-or-cache fetch to support setting the `Accept-Signature` header on outgoing requests based on their integrity metadata.

2.2.1. Main Fetch

Fetch § 4.1 Main fetch step 22 will be updated as follows:

  1. If request’s integrity metadata is not the empty string, or internalResponse’s header list contains `Identity-Digest`, then:
    1. Let processBodyError be this step: run fetch response handover given fetchParams and a network error.
    2. If response’s body is null, then run processBodyError and abort these steps.
    3. Let processBody given bytes be these steps:
      1. Perform server-initiated integrity checks on bytes, request, and internalResponse. If the result is "failed", then run processBodyError and abort these steps.
      2. If bytes do not match request’s integrity metadata Perform client-initiated integrity checks given request and internalResponse. If the result is "failed", then run processBodyError and abort these steps. [SRI]
      3. Set response’s body to bytes as a body.
      4. Run fetch response handover given fetchParams and response.

2.2.2. HTTP-network-or-cache Fetch

Fetch § 4.5 HTTP-network-or-cache fetch will be updated by injecting the following step between the existing step 13 and 14:

  1. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA]

  2. Append the Accept-Signature header for httpRequest.
  3. If httpRequest’s initiator is "prefetch", then set a structured field value given (<a http-header><code>Sec-Purpose</code></a>, the token prefetch) in httpRequest’s header list.

2.2.2.1. Append Accept-Signature

When a request's integrity metadata contains signature-based assertions, user agents will attach `Accept-Signature` headers to the request to inform servers about the client’s expectations. The header’s value will match the grammar defined in [RFC9421], and contain the expected public key(s) as keyid parameters.

A request generated from the following HTML element:
<script src="https://my.cdn/script.js"
        crossorigin="anonymous"
        integrity="ed25519-JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs="></script>

would contain the following header:

Accept-Signature: sig0=("identity-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";type="sri"

If multiple keys are acceptable (e.g. to support key rotation), the `Accept-Signature` header will contain multiple acceptable signatures. That is, the following HTML:

<script src="https://my.cdn/script.js"
        crossorigin="anonymous"
        integrity="ed25519-JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=
                   ed25519-xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE="></script>

would produce the following header in its request:

NOTE: '\' line wrapping per RFC 8792

Accept-Signature: sig0=("identity-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";type="sri" \
                  sig1=("identity-digest";sf);keyid="xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE=";type="sri"

To append the Accept-Signature header for a request (request):

  1. If request’s header list contains `Accept-Signature`, return.

    Note: Developers can set an `Accept-Signature` header for use by their own application. In this case, the user agent will not set additional `Accept-Signature` headers, and may perform a CORS-preflight request.

  2. If request’s integrity metadata is empty, return.

  3. Let parsed be the result of executing Parse metadata on request’s integrity metadata.

  4. If parsed["signatures"] is empty, return.

  5. Let counter be 0.

  6. For each signature in parsed["signatures"]:

    1. Let value be the concatenation of « `sig`, counter, `=("identity-digest";sf);keyid="`, signature["val"], `";type="sri"` ».

    2. Append (`Accept-Signature`, value) to request’s header list.

To support this change, we also need to clean up the processing in CORS-safelisted request-header to support this header, as discussed in [Issue #21]

2.3. Server-Initiated Integrity Checks

Note: This set of algorithms could live either in [Fetch] or [SRI].

To perform server-initiated integrity checks given a byte sequence (bytes), a request (request), and a response (response), execute the following steps. They return "passed" or "failed" as appropriate:

  1. Verify Identify-Digest assertions for bytes and response. If the result is "failed", return "failed".

  2. Verify SRI Message Signature assertions for request, and response. If the result is "failed", return "failed".

  3. Return "passed".

2.3.1. Identity-Digest Validation

To verify Identity-Digest assertions given a byte sequence (bytes) and a response (response), execute the following steps. They return "verified" or "failed":
  1. Let header be the result of getting the `Identity-Digest` header as a "dictionary" from response’s header list.

  2. If header is null, return "verified".

  3. For each algdigest of header:

    1. If alg is not one of "sha-256", "sha-384", or "sha-512", then continue.

    2. Let body digest be the result of executing SRI § 3.3.1 Apply algorithm to bytes on alg and bytes.

    3. If body digest matches digest, continue.

    4. Return "failed".

  4. Return "verified".

Note: This algorithm requires all valid digests delivered via `Identity-Digest` to match the response’s decoded body. Since the server controls both the body and the headers, it seems unnecessary to allow the flexibility of allowing the asserted digests to match more than one resource (as we do in client-initiated checks, which need to support servers' content negotiation).

2.3.2. Signature and Signature-Input Enforcement

To verify SRI Message Signature assertions given a request (request), and a response (response), execute the following steps. They return "verified" or "failed":
  1. Let inputs be the result of getting the `Signature-Input` header as a "dictionary" from response’s header list.

  2. Let signatures be the result of getting the `Signature` header as a "dictionary" from response’s header list.

  3. For each keycomponents of inputs:

    1. If signatures does not contain key, continue.

    2. If any of the following requirements for components are not met, continue:

      1. components is a parameterized Inner List.

      2. components size is 1.

      3. components[0] is the string "identity-digest".

      4. components[0] has a single parameter: sf.

    3. Let params be components parameters.

    4. If any of the following requirements for params are not met, continue:

      1. params does not contain alg.

      2. params contains keyid, and its value is a string which, when forgiving-base64 decoded, returns a byte sequence whose length is 32.

      3. params contains tag, and its value is the string "sri".

    5. If params contains expires, and params["expires"] is greater than the number of seconds between the Unix epoch and the unsafe current time, return "failed".

      Should we try to leave some flexibility here for user agents to accept recently-expired signatures / deal with clock skew? [Issue #34]

    6. Let signature-params be the result of executing the algorithm in Section 2.3 of [RFC9421] on components.

    7. Let signature base be the result of executing the algorithm in Section 2.5 of [RFC9421] on request, |response, and signature-params.

      If this algorithm produces an error, return "failed".

      Note: Errors here might represent invalid component or parameter names, missing headers, etc.

    8. Let public key be the result of forgiving-base64 decoding params["keyid"].

    9. Execute Ed25519’s "Verify" algorithm as defined in Section 5.1.7 of [RFC8023], to verify the signature signatures["key"] over the message signature base using public key.

    10. If verification failed, return "failed".

  4. Return "verified".

Note: This is a reformulation and simplification of the steps described in Section 3.2 of [RFC9421], making the integration with the § 2.1.1 The SRI HTTP Message Signature Profile described above explict.

Note: This algorithm requires all valid signatures delivered with the response to be verified in order to return "verified"

3. Deployment Scenarios

This section is non-normative.

Signature-based SRI is meant to be a general primitive that can be used in a wide variety of ways that we can’t possibly exhaustively document. But below we document a few different scenarios for how signature-based SRI can be used to enable new functionality for the web.

3.1. Non-versioned third-party libraries

The web is built on composability and it is quite common to include JS from third-parties (e.g. analytics scripts or tools for real user monitoring). These scripts are often non-versioned to allow third-parties to continually update and improve these libraries. Signature-based SRI makes it possible to enable integrity validation for these libraries, to ensure that the included libraries are built and served in a trustworthy manner.

3.1.1. Architectural Notes

In this deployment scenario, third-party.com/library.js would deploy signature-based SRI. third-party.com would then document that when including the library, reliant websites should specify integrity="ed25519-[base64-encoded-public-key]".

If third-party.com offers multiple different libraries for different purposes, it is recommended to use isolated keys for each library. This ensures that an attacker can’t swap in third-party.com/foo.js for third-party.com/bar.js.

3.2. Protecting first-party libraries

An alternate deployment scenario is a site using this to protect first-party resources. In many cases, hash-based SRI can work well for first-party use cases. But, some sites have deploy processes where they deploy the main-page separately from subresources, in which case it is possible for any hashes specified in the main-page to become out of date with the contents of subresources. Signature-based SRI makes it possible to enable integrity validation for these first-party resources without adding any constraints on how web apps are deployed.

4. Deployment Considerations

This section is non-normative.

4.1. Key Management

Key management is hard. This proposal doesn’t change that.

It aims instead to be very lightweight. Perhaps it errs in that direction, but the goal is to be the simplest possible mechanimsm that supports known use-cases.

A different take on this proposal could be arbitrarily complex, replicating aspects of the web PKI to chain trust, allow delegation, etc. That seems like more than we need today, and substantially more work. Perhaps something small is good enough?

4.2. Key Rotation

Since this design relies on websites pinning a specific public key in the integrity attribute, this design does not easily support key rotation. If a signing key is compromised, there is no easy way to rotate the key and ensure that reliant websites check signatures against an updated public key.

For now, we think this is probably enough. If the key is compromised, the security model falls back to the status quo web security model, meaning that the impact of a compromised key is limited. In the future if this does turn out to be a significant issue, we could also explore alternate designs that do support key rotation. One simple proposal could be adding support for the client to signal the requested public key in request headers, allowing different parties to specify different public keys. A more complex proposal could support automated key rotation.

Note: This proposal does support pinning multiple keys for a single resource, so it will be possible to support rotation in a coordinated way without requiring each entity to move in lockstep.

4.3. Key Discovery

Servers that support this feature need to include the public key used to validate a resource’s signature in the `Signature-Input` header’s keyid signature parameter. Developer who wish to enforce signature validation against a particular key can do so by requesting the relevant resource, and extracting the key from its headers and inserting it into, for example, a script's integrity attribute.

5. Security Considerations

This section is non-normative.

5.1. Secure Contexts

SRI does not require a secure context, nor does it apply only to resources delivered via encrypted and authenticated channels. That means that it’s entirely possible to believe that SRI offers a level of protection that it simply cannot aspire to. Signatures do not change that calculus.

Thus, it remains recommended that developers rely on integrity metadata only within secure contexts. See also [SECURING-WEB].

5.2. Provenance, not Content

Signatures do not provide any assurance that the content delivered is the content a developer expected. They ensure only that the content was signed by the expected entity. This could allow resources signed by the same entity to be substituted for one another in ways that could violate developer expectations.

In some cases, developers can defend against this confusion by using hashes instead of signatures (or, as discussed above, both hashes and signatures). Servers can likewise defend against this risk by minting fresh keys for each interesting resource. This, of course, creates more key-management problems, but it might be a reasonable tradeoff.

5.3. Rollback Attacks

The simple signature checks described in this document only provide proof of provenance, ensuring that a given resource was at one point signed by someone in posession of the relevant private key. It does not say anything about whether that entity intended to deliver a given resource to you now. In other words, these checks do not prevent rollback/downgrade attacks in which old, known-bad versions of a resource might be delivered, along with their known signatures.

This might not be a problem, depending on developers' use cases. If it becomes a problem, it seems possible to add mitigations in the future. These could take various forms. For example:

  1. We could allow developers to require an "expires" parameter in the `Signature-Input` field, and adjust our verification requirements for SRI to enforce against it. Similarly, we could enforce a maximum age based on the inclusion of a "created" parameter.

  2. We could require additional components of the request to be included in the signature ("content-type", "@path;req", "@method;req", etc) in order to reduce the scope of potential resource substitution.

  3. We could allow developers to send a challenge along with the request (as an `Accept-Signature` parameter), and require that it be incorporated into the `Signature-Input`'s "nonce" parameter.

We’d want to evaluate the tradeoffs in these and other approaches (the last, for example, makes offline signing difficult), of course, but they seem quite plausibly valuable as future enhancements.

6. Privacy Considerations

This section is non-normative.

Given that the validation of a response’s signature continues to require the response to opt-into legibility via CORS, this mechanism does not seem to add any new data channels from the server to the client. The choice of private key used to sign the resource is potentially interesting, but doesn’t seem to offer any capability that isn’t possible more directly by altering the resource body or headers.

7. An End-to-End Example

The following example walks through the process a developer might go through to sign a given resource. Let’s start with the following JSON response:

HTTP/1.1 200 OK
Date: Tue, 20 Apr 2021 02:07:56 GMT
Content-Type: application/json
Content-Length: 18

{"hello": "world"}

First, the developer would deliver information to the client that would support an integrity check upon receipt. To do so, they’ll generate a digest over the response’s body:

user@host:~/path$  echo -n "{\"hello\": \"world\"}" | openssl dgst -binary -sha256 | base64
X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=

And send that digest along with the response via an `Identity-Digest` header;

HTTP/1.1 200 OKDate: Tue, 20 Apr 2021 02:07:56 GMTContent-Type: application/jsonContent-Length: 18Identity-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:{"hello": "world"}

Next, the developer will pick an Ed25519 public/private key pair to use when signing the response. Assume that they (through amazing coincidence!) generate the same key pair that’s used as an example in [RFC9421], section B.2:

user@host:~/path$  openssl genpkey -algorithm ed25519 -out /tmp/tmp_key.pem
user@host:~/path$  cat /tmp/tmp_key.pem
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF
-----END PRIVATE KEY-----
user@host:~/path$  openssl pkey -in /tmp/tmp_key.pem -pubout
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=
-----END PUBLIC KEY-----

For the next step, we’ll need a base64 encoding of the public key’s raw bytes. There’s unfortunately not a trivial way to extract that from the PKCS#8-encoded PEM format above, but the following tiny Python script will do the work:

from cryptography.hazmat.primitives import serialization
import base64

with open("/tmp/tmp_key.pem", "rb") as pem:
    public_key = serialization.load_pem_private_key(
        pem.read(), password=None
    ).public_key()
    byte_string = base64.b64encode(public_key.public_bytes_raw())
    print(byte_string.decode("utf-8"))

With that encoding in hand, the developer can construct a `Signature-Input` header that specifies the `Identity-Digest` header as a signed component, and includes the base64-encoded public key as the keyid parameter (as discussed in [#profile]):

HTTP/1.1 200 OKDate: Tue, 20 Apr 2021 02:07:56 GMTContent-Type: application/jsonContent-Length: 18Identity-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:Signature-Input: signature=("identity-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"{"hello": "world"}

Now we have everything we need to construct the signature base, following the steps described in Section 2.5 of [RFC9421], and choosing the same order as the header’s ordering each time it instructs us to "determine an order" in Section 2.3 of [RFC9421]. We’ll end up with:

"identity-digest";sf: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"@signature-params": ("identity-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"

That’s the string we’ll sign, placing the base64-encoded signature into a `Signature` header on the response:

HTTP/1.1 200 OKDate: Tue, 20 Apr 2021 02:07:56 GMTContent-Type: application/jsonIdentity-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:Content-Length: 18Signature-Input: signature=("identity-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"Signature: signature=:eTKYITprfJYJmsOZlRTmu0szHbt0yLxHYBU0oXDdkx8najLl59IPO0zUofe5T23RGuquHLdZx177tBX45CUcAg==:{"hello": "world"}

Done!

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSP3]
Mike West; Antonio Sartori. Content Security Policy Level 3. URL: https://w3c.github.io/webappsec-csp/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[FETCH-METADATA]
Mike West. Fetch Metadata Request Headers. URL: https://w3c.github.io/webappsec-fetch-metadata/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[ID.pardue-httpbis-identity-digest]
Lucas Pardue; Mike West. HTTP Identity Digest. URL: https://lpardue.github.io/draft-pardue-http-identity-digest/draft-pardue-httpbis-identity-digest.html
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[RFC8023]
M. Thomas; A. Mankin; L. Zhang. Report from the Workshop and Prize on Root Causes and Mitigation of Name Collisions. November 2016. Informational. URL: https://www.rfc-editor.org/rfc/rfc8023
[RFC8032]
S. Josefsson; I. Liusvaara. Edwards-Curve Digital Signature Algorithm (EdDSA). January 2017. Informational. URL: https://www.rfc-editor.org/rfc/rfc8032
[RFC9421]
A. Backman, Ed.; J. Richer, Ed.; M. Sporny. HTTP Message Signatures. February 2024. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc9421
[RFC9651]
M. Nottingham; P-H. Kamp. Structured Field Values for HTTP. September 2024. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc9651
[SRI]
Devdatta Akhawe; et al. Subresource Integrity. URL: https://w3c.github.io/webappsec-subresource-integrity/

Informative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[PCIv4-SRI-Gaps]
Yoav Weiss; Ilya Grigorik. PCIv4: SRI gaps and opportunities. URL: https://docs.google.com/document/d/1RcUpbpWPxXTyW0Qwczs9GCTLPD3-LcbbhL4ooBUevTM/edit?usp=sharing
[SECURING-WEB]
Mark Nottingham. Securing the Web. TAG Finding. URL: https://www.w3.org/2001/tag/doc/web-https

Issues Index

TODO(mkwst): Write up this integration, which requires at least a grammar update in CSP. [Issue #36]
Perhaps something more specific to make room for variants in the future that have different constraints? enforce-ed25519-provenance? ed25519-integrity? Etc? [Issue #34]
To support this change, we also need to clean up the processing in CORS-safelisted request-header to support this header, as discussed in [Issue #21]
Should we try to leave some flexibility here for user agents to accept recently-expired signatures / deal with clock skew? [Issue #34]