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:
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); alg="Ed25519"; keyid="[base64-encoded public key]"; tag="sri" Signature : sig1=:[base64-encoded result of Ed25519(`console.log("Hello, world!");`)]: 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 achive the goals developers are aiming for.
-
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 transfer encodings (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.1 Identity-Digest Enforcement below for more details.
-
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.
-
Client-initiated integrity checks: Pages need to be able to specify integrity metadata for
script
andlink
elements that can be matched against the server-initiated checks described above. The work necessary is described in § 2.1 Patches to SRI below. -
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).
Implementing the mechanism in this document therefore requires:
-
Implementing `
Identity-Digest
` checks, at least for the subset of resource types upon which SRI can act: scripts and stylesheets. -
Implementing the subset of HTTP Message Signatures required to support the headers which meet the verification requirements for SRI.
-
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:
-
Prior to sending the request, the page’s CSP will verify the content of the relevent
script
element’sintegrity
attribute, ensuring that any public keys asserted match the page’s requirements. -
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. -
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. -
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. -
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:
-
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
-
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.
-
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.
This might not be necessary. It allows us to explain things like packaging constraints in ways that seem useful, but does introduce some additional complexity in developers' mental model. So, consider it a decision point.
-
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:
-
Include the following component identifiers with their associated constraints:
-
identity-digest
, which MUST include thesf
parameter and no other parameters.
-
-
Include the following signature parameters with their associated constraints:
-
alg
, whose value MUST be the stringed25519
-
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 stringsri
Perhaps something more specific to make room for variants in the future that have different constraints?
enforce-ed25519-provenance
?
-
-
Order the signature’s parameters alphabetically.
The signature’s input MAY include the following signature parameters, with their associated constraints:
-
- Structured Field Types:
-
-
The
identity-digest
component references the `Identity-Digest
` header defined in [ID.pardue-http-identity-digest]. It is a Dictionary Structured Field.
-
- 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 allowed is
ed25519
. - Determine Key/Algorithm Appropriateness:
-
Since the only accepted algorithm is
ed25519
, it is appropriate for any context in which this profile will be used, and we require it to be specified as thealg
parameter to the signature’s input. - 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
Signature-Input
` header values would therefore include:
-
("identity-digest";sf);alg="ed25519";keyid="MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"
For posterity, this set of requirements has a few helpful implications:
-
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. -
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. -
Specifying the
alg
parameter as "ed25519
" 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. -
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.
-
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" by locking in alphabetical sorting of the signature’s parameters. We really only need to do this when generating the signature base: the `
Signature-Input
` header could in theory be arbitrarily sorted. In practice, however, it seems prudent to ensure that we make the expected representation as clear as possible.
2.1.2. Parse metadata.
First, we’ll define valid signature algorithms:
-
The valid SRI signature algorithm token set is the ordered set « "
ed25519
" » (corresponding to Ed25519 [RFC8032]). - A string is a valid SRI signature algorithm token if its ASCII lowercase is contained in the valid SRI signature algorithm token set.
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:
-
Let result be
the empty setthe ordered map «[ "hashes" → « », "signatures" → « » ]». -
For each item returned by splitting metadata on spaces:
-
Let expression-and-options be the result of splitting item on U+003F (?).
-
Let algorithm-expression be expression-and-options[0].
-
Let base64-value be the empty string.
-
Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-).
-
Let algorithm be algorithm-and-value[0].
-
If algorithm-and-value[1] exists, set base64-value to algorithm-and-value[1].
-
If algorithm is not a valid SRI hash algorithm token, then continue. -
Let
metadatadata be the ordered map «["alg" → algorithm, "val" → base64-value]». -
Append metadata to result. -
If algorithm is a valid SRI hash algorithm token, then append data to result["
hashes
"]. -
Otherwise, if algorithm is a valid SRI signature algorithm token, then append data to result["
signatures
"].
-
-
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:
-
Let parsedMetadata be the result of executing [[SRI#parse-metadata]|] on metadataList.
-
If both parsedMetadata ["
hashes
"] and |parsedMetadata["signatures
"] are empty set, returntrue
. -
Let
metadatahash-metadata be the result of executing SRI § 3.3.3 Get the strongest metadata from set on parsedMetadata ["hashes
"] .. -
Let signature-metadata be the result of executing SRI § 3.3.3 Get the strongest metadata from set on parsedMetadata["
signatures
"]. -
Let hash-match be
true
if hash-metadata is empty, andfalse
otherwise. -
Let signature-match be
true
if signature-metadata is empty, andfalse
otherwise. -
For each item in
metadatahash-metadata :-
Let algorithm be the item["alg"].
-
Let expectedValue be the item["val"].
-
Let actualValue be the result of SRI § 3.3.1 Apply algorithm to bytes on algorithm and bytes.
-
If actualValue is a case-sensitive match for expectedValue,
returnset hash-match totrue
true
and break.
-
-
For each item in signature-metadata:
- Let algorithm be the item["alg"].
- Let public key be the item["val"].
- Let result be the result of validating an integrity signature over response using algorithm and public key.
-
If result is
true
, set signature-match totrue
and break.
-
ReturnReturnfalse
.true
if both hash-match and signature-match aretrue
. Otherwise returnfalse
.
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].
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.
-
If algorithm is not an ASCII case-insensitive match for "ed25519", then return "
invalid
". -
Verify an HTTP message signature as defined in Section 3.2 of [RFC9421], given response and the verification requirements for SRI.
Note: When we support more than one algorithm, we’ll want to change this from a hard-coded string to a mapping between case-insensitive and canonical strings.
-
If a signature can be validated, return "
valid
". -
Otherwise, return "
invalid
".
2.2. Patches to Fetch
Fetch needs to change in two ways:
First, the trivial change: Step 22.3.1 of Fetch § 4.1 Main fetch should be updated as follows:
-
If bytes do not match request’s integrity metadata and response , then run processBodyError and abort these steps. [SRI]
2.2.1. Identity-Digest
Enforcement
Next, the more complicated change: Fetch needs to enforce assertions about
resource integrity made via `Identity-Digest
` headers.
TODO(mkwst): Spell out how that enforcement works.
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.
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:
-
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. -
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. -
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.