Change:
8.4. Encapsulated requests can be copied and replayed by the oblivious <del> proxy resource.  The design of oblivious HTTP does not assume that the oblivious proxy resource will not replay requests.  In addition, if a client sends an encapsulated request in TLS early data (see Section 8 of TLS and RFC8470), a network-based adversary might be able to cause the request to be replayed.  In both cases, the effect of a replay attack and the mitigations that might be employed are similar to TLS early data. </del> <ins> proxy resource.  The threat model for oblivious HTTP allows the possibility that an oblivious proxy resource might replay requests. Furthermore, if a client sends an encapsulated request in TLS early data (see Section 8 of TLS and RFC8470), a network-based adversary might be able to cause the request to be replayed.  In both cases, the effect of a replay attack and the mitigations that might be employed are similar to TLS early data. It is the responsibility of the application that uses oblivious HTTP to either reject replayed requests or to ensure that replayed requests have no adverse affects on their operation.  This section describes some approaches that are universally applicable and suggestions for more targeted techniques. </ins> A client or oblivious proxy resource MUST NOT automatically attempt to retry a failed request unless it receives a positive signal

Discussion:
This recommends the use of in requests as discussed on and . Most of the heavy lifting is outsourced to , so I won't merge this until we have some greater certainty about the destiny of that draft.
This seems like a separate issue, but: it seems really quite problematic if two requests had the same enc value but different values for t, as the client is then either reusing its key share or something else is going on. What my suggestion meant to say is that, if you get a value of t outside of the window, reject it or ask the client to retry with a new value of t. And use the new enc value to enforce your replay check for that given value t. That is, logically: Given (enc, t) and enc has not been seen before in t, accept. Given (enc, t) and enc has been seen in t, reject as replay. Given (enc, t') and t' is not the current time window, reject and ask them to retry
Yeah, that's right. I was interpreting your request as saying that you might need to do something with duplicate enc values even when you forgot the original.
In and , we're talking about adding a timestamp to requests. The problem with that is that - unlike TLS - we don't have any way to correct for gross clock errors. TLS manages this easily by leveraging a previous session to make that correction, reducing any relative clock error to the amount of drift over the week that a ticket can be used for early data. This design has nothing like that it can use. To correct for clock errors, we should think about whether the server should be able to signal that the request was rejected as a result of the time being too far out of synchronization.
The problem with allowing a server to correct your time is that you might be tricked into setting a time that is useful for tracking you.

To that end, if we build a mechanism we need either to recommend that clients : seek an independent and trustworthy source of time rather than use any time that the server has provided; that is, the signal is an error code, without a direct means of correcting client time only use any server-provided time correction for an immediate retry; clients cannot remember that time for any requests that they wish to remain unlinked to the original; that is, it might be OK for a retry to be linked to the first attempt, but clients should not allow server-provided information to affect multiple requests
The current design doesn't add any data to requests, so servers are only able to provide anti-replay based on what they see: the encapsulation and the content of the HTTP request. One common factor in anti-replay designs is time. It is easier to apply anti-replay protections for timestamped requests as any state can be bounded in time: requests are rejected as being a replay if they fall outside a small time window. Should we include a timestamp in requests? Or, should we merely insist on the Date header field being present? It is somewhat for Date to be present on requests, but it is permitted. The other thing to consider is clock drift. TLS uses an anti-replay design that uses relative times, which means that only relative clock drift over a short time needs to be accounted for. Here, we have no prior synchronization, so we are forced to use absolute times. That likely means a greater allowance for clock drift. It also likely means that we need to include a new signal to clients for when their time is so badly off that their requests are being rejected. Experience shows that client clocks are terrifically bad, but given prompting they might be able to adjust to match the server clock. We need to be very careful to ensure that this sort of signal is properly authenticated, so that if clients do retry requests, they aren't subject to attacks that might cause them to be complicit in generating a replay attack. We might also consider a nonce or other anti-replay measures, but as clients are required to perform a new encapsulation for every request, the value can be used. (This issue is split from .)
The most obvious mechanism here for protecting against anti-replay is a timestamp in combination with a per-entry nonce. There are a few things to consider when deciding on how to encode the time.

The first question is whether to encrypt the timestamp or not. An encrypted timestamp would result in servers spending the cycles necessary to decrypt a request before they can be sure if it is a replay. Each server will maintain nonces over a certain time window and discard requests that are fall outside this span (either earlier or later). An encrypted timestamp means that a server can still use the nonce to determine if a request is a replay before decrypting. However, requests from outside the time window will need to be decrypted to determine that they should be discarded. This is more work. However, we should assume that - under normal operation - replays will be rare. Clock skew is the obvious cause for a request falling outside of the time window that a server is tracking. However, that only really points to a need for having a wider time window or feedback that allows client time to be corrected. It's only the DoS cost that might play into a choice to avoid the extra computation cost. Validation of an HPKE-encrypted message is only possible after performing lots of work. For an attacker looking to exhaust server computation resources, it is possible to just send a server complete garbage. The server only learns that the message was garbage after performing an expensive key exchange and decryption. A timestamp on the outside of the message doesn't save this, as this can be set to whatever value best suits the attacker. That all leads me to suggest that including the timestamp under encryption is probably the best choice. Also, if we want to provide an authenticated signal for clock skew errors, it will be easier to have the timestamp under protection. Then, when the timestamp falls outside of the server time window, the server can generate an authenticated response. Using for the nonce part gives us a very high entropy value with very low collision probability. That allows us to use a timestamp that is quite coarse. The only reason we would want to increase clock resolution is so that servers can manage narrow anti-replay windows. Because we are operating in near-real time, anti-replay windows need to be wide enough to allow for clock skew and not a lot else. How much tolerance for clock skew we need will depend on whether we have good correction mechanisms in place.

A coarser clock forces a server to maintain state for a slightly wider window, to allow for the requests within their target window to be adjusted by the coarseness of the clock. For instance, if the desired window is 10 minutes and the clock resolution is 1 day, then the window is expanded to 2 days. Thus, we should not reduce resolution too much. Provided that it is not too coarse, a coarse value reveals less about the clock of the client. Though we cannot stop some information about the client clock from leaking, less information is better. Note that we don't have to worry so much about values crossing thresholds (e.g., from one day to the next if the resolution is 1 day), as the server cannot link requests from the same client to use information about when it crosses that threshold. This isn't that much value as clients are able to randomize their clock to obscure the true value. This is likely to be very effective as a privacy protection as each request is handled in isolation. It must be possible to unambiguously represent any time in the period over which a server public key is valid. If the range of possible timestamp values is shorter than the lifetime of a key, the same timestamp value might identify multiple times in the lifetime of that key. A 32-bit value in seconds covers 136 years. If we take the low 32-bits of the current time in seconds, then a timestamp will be valid for multiple times 136 years apart. That's probably long enough that a server should have replaced its public key. Given what we know about public key usage in other contexts, it's probably wise to cover a period of multiple years. A 32-bit value in seconds is clearly good. Clock skew in the order of a second is extremely common, so this doesn't significantly increase the state a server needs to maintain. If we were to reduce the size to the next threshold and choose 24 bits, seconds doesn't work as that gives a range of only half a year. We can expand that 32 times (i.e., 17 years) by shifting by 5 bits and having a resolution of 32 seconds. That seems like a poor trade.

That's enough for anti-replay (17 years is far too long to hold onto the same key), but the shift by 5 is annoying to implement, and the space savings are hardly significant. It also forces servers to keep state for a minimum period of 64 seconds, which is at the point that it could be longer than some servers would otherwise need to hold anti-replay state. The only benefit is the reduction in timer resolution from clients. The 32-bit value seems like the best answer. It's simple and effective. We can pack that in ahead of the request message and encrypt it.
I agree with the conclusion to encrypt the timestamp (proxies don't really need it and it's very little extra work) and on the resolution (clients that want to hide information can mask out the lower bits), but not quite on the range. In particular: given a validity window of length broken up into two windows represented by the same timestamp range, we don't want a replay from the first window to be valid in the second window. This is where I think we diverge. This seems to encourage poor key management practices. I'd prefer the spec take a stricter stance on this, and 24 bits seems like it would be quite reasonable in this regard. Servers can make sensible choices about what state their willing to store based on anticipated clock skew in honest clients, but the spec ultimately needs to be robust against malicious clients replaying messages at arbitrary points in the future within the validity window of a public key. At the end of the day, this seems to boil down to how long we want or expect keys to be valid, and I'm just not comfortable with allowances for extremely long windows.
Before designing a system, I think we should start by asking the threshold question of whether we need anti-replay at all. First, there are a number of systems which simply don't need it: anything that's idempotent on the server doesn't need replay detection. Even systems which are not idempotent quite likely have their own anti-replay mechanisms in order to deal with application-level replay conditions. For instance suppose that you have a telemetry or crash reporting system: you want it to be robust to transport-level failures which means that you keep retrying in the face of those failures. But the HTTP anti-replay mechanisms are at the TLS layer and so you also need application layer anti-replay.

Finally, even if we do have an anti-replay mechanism at this layer, it's not going to be effective unless we provide strict application guidance. For instance, suppose that the proxy collects the client's message and then generates a transport error to force a replay at the client. If the client generates a new transaction (not unreasonable) then this won't trigger the anti-replay defenses. And of course if the client just re-sends the same message then the proxy learns information. For these reasons, I think we should consider punting this entirely.
I think it's true that we can't do much about replays of the same application data, since a server can't distinguish between the scenario you describe (proxy forces client to replay data with a new transaction) and a dishonest client that's simply replaying the same application data. But I think we can do something here about replays of the encapsulated requests. Whether or not we do seems to boil down to the threat model and the relative cost of attack. How hard is it for an attacker to trigger these client-side replays? Trivial for the proxy, as noted, and certainly easier than transparently storing and replaying client requests in the future. However, the client does have to actively generate the new transaction, so it has the opportunity to try and detect this sort of misbehavior. (Whether or not it could distinguish between a maliciously truncated transport connection by an on-path attacker, a maliciously truncated connection by the proxy, or a legitimate error seems a separable question.) On balance, I think the added complexity here is palatable for what can be a pretty trivial attack to be carried out by malicious proxies without the client seeing anything.
I certainly agree that the attack is trivial. My point is just that the application layer protocol that cares about replay will need A-R defenses anyway and so won't this be redundant?
Possibly, yeah. We should work through some use cases to see if it's truly needed at this layer.
There's an alternative: we suggest/recommend that clients populate the header field in requests and allow/encourage the anti-replay to occur at the next layer up. Parsing is annoying and error prone, but it would mean that the design is kept simple at this layer.
NAME that seems like a reasonable alternative!
LGTM after some reformatting. Thanks for the updates, NAME