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); 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.
-
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.
-
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).
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:
-
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.
-
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.
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. -
-
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 stringsri
Perhaps something more specific to make room for variants in the future that have different constraints?
enforce-ed25519-provenance
?ed25519-integrity
? Etc? [Issue #34]
-
-
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 thealg
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:
-
- Structured Field Types:
-
-
The
identity-digest
component references the `Identity-Digest
` header defined in [ID.pardue-httpbis-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 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
- 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.
Signature-Input
` header values would therefore include:
-
("identity-digest";sf);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. -
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. -
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" 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
":
-
Let parsed be the result of parsing structured fields with
input_string
set to header’s value, andheader_type
set to "dictionary
". -
If parsing failed or if parsed is empty, return "
invalid
". -
For each key → value of parsed:
-
If value is not a byte sequence, return "
invalid
". -
If key is not contained within the list « "sha-256", "sha-384", "sha-512" », return "
invalid
". -
If key is "
sha-256
", and value’s length is not 32, return "invalid
". -
If key is "
sha-384
", and value’s length is not 48, return "invalid
". -
If key is "
sha-512
", and value’s length is not 64, return "invalid
".
-
-
Return "
valid
".
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.
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
":
-
Let parsedMetadata be the result of executing Parse metadata on request’s integrity metadata.
-
If both parsedMetadata["
hashes
"] and parsedMetadata["signatures
"] are empty set, return "passed
". -
Let hash-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 hash-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, set hash-match to
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 request and response using algorithm and public key.
-
If result is "
valid
", set signature-match totrue
and break.
-
-
Return "
passed
" if both hash-match and signature-match aretrue
. 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.
-
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:
-
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
". -
When executing Step 4 of the verification algorithm, use the verification requirements for SRI described above.
-
When executing Step 5 of the verification algorithm:
-
"Determine the verification key material" by base64 decoding the signature input’s
keyid
parameter. -
"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.
-
-
Assert: When executing Step 6, the result of "Determine the algorithm" is "
ed25519
" due to the verification requirements for SRI applied above.
-
-
If result is failure, return "
invalid
". -
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:
-
If request’s integrity metadata is not the empty string,
or internalResponse’s header list contains `
Identity-Digest
`, then:- Let processBodyError be this step: run fetch response handover given fetchParams and a network error.
- If response’s body is null, then run processBodyError and abort these steps.
-
Let processBody given bytes be these steps:
-
Perform
server-initiated integrity checks on bytes, request, and internalResponse.
If the result is "
failed
", then run processBodyError and abort these steps. -
If bytes do not match request’s integrity metadataPerform client-initiated integrity checks given request and internalResponse. If the result is "failed
", then run processBodyError and abort these steps. [SRI] - Set response’s body to bytes as a body.
- Run fetch response handover given fetchParams and response.
-
Perform
server-initiated integrity checks on bytes, request, and internalResponse.
If the result is "
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:
-
Append the Fetch metadata headers for httpRequest. [FETCH-METADATA]
-
Append the
Accept-Signature
header for httpRequest. -
If httpRequest’s initiator is "
prefetch
", then set a structured field value given (<a http-header><code>Sec-Purpose</code></a>
, the tokenprefetch
) 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.
< 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):
-
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. -
If request’s integrity metadata is empty, return.
-
Let parsed be the result of executing Parse metadata on request’s integrity metadata.
-
If parsed["
signatures
"] is empty, return. -
Let counter be 0.
-
For each signature in parsed["
signatures
"]:-
Let value be the concatenation of « `
sig
`, counter, `=("identity-digest";sf);keyid="
`, signature["val
"], `";type="sri"
` ». -
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:
-
Verify
Identify-Digest
assertions for bytes and response. If the result is "failed
", return "failed
". -
Verify SRI Message Signature assertions for request, and response. If the result is "
failed
", return "failed
". -
Return "
passed
".
2.3.1. Identity-Digest
Validation
Identity-Digest
assertions given a byte sequence (bytes) and a response (response), execute the
following steps. They return "verified
" or "failed
":
-
Let header be the result of getting the `
Identity-Digest
` header as a "dictionary
" from response’s header list. -
If header is
null
, return "verified
". -
For each alg → digest of header:
-
If alg is not one of "sha-256", "sha-384", or "sha-512", then continue.
-
Let body digest be the result of executing SRI § 3.3.1 Apply algorithm to bytes on alg and bytes.
-
If body digest matches digest, continue.
-
Return "
failed
".
-
-
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
SRI
Message Signature assertions given a request (request), and a response (response), execute the
following steps. They return "verified
" or "failed
":
-
Let inputs be the result of getting the `
Signature-Input
` header as a "dictionary
" from response’s header list. -
Let signatures be the result of getting the `
Signature
` header as a "dictionary
" from response’s header list. -
For each key → components of inputs:
-
If any of the following requirements for components are not met, continue:
-
Let params be components parameters.
-
If any of the following requirements for params are not met, continue:
-
params does not contain
alg
. -
params contains
keyid
, and its value is a string which, when forgiving-base64 decoded, returns a byte sequence whose length is 32.
-
-
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]
-
Let signature-params be the result of executing the algorithm in Section 2.3 of [RFC9421] on components.
-
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.
-
Let public key be the result of forgiving-base64 decoding params["
keyid
"]. -
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. -
If verification failed, return "
failed
".
-
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:
-
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.
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 OK Date : Tue, 20 Apr 2021 02:07:56 GMT Content-Type : application/json Content-Length : 18 Identity-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 OK Date : Tue, 20 Apr 2021 02:07:56 GMT Content-Type : application/json Content-Length : 18 Identity-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 OK Date : Tue, 20 Apr 2021 02:07:56 GMT Content-Type : application/json Identity-Digest : sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: Content-Length : 18 Signature-Input : signature=("identity-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri" Signature : signature=:eTKYITprfJYJmsOZlRTmu0szHbt0yLxHYBU0oXDdkx8najLl59IPO0zUofe5T23RGuquHLdZx177tBX45CUcAg==: { "hello" : "world" }
Done!