Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reject or ignore unknown signature metadata parameters? #38

Open
mikewest opened this issue Dec 20, 2024 · 9 comments
Open

Reject or ignore unknown signature metadata parameters? #38

mikewest opened this issue Dec 20, 2024 · 9 comments

Comments

@mikewest
Copy link
Member

@yoavweiss asked about the behavior for unknown signature parameters, and whether rejecting a signature that contains unknown parameters is the behavior we want.

I think we have a few options here:

  1. We could accept any and all parameters, simply ignoring those we don't understand. I think we'd need to include them in the signature base to ensure that the signature remained consistent between user agents that supported the new parameter and those that didn't, which would only really be possible if the parameters didn't have an effect on the signature (e.g. changing or acting as inputs to the signature algorithm, etc). That doesn't seem like an unreasonable thing to do, but does feel like it can cause some problems (e.g. typoing expires would simply break expiration rather than breaking the signature in a way developers would notice).

  2. We could exclude signatures that contain new parameters from our defined profile, ignoring them when performing validation. This is what's currently implemented in Chromium's prototype.

Both 1 and 2 allow evolution: 1 by simply adding new parameters to a signature that are transparently ignored in older browsers, 2 by delivering multiple signatures with and without the new parameter.

2 is more or less the same mechanism we'll be using for key rotation, so it's a joint that we're going to expect developers to be familiar with. I think I'm comfortable relying on it for agility here too, but I'd be interested in others' opinions.

@punkeel
Copy link

punkeel commented Jan 1, 2025

Happy new year!

(I think we agree, so this just to share/clarify my understanding.)

I think we'd need to include them in the signature base to ensure that the signature remained consistent between user agents that supported the new parameter and those that didn't, which would only really be possible if the parameters didn't have an effect on the signature (e.g. changing or acting as inputs to the signature algorithm, etc).

For signature components: it'll never be possible, the user-agent needs to know what to include from the request.

For signature parameters, though: my understanding is that this is the behavior described by RFC 9421, which is possible because parameters are self-contained key="value" pairs.

My intuition is that signature parameters bind/harden/enforce extra requirements on the signature. These extra requirements are signed (great! would be lame if not), and it's fine1 to ignore them otherwise. alg is the exception as it's a breaking change that fails closed (the resulting signature is invalid).

1 for some very loose definition of fine, it's of course better to check/validate the values we know about! :)

typoing expires would simply break expiration rather than breaking the signature in a way developers would notice

Having expires in the Accept-Signature would catch and prevent that.

@mikewest
Copy link
Member Author

mikewest commented Jan 7, 2025

I agree with you regarding components. We need to define how they're serialized into the signature base, so there's little we can do when presented with an unknown component other than punting on the signature entirely.

For parameters, examples might be helpful. Given the following Signature-Input header with an unknown parameter:

Signature-Input: sig=("identity-digest";sf);keyid="...";type="sri";unknown="value"

1 above would mean that we generate the following signature base and validate signatures against it:

"identity-digest";sf: sha-256=:...:
"@signature-params": ("identity-digest";sf);keyid="...";tag="sri";unknown="value"

While 2 above would mean that we ignore the signature entirely.

I'm suggesting that it's simplest for developers for us to require the same evolution mechanism for new parameters that we require for key rotation (and components): servers will deliver multiple signatures, one with the new parameter, one without. Those would serialized into distinct signature bases, and the signatures associated with those inputs would likewise be distinct. That is:

Signature-Input: oldsig=("identity-digest";sf);keyid="...";type="sri",                 \
                 newsig=("identity-digest";sf);keyid="...";type="sri";unknown="value"

To spell it out, this is exactly how we're asking them to support key rotation, wherein two signature inputs would be delivered, one with the old key, one with the new key.

That seems straightforward to explain, and is my preference at the moment.

@punkeel
Copy link

punkeel commented Jan 7, 2025

Thanks for your answers! This matches my understanding of the RFC 👍

1 above would mean that we generate the following signature base and validate signatures against it:

"identity-digest";sf: sha-256=:...:
"@signature-params": ("identity-digest";sf);keyid="...";tag="sri";unknown="value"

I think the important bit here is to note it's possible to check that signature. The unknown parameter is ignored, which is the same behaviors developers would get by omitting it. I insist on this being possible because it feels, to me, like which would only really be possible if the parameters didn't have an effect on the signature (original post) undersells it.


My first concern here is the lack of negotiation, servers can't easily tell nor measure which components/parameters are supported - and may have to send increasingly larger headers basically forever. Or, alternatively, they will have to pick two: a weaker signature (without the new signature parameters), and the state-of-the-art one.

Feature Browser v1 Browser v2 Browser v3
Basic SRI support
unknown1
unknown2

(This only gets worse with multiple engines supporting various configurations, so I'll assume the simplest scenario - one engine adding features/support over time.)

A Server starts with a simple SRI signature. When Browser v2 is introduced, Server adds support for unknown1 by sending two headers. Even after Browser v2 is rolled out, Server still needs to send the Basic SRI signature.

When Browser v3 is introduced, Server adds support for unknown2 by sending three headers. After Browser v3 is rolled out, Server still needs to send the Basic SRI signature. Should they drop the middle signature, such that up-to-date Browser gets the best signature, and everybody else uses the Basic one?


Second concern, and maybe a worse one: the "upgraded" signature doesn't have to be used? If a browser receives two signatures, a basic one and an "upgraded" one:

Signature-Input: oldsig=("identity-digest";sf);keyid="...";type="sri",                 \
                 newsig=("identity-digest";sf);keyid="...";type="sri";known="value"

User-agents check the first oldsig, see it as valid, and just move on?

With 1, browsers wouldn't ignore parameters they know about - as the "weaker" signature doesn't exist.


Finally, not a concern but relevant here: I'm not sure key rotations are impacted/impactful here. To achieve key rotation, websites need to change their <script integrity="..."> tag to include two keys:

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

This key is then copied into the Accept-Signature: header:

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

Servers can use this request header to tell which signature they want to serve; and so, which key to use.

In this case, servers are free to pick which key to use—rather than always delivering multiple signatures, right? (I suspect this ties back into #23.)

@mikewest
Copy link
Member Author

mikewest commented Jan 7, 2025

"identity-digest";sf: sha-256=:...:
"@signature-params": ("identity-digest";sf);keyid="...";tag="sri";unknown="value"

I think the important bit here is to note it's possible to check that signature. The unknown parameter is ignored, which is the same behaviors developers would get by omitting it. I insist on this being possible because it feels, to me, like which would only really be possible if the parameters didn't have an effect on the signature (original post) undersells it.

We may just be quibbling over the pedantic meaning of "ignore" here. :) The unknown parameter is not "ignored" here insofar as it's included in the signature base and therefore is part of the signed data. It is "ignored" insofar as it doesn't have whatever effect the developer would wish for it to have.

When I wrote "didn't have an effect on the signature", I meant something along the lines of the parameter being important to the algorithm that generated the signature. Imagine a silly variant of Ed25519 that allowed developers to select between distinct signature output lengths (because longer is more secure!): that would be impossible to integrate in a backwards-compatible way, as the parameter would effect the signature in ways beyond its presence in the signature base.

My first concern here is the lack of negotiation, servers can't easily tell nor measure which components/parameters are supported - and may have to send increasingly larger headers basically forever.

Let's take it as given that we're going to add parameters and components. I think we have two ways to approach that invariant:

  1. We can take CSP's route, and allow developers to deliver a single header while ensuring that any new addition maintains backwards compatibility with everything that came before.

  2. We can ignore (and not fail in the presence of) signatures that contain bits we don't understand.

My experience with option 1 is that it leads to more confusion down the road, and complicates the process of evolving the feature. If we can make 2 work, I'd prefer it.

(Note: we do have a third option, which is to say that some addition is large enough that it actually constitutes a new profile: this will likely be the case for algorithm changes, for instance.)

(This only gets worse with multiple engines supporting various configurations, so I'll assume the simplest scenario - one engine adding features/support over time.)

Unfortunately, we're never going to live in a world in which every engine moves in lockstep. So I don't think we can assume away this complexity: we simply must design a system which can deal with divergences in support over time, both across engines and within variants of a given engine. :)

A Server starts with a simple SRI signature. When Browser v2 is introduced, Server adds support for unknown1 by sending two headers. Even after Browser v2 is rolled out, Server still needs to send the Basic SRI signature.

If developers are comfortable with dynamic responses, it seems reasonable to use Accept-Signature to negotiate which signature sets are delivered.

Second concern, and maybe a worse one: the "upgraded" signature doesn't have to be used? If a browser receives two signatures, a basic one and an "upgraded" one:

Signature-Input: oldsig=("identity-digest";sf);keyid="...";type="sri",                 \
                 newsig=("identity-digest";sf);keyid="...";type="sri";known="value"

User-agents check the first oldsig, see it as valid, and just move on?

In the spec, I've modeled things as multiple steps (https://wicg.github.io/signature-based-sri/#overview). Here, we'd validate each of the signatures upon receipt as "server-initiated signature checks". If they're all valid, wonderful! If not, we can abort the request right away. Then, when we have the body, we'll validate Identity-Digest's assertion. Assuming it's good, we'll then perform the client-initiated checks against signatures and content. As long as one valid signature matches the page's requirement, we're fine proceeding.

So, in this example, we'd internally validate the constraints we understand in two sets of inputs, generate two signature bases, validate two signatures, etc. New browsers might indeed understand more pieces of newsig, and might fail where older browsers wouldn't (because some new constraint wasn't met).

Finally, not a concern but relevant here: I'm not sure key rotations are impacted/impactful here. To achieve key rotation, websites need to change their <script integrity="..."> tag to include two keys:

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

This key is then copied into the Accept-Signature: header:

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

Servers can use this request header to tell which signature they want to serve; and so, which key to use.

In this case, servers are free to pick which key to use—rather than always delivering multiple signatures, right? (I suspect this ties back into #23.)

That's correct. I still have primarily an offline-signing model in mind, where servers would bake signatures with two keys into a resource that's then delivered to all requestors. But you're entirely correct that servers interested in dynamic signing would be able to determine which signatures to attack based on the Accept-Signature header.

@punkeel
Copy link

punkeel commented Jan 7, 2025

Thanks again for your detailed response!

We may just be quibbling over the pedantic meaning of "ignore" here. :)

Agreed, my apologies. I'll stop there :)

I still have primarily an offline-signing model in mind, where servers would bake signatures with two keys into a resource that's then delivered to all requestors.

Aye! Just to offer another scenario, the one I had in mind when I wrote my reply: offline-signing with a "smart" server that picks the right signature(s) for a request. I need to weigh the pros/cons.

In the spec, I've modeled things as multiple steps (https://wicg.github.io/signature-based-sri/#overview). Here, we'd validate each of the signatures upon receipt as "server-initiated signature checks". If they're all valid, wonderful! If not, we can abort the request right away. Then, when we have the body, we'll validate Identity-Digest's assertion. Assuming it's good, we'll then perform the client-initiated checks against signatures and content. As long as one valid signature matches the page's requirement, we're fine proceeding.

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

This is super helpful. Oops! for missing the plain-English note…

My experience with option 1 is that it leads to more confusion down the road, and complicates the process of evolving the feature. If we can make 2 work, I'd prefer it.

👍

@mikewest
Copy link
Member Author

mikewest commented Jan 7, 2025

I still have primarily an offline-signing model in mind, where servers would bake signatures with two keys into a resource that's then delivered to all requestors.

Aye! Just to offer another scenario, the one I had in mind when I wrote my reply: offline-signing with a "smart" server that picks the right signature(s) for a request. I need to weigh the pros/cons.

Makes sense. I'd certainly be interested in hearing about deployment considerations you run into as we evaluate whether or not this approach is deployable in practice. Again, @ddworken probably will have deployment opinions from Google's perspective (and, presumably @yoavweiss from Shopify's?) which might be worth discussing together at some point. WebAppSec next week, for instance? :)

My experience with option 1 is that it leads to more confusion down the road, and complicates the process of evolving the feature. If we can make 2 work, I'd prefer it.

👍

I'm going to leave this open for a bit to make sure we're not missing anything, but at the moment I think this is where we'll land.

@ddworken
Copy link
Contributor

ddworken commented Jan 7, 2025

I think I might have a contrasting opinion here. My concern is that with build-time signing, option 2 may be difficult. For example, imagine we were in a scenario where:

  • BrowserFoo supports params one
  • BrowserBar supports params one and two
  • BrowserBaz supports params one and three. And they just shipped support for two in a new version of the browser.

IIUC, with option 2, at build-time we'd need to sign potentially four different signatures, including a new one that is required just for the updated version of BrowserBaz. This isn't impossible, but it does make things more complex and will potentially require rebuilding everything if there are changes in what browsers support what parameters.

So I'm wondering: How big is the downside to option 1? It seems like just including the parameters in the signature base even if the browser doesn't understand them is pretty simple? I understand wanting to avoid the CSP complexities, but my (maybe wrong) assumption is that signature params won't ever get as complicated as CSP.

@yoavweiss
Copy link
Collaborator

(Note: we do have a third option, which is to say that some addition is large enough that it actually constitutes a new profile: this will likely be the case for algorithm changes, for instance.)

How would introducing a new profile work? New headers? Negotiated as part of Accept-Signature? Something else?

In the spec, I've modeled things as multiple steps (https://wicg.github.io/signature-based-sri/#overview). Here, we'd validate each of the signatures upon receipt as "server-initiated signature checks". If they're all valid, wonderful!

So once developers need to add two (or more) signatures for varying parameter support, a supporting browser will have to do twice (or thrice) the work to validate signatures? Couldn't that end up having some performance impact?

IIUC, with option 2, at build-time we'd need to sign potentially four different signatures, including a new one that is required just for the updated version of BrowserBaz. This isn't impossible, but it does make things more complex and will potentially require rebuilding everything if there are changes in what browsers support what parameters.

I share that concern. Option 1 seems significantly simpler from a deployment perspective.

  1. typoing expires would simply break expiration rather than breaking the signature in a way developers would notice

Could we tackle this by other means other than ignoring the signature entirely? E.g. console warnings/devtools issues/reporting?

@mikewest
Copy link
Member Author

mikewest commented Jan 8, 2025

Thanks, @ddworken and @yoavweiss!

To @ddworken:

I think I might have a contrasting opinion here. My concern is that with build-time signing, option 2 may be difficult. For example, imagine we were in a scenario where:

  • BrowserFoo supports params one
  • BrowserBar supports params one and two
  • BrowserBaz supports params one and three. And they just shipped support for two in a new version of the browser.

IIUC, with option 2, at build-time we'd need to sign potentially four different signatures, including a new one that is required just for the updated version of BrowserBaz. This isn't impossible, but it does make things more complex and will potentially require rebuilding everything if there are changes in what browsers support what parameters.

So I'm wondering: How big is the downside to option 1? It seems like just including the parameters in the signature base even if the browser doesn't understand them is pretty simple?

Yup. It's an approach that's clearly technically possible as long as we remain in a world where any parameter we add has no effect on signature base generation or signature validation. I gave a toy example above, but let's flesh it out to discuss the implications. (The examples below are wild inventions, not things I think would be valuable: I'm trying to imagine what parameters folks might need (and, secretly, hope we'll never add any! :) ):

  1. Let's say that cryptographers invent an amazing variant of https://www.ietf.org/archive/id/draft-irtf-cfrg-det-sigs-with-noise-04.html that provided some parameterization to signature generation that was less compatible with existing validators: noise-level=87000 or similar. Using that parameter would effect the signature base in predicable ways, so we could easily include it. It would also effect signature generation, meaning that the signature expected by a system that understood the parameter would be distinct from the signature expected by a system that didn't.

  2. Let's say we invented a variant of created to sign the time at which a resource was served, but still wanted to support offline signing. We could support that by injecting the current time into the signature base. Doing so would provide the desired feature, but also change the set of metadata over which a signature was generated. We'd again be in a situation wherein a client that understood the new parameter would expect one signature, while clients that didn't expected another.

I don't think we could support either of these kinds of parameters in a system that accepted unknown parameters as part of the signature, and it's not clear to me how we'd add them in a backwards compatible way.

I understand wanting to avoid the CSP complexities, but my (maybe wrong) assumption is that signature params won't ever get as complicated as CSP.

You'll be shocked to learn that CSP authors also didn't anticipate the complexity of the backwards compatibility decisions we made. :)


To @yoavweiss:

(Note: we do have a third option, which is to say that some addition is large enough that it actually constitutes a new profile: this will likely be the case for algorithm changes, for instance.)

How would introducing a new profile work? New headers? Negotiated as part of Accept-Signature? Something else?

It's a variant of option 2 that would change the tag, explicitly shifting out of the currently defined processing model, into a new one we define separately. That's how I imagine we'd support algorithms other than ed25519, for instance.

In the spec, I've modeled things as multiple steps (https://wicg.github.io/signature-based-sri/#overview). Here, we'd validate each of the signatures upon receipt as "server-initiated signature checks". If they're all valid, wonderful!

So once developers need to add two (or more) signatures for varying parameter support, a supporting browser will have to do twice (or thrice) the work to validate signatures? Couldn't that end up having some performance impact?

Yup. Happily, Ed25519 is pretty quick in this case, as the signature base we're validating over is quite small. In my (very very) limited testing, the perf bottleneck is the SHA-256 hashing, not the signature validation. We should certainly add some telemetry, though.

IIUC, with option 2, at build-time we'd need to sign potentially four different signatures, including a new one that is required just for the updated version of BrowserBaz. This isn't impossible, but it does make things more complex and will potentially require rebuilding everything if there are changes in what browsers support what parameters.

I share that concern. Option 1 seems significantly simpler from a deployment perspective.

  1. typoing expires would simply break expiration rather than breaking the signature in a way developers would notice

Could we tackle this by other means other than ignoring the signature entirely? E.g. console warnings/devtools issues/reporting?

We should certainly have devtools integration. But Postel was wrong. 🤷 Absent practical implications I'm missing, my preference would be to reject the unknown in a way that doesn't break the known. That general philosophical stance, along with the practical considerations noted above, land me pretty solidly on option 2.

That said, all y'all are actually deploying this in the wild. If you all tell me that I'm wrong, I'll listen. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants