2727 // the wire. Most signers leave it off (the verifier looks the alg
2828 // up by `keyid`); set to true for explicit signalling.
2929 include_alg bool
30+ // scheme is used to reconstruct `@target-uri` and `@scheme` when
31+ // `req.url` is origin-form (e.g. `/foo`, as produced by
32+ // `http.parse_request*`). The default matches the common signing
33+ // scenario (TLS-protected APIs). Ignored when `req.url` already
34+ // carries a scheme.
35+ scheme string = 'https'
3036}
3137
3238// sign_request signs an HTTP request in place by appending the
3945// RFC 9421 §7.2.1 RECOMMENDS the parameter for replay protection.
4046// Pass an explicit `created: 0` only if you know you don't want it.
4147pub fn sign_request (mut req http.Request, key Key, opts SignRequestOptions) ! {
42- c := request_components (req)!
48+ c := request_components (req, opts.scheme )!
4349 mut comps := opts.components.clone ()
4450 if comps.len == 0 {
4551 comps = default_request_components (req)
@@ -69,14 +75,18 @@ pub struct VerifyRequestOptions {
6975pub :
7076 label string
7177 now_unix i64
78+ // scheme — see SignRequestOptions.scheme. Both ends of the
79+ // signature must agree on the scheme used to reconstruct the
80+ // target URI, otherwise the signature bases differ.
81+ scheme string = 'https'
7282}
7383
7484// verify_request verifies a labelled signature on an HTTP request. If
7585// `opts.label` is empty and exactly one signature is present, that
7686// one is checked. If `opts.now_unix > 0`, the `expires` parameter is
7787// also enforced.
7888pub fn verify_request (req http.Request, key Key, opts VerifyRequestOptions) ! {
79- c := request_components (req)!
89+ c := request_components (req, opts.scheme )!
8090 sig_input := merged_dict_field (req.header, 'Signature-Input' ) or {
8191 return MalformedMessage{
8292 reason: 'request has no Signature-Input header'
@@ -180,14 +190,13 @@ fn merged_dict_field(h http.Header, name string) ?string {
180190}
181191
182192// request_components extracts the derived-component values from an
183- // http.Request. The url field is parsed once; if it is not a valid
184- // URL we surface a typed error rather than silently dropping
185- // components that depend on it (@scheme, @authority, @path, @query).
186- fn request_components (req http.Request) ! Components {
187- mut c := Components{
188- method: req.method.str ()
189- target_uri: req.url
190- }
193+ // http.Request. `default_scheme` is used when `req.url` is in
194+ // origin-form (e.g. `/foo?bar=1`, as produced by `http.parse_request*`
195+ // for inbound HTTP/1.1 messages); in that case `@target-uri` is
196+ // reconstructed as `<scheme>://<authority><url>` per RFC 9110 §7.1.
197+ // If `req.url` is not a valid URL we surface a typed error rather
198+ // than silently dropping components that depend on it.
199+ fn request_components (req http.Request, default_scheme string ) ! Components {
191200 parsed := urllib.parse (req.url) or {
192201 return MalformedMessage{
193202 reason: 'request url "${req.url} " is not a valid URL: ${err.msg()} '
@@ -198,11 +207,21 @@ fn request_components(req http.Request) !Components {
198207 } else {
199208 req.host
200209 }
210+ scheme := if parsed.scheme != '' { parsed.scheme } else { default_scheme }
211+ mut c := Components{
212+ method: req.method.str ()
213+ }
214+ is_origin_form := req.url.starts_with ('/' )
215+ c.target_uri = if is_origin_form && authority != '' {
216+ '${scheme} ://${authority}${req.url} '
217+ } else {
218+ req.url
219+ }
201220 if authority != '' {
202221 c.authority = authority
203222 }
204- if parsed. scheme != '' {
205- c.scheme = parsed. scheme
223+ if scheme != '' {
224+ c.scheme = scheme
206225 }
207226 if parsed.path != '' {
208227 c.path = parsed.path
0 commit comments