diff --git a/.gitignore b/.gitignore index fc34f12..59cce46 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /node_modules/ /versioned/ Gemfile.lock +__pycache__/ archive.json draft-richer-oauth-httpsig.xml package-lock.json diff --git a/draft-richer-oauth-httpsig.md b/draft-richer-oauth-httpsig.md index 7895a27..add9819 100644 --- a/draft-richer-oauth-httpsig.md +++ b/draft-richer-oauth-httpsig.md @@ -37,20 +37,35 @@ author: normative: BCP195: DIGEST: RFC9530 - HTTP: RFC9111 - JWK: RFC7517 OAUTH: RFC6749 - OAUTH-BEARER: RFC6750 STRUCTURED: RFC9651 MTLS: RFC8705 HTTPSIG: RFC9421 DPOP: RFC9449 - RAR: RFC9636 PAR: RFC9126 DYNREG: RFC7591 - JSON: RFC8259 HTTPAUTH: RFC7235 + JWK: RFC7517 + JWA: RFC7518 + POPKEY: RFC7800 + RFC9864: RFC8032: + FIPS204: + title: "Module-Lattice-Based Digital Signature Standard" + author: + org: "National Institute of Standards and Technology (NIST)" + date: 2024-08 + seriesinfo: + "NIST": "FIPS 204" + "DOI": "10.6028/NIST.FIPS.204" + target: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf + SEC1: + target: https://www.secg.org/sec1-v2.pdf + title: "SEC 1: Elliptic Curve Cryptography" + author: + org: Certicom Research + date: 2009-05 + refcontent: "Standards for Efficient Cryptography, Version 2.0" informative: I-D.ietf-oauth-signed-http-request: @@ -98,7 +113,7 @@ To bind an access token to a key, the AS needs to know which key to bind to whic - A static method that depends on key material available as part of the client registration - A runtime method that allows a client to introduce key material during the token request phase of {{OAUTH}} -As part of its registration, a client MUST indicate which method it will use, using either the `httpsig_key_binding_method` client registration metadata parameter defined (TBD) in {{IANA}} when using Dynamic Client Registration ({{DYNREG}}) or Client ID Metadata Document ({{I-D.ietf-oauth-client-id-metadata-document}}), or via an out of band method. +As part of its registration, a client MUST indicate which method it will use, using either the `httpsig_key_binding_method` client registration metadata parameter defined in {{IANA}} when using Dynamic Client Registration ({{DYNREG}}) or Client ID Metadata Document ({{I-D.ietf-oauth-client-id-metadata-document}}), or via an out of band method. \[\[ Editor's note: do we want to add an AS/RS metadata parameter to signal support for each type? \]\] @@ -106,19 +121,17 @@ As part of its registration, a client MUST indicate which method it will use, us ## Pre-Registration of Keys {#preregister} -A client pre-registering its keys for {{HTTPSIG}} binding MUST include the key in its registered `jwks` value or make it available from its `jwks_uri` endpoint. The JWK MUST have a `kid` field and MUST indicate a signing algorithm in its `alg` field. The key ID for the public key used for HTTP Message Signature bound access tokens MUST be identified using the `httpsig_bound_access_token_kid` field in the client's metadata. +A client pre-registering its key for {{HTTPSIG}} binding MUST include the key in its registered `jwks` value or make it available from its `jwks_uri` endpoint. The JWK MUST have a `kid` member and MUST indicate a signing algorithm in its `alg` member. -\[\[ Editor's note: do we want to have a client field for the signing alg or just leave that to the key all the time? I prefer to keep it in the key. \]\] +The key MUST be an asymmetric key, and the registered JWK MUST be its public key. A shared secret MUST NOT be used to bind an access token, as described in {{Security}}. -A pre-registered key MAY be a shared secret (such as for use in an HMAC signature), but public key cryptography is RECOMMENDED. - -If the key is pre-registered, the signature algorithm MUST be derived from the indicated key and the `alg` signature parameter MUST NOT be used. +The client selects which of its registered keys the token is bound to when it makes the token request, as described in {{request}}. The client's metadata names the key set rather than a key within it, allowing a client to add and retire keys without any change to its registration. Note that pre-registration can occur statically or dynamically (such as by using {{DYNREG}}), as long as the key is associated with the client's `client_id` before the token request is made. ### Example Client Registration -A client can publish the key binding parameters as part of a {{I-D.ietf-oauth-client-id-metadata-document}} alongside its `jwks` or `jwks_uri` values. For example, a client with the `client_id` value `https://client.example.com/client-metadata.json` would publish the following document at that URL, indicating that it uses a pre-registered key: +A client can publish its key binding metadata as part of a {{I-D.ietf-oauth-client-id-metadata-document}} alongside its `jwks` or `jwks_uri` values. For example, a client with the `client_id` value `https://client.example.com/client-metadata.json` would publish the following document at that URL, indicating that it uses a pre-registered key: ~~~ json { @@ -132,11 +145,10 @@ A client can publish the key binding parameters as part of a {{I-D.ietf-oauth-cl "crv": "Ed25519", "kid": "j-0Ny45NWmqGq6GQ", "x": "iuemcj_GhRHmY_yCsMlDNp3BQgPZDdG00VRsg_BgU3s", - "alg": "EdDSA" + "alg": "Ed25519" } ] }, - "httpsig_bound_access_token_kid": "j-0Ny45NWmqGq6GQ", "httpsig_key_binding_method": "preregistered" } ~~~ @@ -145,13 +157,7 @@ A client can publish the key binding parameters as part of a {{I-D.ietf-oauth-cl Instead of pre-registering a key, a client can introduce its key during the token request in a similar fashion as {{DPOP}}. -To use this mode, the client MUST: - -* Include the `alg` signature parameter with a valid value from the HTTP Message Signatures Algorithms Registry indicating an asymmetric signature algorithm. -* Include its public key in the key type specific parameters as described in {{embed-keys}}. -* Include the `keyid` signature parameter uniquely identifying the key. - -The included key MUST be appropriate for the indicated algorithm. +A client using this method presents its public key in the token request itself, as described in {{request}}. The key is not registered with the AS beforehand, and the AS binds a token to the presented key subject to its own policy. ## Token Request {#request} @@ -179,48 +185,99 @@ A client using this method MUST sign the token endpoint request using {{HTTPSIG} The covered components MUST include the client's authentication, if available. If using HTTP Basic, this means including the `authorization` field. -The signature MUST include the following parameters: +The signature MUST include the following signature parameters: - `created` a timestamp for signature creation; this MUST be within a small number of seconds of issuance (e.g. 30 seconds to account for clock skew) - `nonce` a random unique value that the AS can use to prevent signature replay within the small validity time window - `tag` a string indicating that this is being used for requesting a bound token, MUST be the value "httpsig-oauth-token-request" -- `keyid` the identifier for the key to be used for binding the token (matching `kid` in the token); if client uses pre-registered keys as in {{preregister}}, the value MUST match the `httpsig_bound_access_token_kid` value -Additionally, if the key is presented at runtime, the parameters and public key MUST be included as signature parameters as defined in {{runtime}}. +The remaining signature parameters identify the key, and depend on the key binding method. + +With a pre-registered key as in {{preregister}}: + +- `keyid` MUST be included. Its value MUST match the `kid` member of a key in the client's registered `jwks` value or at its `jwks_uri` endpoint. +- `alg` and `pub` MUST NOT be included. The algorithm comes from the identified key, as described in {{alg-preregistered}}. + +With a runtime-introduced key as in {{runtime}}: -An example request to the token endpoint (using a runtime-provided key here) can look like the following: +- `alg` MUST be included, with a value from the "HTTP Signature Algorithms" registry as described in {{alg-runtime}}. +- `pub` MUST be included, carrying the public key as described in {{embed-keys}}. +- `keyid` MUST NOT be included, as the key is presented in full. + +An example token request using a pre-registered key can look like the following: + +~~~ http-message +{::include tools/examples/token-request-prereg-signed.http} +~~~ + +An example token request introducing the same key at runtime can look like the following: ~~~ http-message {::include tools/examples/token-request-signed.http} ~~~ +# Signature Algorithms {#algorithms} + +The two key binding methods name signature algorithms in different registries. A key bound by one method is never used with the other, and this document neither defines nor relies on any correspondence between the two registries. + +## Pre-Registered Keys {#alg-preregistered} + +A pre-registered key is a JSON Web Key {{JWK}} whose `alg` value names an algorithm from the "JSON Web Signature and Encryption Algorithms" registry established by {{JWA}}. Signatures made with such a key use the JOSE signing algorithms of {{Section 3.3.7 of HTTPSIG}}, which take the algorithm from the key material. The `alg` signature parameter is not used with those algorithms, and the client MUST NOT include it. + +The JWK's `alg` value MUST be fully specified. A polymorphic identifier such as `EdDSA`, which names a different signature algorithm depending on the key it is used with, MUST NOT be used. The fully specified equivalent defined in {{RFC9864}} is used in its place. + +## Runtime-Introduced Keys {#alg-runtime} + +A runtime-introduced key names its algorithm in the `alg` signature parameter, using a value from the "HTTP Signature Algorithms" registry established by {{Section 6.2 of HTTPSIG}}, as required by {{Section 3.3 of HTTPSIG}}. The algorithm MUST be an asymmetric signature algorithm for which this document defines a public key encoding in {{embed-keys}}. + +\[\[ Editor's note: we keep the two paths apart on purpose. Bridging them would mean asserting that some JWS algorithm and some "HTTP Signature Algorithms" entry are the same algorithm, and the two registries are maintained independently with nothing requiring either to track the other. Are we happy to pay a second code path at the RS to avoid that? \]\] + # Embedding a Public Key Value {#embed-keys} -When encoding a public key value in a runtime request as in {{runtime}}, the client includes the public key material appropriate to the signature algorithm being used. +When encoding a public key value in a runtime request as in {{runtime}}, the client includes the public key material in the `pub` signature parameter attached to the signature input, encoded as a Byte Sequence as defined in {{STRUCTURED}}. + +The contents of the `pub` signature parameter are the public key material appropriate to the signature algorithm indicated by the `alg` signature parameter, as defined in the following sections. + +## ECDSA + +If the `alg` value is `ecdsa-p256-sha256` or `ecdsa-p384-sha384`, the `pub` signature parameter contains the uncompressed point representation of the public key, which is the single octet `0x04` followed by the big-endian, zero-padded, fixed-length encodings of the key's X and Y coordinates. This is the output of the Elliptic-Curve-Point-to-Octet-String Conversion in Section 2.3.3 of {{SEC1}} with point compression off. -## Elliptic Curve +For `ecdsa-p256-sha256`, the X and Y values are exactly 32 octets each and the `pub` value is exactly 65 octets. For `ecdsa-p384-sha384`, the X and Y values are exactly 48 octets each and the `pub` value is exactly 97 octets. -If the `alg` value is `ecdsa-p256-sha256` or `ecdsa-p384-sha384`, the public key is encoded in two additional signature parameters and values attached to the signature input: +## Ed25519 -- `pub_key_x`: the big-endian encoded, zero-padded bytes of the key's X value, encoded as a Byte Sequence -- `pub_key_y`: the big-endian encoded, zero-padded bytes of the key's Y value, encoded as a Byte Sequence +If the `alg` value is `ed25519`, the `pub` signature parameter contains the Ed25519 public key `A`, which is the encoding of the point `[s]B` as specified in {{Section 5.1.5 of RFC8032}}. -For `ecdsa-p256-sha256`, the key values are padded to exactly 32 bytes each. For `ecdsa-p384-sha384`, the key values are padded to exactly 48 bytes each. +The key value is exactly 32 octets in length. -## Edwards Elliptic Curve +## ML-DSA -If the `alg` value is `ed25519`, the public key is encoded in one additional signature parameter and value attached to the signature input: +If the `alg` value indicates an ML-DSA algorithm, the `pub` signature parameter contains the ML-DSA public key encoded as described in Section 7.2 of {{FIPS204}}, which is the output of the `pkEncode` routine, Algorithm 22 of {{FIPS204}}. -- `pub_key_a`: the little-endian encoded compressed Edwards point defined in {{RFC8032}}, encoded as a Byte Sequence +The value is exactly 1312 octets for the ML-DSA-44 parameter set, 1952 octets for ML-DSA-65, and 2592 octets for ML-DSA-87. -The key value is exactly 32 bytes in length. +\[\[ Editor's note: ML-DSA algorithm identifiers for use with {{HTTPSIG}} are being defined in separate work. We should add a reference here once that document is available. \]\] -## RSA -If the `alg` value is `rsa-pss-sha512` or `rsa-v1_5-sha256`, the public key is encoded in two additional signature parameters and values attached to the signature input: +# HTTP Signature Key Confirmation Method {#htsk} + +A key introduced at runtime as in {{runtime}} is not a JSON Web Key and has no JWK representation, so the confirmation methods of {{POPKEY}} cannot carry it. This document defines the HTTP Signature Key confirmation method for that purpose, using the member name `htsk`. + +The value of the `htsk` member is a JSON object with two members: + +- `alg`: the algorithm identifier from the "HTTP Signature Algorithms" registry, as a string +- `pub`: the base64url encoding, without padding, of the `pub` octets defined in {{embed-keys}} for that algorithm + +Both members MUST be present. A recipient MUST reject the confirmation if it does not recognize the `alg` value or if the `pub` value is not a well-formed public key for that algorithm. + +~~~ json +{ + "alg": "ed25519", + "pub": "iuemcj_GhRHmY_yCsMlDNp3BQgPZDdG00VRsg_BgU3s" +} +~~~ -- `pub_key_n`: the big-endian encoded unsigned modulus of the key (no sign byte and no leading 0x00 octet), encoded as a Byte Sequence -- `pub_key_e`: the big-endian encoded exponent of the key, encoded as a Byte Sequence +The `htsk` member is used in the `cnf` claim of a JWT and in the `cnf` member of a token introspection response, as described in {{issuing}}. # Issuing an HTTP Message Signature Bound Access Token {#issuing} @@ -228,7 +285,10 @@ The AS MUST validate the signature of the token request sent in {{request}} agai The request MUST fail with an error if any of the following occur: -- The key named in `kid` cannot be found or is not associated with the requesting client +- With a pre-registered key as in {{preregister}}, the key named in `keyid` cannot be found or is not associated with the requesting client +- With a pre-registered key as in {{preregister}}, the `alg` or `pub` signature parameter is present, or the JWK's `alg` value is not a fully specified asymmetric algorithm as required by {{alg-preregistered}} +- With a runtime-introduced key as in {{runtime}}, the `keyid` signature parameter is present, or the `alg` or `pub` signature parameter is absent +- With a runtime-introduced key as in {{runtime}}, the `alg` value is not an asymmetric algorithm for which this document defines a public key encoding in {{embed-keys}}, or the `pub` value is not a well-formed public key for it - There is more than one signature with the tag "httpsig-oauth-token-request" - The `created` value of the signature is too far in the past - The `nonce` value is used more than once within the validity window of the signature @@ -249,12 +309,53 @@ Content-Type: application/json The client MUST associate this returned access token with the key used to make the requst. -\[\[ Editor's note: we should define confirmation methods for access tokens here, including JWT values and introspection response values to allow the RS to verify the signature w/o the client's registration information. Leaving the following sections as placeholders. \]\] +The confirmation carries the key the token is bound to, allowing an RS to validate a presented signature without reference to the client's registration. The member carrying the key depends on the key binding method and indicates which algorithm registry applies: + +- A pre-registered key is carried in the `jwk` member of the `cnf` claim, as defined in {{Section 3.2 of POPKEY}}, and names its algorithm in the JWK's `alg` value as in {{alg-preregistered}} +- A runtime-introduced key is carried in the HTTP Signature Key (`htsk`) member defined in {{htsk}}, and names its algorithm in that object's `alg` member as in {{alg-runtime}} ## Encoding Confirmation in a JWT +A token bound to a pre-registered key can look like the following: + +~~~ json +{ + "iss": "https://server.example.com", + "aud": "https://resource.example.com", + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "alg": "Ed25519", + "x": "iuemcj_GhRHmY_yCsMlDNp3BQgPZDdG00VRsg_BgU3s" + } + } +} +~~~ + +A token bound to a runtime-introduced key can look like the following: + +~~~ json +{ + "iss": "https://server.example.com", + "aud": "https://resource.example.com", + "cnf": { + "htsk": { + "alg": "ed25519", + "pub": "iuemcj_GhRHmY_yCsMlDNp3BQgPZDdG00VRsg_BgU3s" + } + } +} +~~~ + +A confirmation MUST carry exactly one of these members. + ## Returning Confirmation in Token Introspection +The same `cnf` member is returned in a token introspection response. + +\[\[ Editor's note: we need to register `htsk` in the "JWT Confirmation Methods" registry. Nothing further is needed for introspection, since `cnf` is already a registered token introspection response parameter and carries the same structure. \]\] + # Presenting an HTTP Message Signature Bound Access Token {#presenting} HTTP Message Signature bound access token MUST be presented in an HTTP Authorization field using the `HTTPSig` authorization scheme. @@ -285,16 +386,15 @@ The request MAY include multiple signatures to serve different needs. If the request includes an entity body (such as a POST, PUT, or QUERY), the client SHOULD calculate the digest as per {{DIGEST}} and also sign the digest header (such as Content-Digest). -The signature MUST include the following parameters: +The signature MUST include the following signature parameters: - `created` a timestamp for signature creation; this MUST be within a small number of seconds of issuance (e.g. 30 seconds to account for clock skew) - `nonce` a random unique value that the AS can use to prevent signature replay within the small validity time window - `tag` a string indicating that this is being used for requesting a bound token, MUST be the value "httpsig-oauth" -- `keyid` the identifier for the key used to sign the request -The client MUST NOT include an `alg` signature parameter. +The client MUST NOT include the `alg`, `keyid`, or `pub` signature parameters, as the RS determines the key from the binding of the presented access token. -For example, the following signed request includes a signature with the needed parameters: +For example, the following signed request includes a signature with the needed signature parameters: ~~~ http-message {::include tools/examples/present-request-signed.http} @@ -304,14 +404,16 @@ For example, the following signed request includes a signature with the needed p In order for a request protected by an HTTP Message Signature bound access token to be considered valid, the RS MUST perform the following checks: -- The presented signature validates using the key bound to the token -- The signature validates using the HTTP_VERIFY algorithm associated with the key -- The `keyid` value matches the identifier for the key bound to the token +- The presented signature validates using the key the token is bound to +- For a `jwk` confirmation, the signature validates under the HTTP_VERIFY application of the JWS algorithm named by the JWK's `alg` member, applied as described in {{Section 3.3.7 of HTTPSIG}} +- For an `htsk` confirmation, the signature validates under the HTTP_VERIFY primitive of the named "HTTP Signature Algorithms" entry - The `created` value is not too far in the past (e.g. 30 seconds to account for clock skew and network delays) - The `nonce` value has not been previously used within the time validity window of this request - The `tag` value is "httpsig-oauth" -- The covered components and parameters include all items enumerated in {{presenting}}, including the Authorization header field -- The `alg` parameter is not present +- The covered components and signature parameters include all items enumerated in {{presenting}}, including the Authorization header field +- The `alg`, `keyid`, and `pub` signature parameters are not present + +The check on the `alg`, `keyid`, and `pub` signature parameters, and the corresponding checks in {{issuing}}, are required rather than optional. {{Section 3.2.1 of HTTPSIG}} allows an application to impose requirements beyond those of {{HTTPSIG}}, but requires that it enforce them during verification and fail any signature that does not conform. If the request includes an entity body (such as a POST, PUT, or QUERY) and a digest as per {{DIGEST}}, the RS MUST validate the digest. @@ -323,9 +425,39 @@ For example, to validate the request: {::include tools/examples/rs-request-signed.http} ~~~ -The RS determines the key bound to the token (in this example, assume the RS introspects the token to get the key material) and validates the `kid` value against that the `keyid` in the signature input. The RS determines the algorithm from the key material. +Nothing in the request identifies the key, so the RS determines the key and the algorithm from the token's confirmation method (in this example, assume the RS introspects the token to get it). The same request validates under either binding method, and only the confirmation differs. + +If the token was issued for a pre-registered key, the confirmation carries the JWK and the algorithm is the JWS algorithm named by its `alg` member: + +~~~ json +{ + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "x": "R4YT13Ra4frLCH75BdM53OEHYi_YY2eQgR43BWWQpVw", + "y": "UFVFPBGODokYW06OXCqDskhZSU3RpUI6fR8_OCz2doo" + } + } +} +~~~ + +If the token was issued for a key introduced at runtime, the confirmation carries the same key as an `htsk` member, and the algorithm is the "HTTP Signature Algorithms" value named by its `alg` member: + +~~~ +{ + "cnf": { + "htsk": { + "alg": "ecdsa-p256-sha256", + "pub": "BEeGE9d0WuH6ywh--QXTOdzhB2Iv2GNnkIEeNwVlkKVcUF\ + VFPBGODokYW06OXCqDskhZSU3RpUI6fR8_OCz2doo" + } + } +} +~~~ -In this example, the client has a key with the `kid` value of `test-key-ecdsa-p256`. The signature input string is: +The signature base is the same in either case: ~~~ {::include tools/examples/rs-sig-base.sigbase} @@ -337,7 +469,7 @@ The RS then calculates the signature validation against the signature base using # IANA Considerations {#IANA} -\[\[ TBD: register the token type and new parameters into their appropriate registries, as well as the JWT and introspection parameters needed for confirmation methods. \]\] +\[\[ TBD: register the `httpsig` token type, the `httpsig_key_binding_method` client registration metadata parameter, the `pub` signature parameter defined in {{embed-keys}} in the "HTTP Signature Metadata Parameters" registry established by {{HTTPSIG}}, and the HTTP Signature Key (`htsk`) confirmation method in the "JWT Confirmation Methods" registry. The `jwk` confirmation method is already registered by {{POPKEY}}, and `cnf` is already a registered token introspection response parameter. \]\] # Security Considerations {#Security} @@ -348,6 +480,7 @@ The RS then calculates the signature validation against the signature base using - Insufficient coverage of a message allows a signature to be attached to a different message. - Failure to check derived attributes allows a signature to be replayed. - Signatures could be replayed outside of their vailidty window if not checked. +- An access token cannot be bound to a shared secret. Every party that validates a presented signature needs the key that produced it, and a verifier holding symmetric key material can produce a valid signature of its own as per {{Section 7.3.3 of HTTPSIG}}. Binding a token to a shared secret would allow every RS that accepts it to produce requests indistinguishable from the client's. The client's registered `jwks` and `jwks_uri` values carry public keys only ({{DYNREG}}). # Privacy Considerations {#Privacy} @@ -362,7 +495,15 @@ The RS then calculates the signature validation against the signature base using - -03 - Added co-authors - - Changed inline key presentation from header to signature parameter + - Changed inline key presentation from a header field carrying a JWK to a single `pub` signature parameter carrying the raw public key + - Limited the inline key representation to ECDSA, Ed25519, and ML-DSA + - Required the `alg` signature parameter for runtime key introduction + - Removed `keyid` from runtime key introduction and from token presentation + - Required the bound key to be asymmetric, disallowing shared secrets + - Kept each binding method within a single algorithm registry, with no correspondence defined between the two + - Required pre-registered JWK algorithm identifiers to be fully specified + - Defined the `jwk` confirmation method for pre-registered keys and a new HTTP Signature Key (`htsk`) confirmation method for runtime-introduced keys + - Removed the `httpsig_bound_access_token_kid` client metadata field in favor of selecting a pre-registered key with the `keyid` signature parameter - -02 - Editorial fixes @@ -384,7 +525,7 @@ The RS then calculates the signature validation against the signature base using ## Client Authentication -Similarly to {{MTLS}}, {{HTTPSIG}} could be used as a generic client authentication mechanism for the client calling the AS for any authenticated call, including token PAR, the token endpoint. Since {{HTTPSIG}} allows for multiple signatures with different usage parameters (including `tag`), this could be layered on top of even the runtime token request key binding, allowing a client to use one key for authentication and another for token use. +Similarly to {{MTLS}}, {{HTTPSIG}} could be used as a generic client authentication mechanism for the client calling the AS for any authenticated call, including token PAR, the token endpoint. Since {{HTTPSIG}} allows for multiple signatures with different signature parameters (including `tag`), this could be layered on top of even the runtime token request key binding, allowing a client to use one key for authentication and another for token use. ## AS Responses diff --git a/tools/examples/content-digest.hdr b/tools/examples/content-digest.hdr index 172d2db..45397ce 100644 --- a/tools/examples/content-digest.hdr +++ b/tools/examples/content-digest.hdr @@ -1 +1,4 @@ -Content-Digest: sha-256=:4fEzRVTGqfZg7lqf/d3oxXu837pvb3L0GN24+F1VkZk=: +NOTE: '\' line wrapping per RFC 8792 + +Content-Digest: sha-256=:4fEzRVTGqfZg7lqf/d3oxXu837pvb3L0GN24+F\ + 1VkZk=: diff --git a/tools/examples/present-request-signed.http b/tools/examples/present-request-signed.http index 97bc86f..ffaadfa 100644 --- a/tools/examples/present-request-signed.http +++ b/tools/examples/present-request-signed.http @@ -5,7 +5,7 @@ Host: example.com Date: Mon, 20 Apr 2026 02:07:55 GMT Authorization: HTTPSig 2340897.34j123-134uh2345n Signature-Input: sig1=("@method" "@target-uri" "authorization")\ - ;created=1776650875;keyid="j-0Ny45NWmqGq6G4UxLjGjNuloktugtOW4jfGCCg\ - efQ";nonce="k9Jyxempel2305Nmx7Rk";tag="httpsig-oauth" -Signature: sig1=:kFJC2WoBbrQc8tsKiowIb8oeIA533qmKvzdKf8kndJ7kaLxGmm2v\ - 9+IPB8kLE0WUea8KryJGSV7ji1apLkeKBg==: + ;created=1776650875;nonce="k9Jyxempel2305Nmx7Rk"\ + ;tag="httpsig-oauth" +Signature: sig1=:uvIxaaJED8BxpYUL6cBd1v5uy24SptQnVQ35oKxWflg8GumiH77\ + 60va6P2qWxadrLodjGmIFna3bS70KxYv7CQ==: diff --git a/tools/examples/rs-request-signed.http b/tools/examples/rs-request-signed.http index 39414d0..dab7a0c 100644 --- a/tools/examples/rs-request-signed.http +++ b/tools/examples/rs-request-signed.http @@ -5,7 +5,7 @@ Host: example.com Date: Mon, 20 Apr 2026 02:07:55 GMT Authorization: HTTPSig 2340897.34j123-134uh2345n Signature-Input: sig1=("@method" "@target-uri" "authorization")\ - ;created=1776650875;keyid="test-key-ecdsa-p256"\ - ;nonce="k9Jyxempel2305Nmx7Rk";tag="httpsig-oauth" -Signature: sig1=:ggBLokBFCW7NcE4z1P5zV1wn7vmnRADiMcpAVFb1q8kOQ3nlhXqQ\ - c9LPWdz1h4FGRtImK9LqNdDdpn9G4o9zRw==: + ;created=1776650875;nonce="k9Jyxempel2305Nmx7Rk"\ + ;tag="httpsig-oauth" +Signature: sig1=:iIwwgqVdmaneSVYJaLAMCf9yz8SnvNfUPZgeyWdkEjsO3+29XB0\ + OLVBsR0w9Q+uw04yOVV1JeogdVvpyBhw3ZA==: diff --git a/tools/examples/rs-sig-base.sigbase b/tools/examples/rs-sig-base.sigbase index 72eb120..643760b 100644 --- a/tools/examples/rs-sig-base.sigbase +++ b/tools/examples/rs-sig-base.sigbase @@ -4,5 +4,5 @@ NOTE: '\' line wrapping per RFC 8792 "@target-uri": https://example.com/foo "authorization": HTTPSig 2340897.34j123-134uh2345n "@signature-params": ("@method" "@target-uri" "authorization")\ - ;created=1776650875;keyid="test-key-ecdsa-p256"\ - ;nonce="k9Jyxempel2305Nmx7Rk";tag="httpsig-oauth" + ;created=1776650875;nonce="k9Jyxempel2305Nmx7Rk"\ + ;tag="httpsig-oauth" diff --git a/tools/examples/token-request-prereg-signed.http b/tools/examples/token-request-prereg-signed.http new file mode 100644 index 0000000..be63218 --- /dev/null +++ b/tools/examples/token-request-prereg-signed.http @@ -0,0 +1,16 @@ +NOTE: '\' line wrapping per RFC 8792 + +POST /token HTTP/1.1 +Host: server.example.com +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Content-Type: application/x-www-form-urlencoded +Content-Digest: sha-256=:4fEzRVTGqfZg7lqf/d3oxXu837pvb3L0GN24+F\ + 1VkZk=: +Signature-Input: sig1=("@method" "@target-uri" "content-digest" \ + "authorization");created=1618884473;keyid="j-0Ny45NWmqGq6GQ"\ + ;nonce="b3k2pp5k7z-50gnX1b06";tag="httpsig-oauth-token-request" +Signature: sig1=:blPeRn3D3nkYqc1RNzCzge9E0n/Guf9yA1BBOeMFOOeSwuDdrVN\ + cc5KSUKSOabdVHXVExQT0v6J+e3QDc3zzBA==: + +grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_u\ + ri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb diff --git a/tools/examples/token-request-signed.http b/tools/examples/token-request-signed.http index 73b7249..a8606d5 100644 --- a/tools/examples/token-request-signed.http +++ b/tools/examples/token-request-signed.http @@ -4,14 +4,14 @@ POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded -Content-Digest: sha-256=:4fEzRVTGqfZg7lqf/d3oxXu837pvb3L0GN24+F1VkZk=: +Content-Digest: sha-256=:4fEzRVTGqfZg7lqf/d3oxXu837pvb3L0GN24+F\ + 1VkZk=: Signature-Input: sig1=("@method" "@target-uri" "content-digest" \ - "authorization");created=1618884473;keyid="j-0Ny45NWmqGq6G4UxLjGjNu\ - loktugtOW4jfGCCgefQ";alg="ed25519";nonce="b3k2pp5k7z-50gnX1b06"\ - ;tag="httpsig-oauth-token-request";pub_key_a=:iuemcj/GhRHmY/yCsMlDN\ - p3BQgPZDdG00VRsg/BgU3s=: -Signature: sig1=:ikilCUkL+BjDRXdC7m4eNgU/4dQtijp4BTgehqYRxJX+sPr394dx\ - qGAPzV99eUVDhN1cgAk2VFXvN2pNfpFTBA==: + "authorization");created=1618884473;alg="ed25519"\ + ;nonce="b3k2pp5k7z-50gnX1b06";tag="httpsig-oauth-token-request"\ + ;pub=:iuemcj/GhRHmY/yCsMlDNp3BQgPZDdG00VRsg/BgU3s=: +Signature: sig1=:W1jjnCpI59MI13gajmWP9DViwQHXhQdQ8r8q+h+Drp2JbJyu/md\ + QrMMkGuiMvkWWY/noYbi9L4cW/lTOZigsCQ==: -grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_ur\ - i=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb +grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_u\ + ri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb diff --git a/tools/genexamples.sh b/tools/genexamples.sh index f11710b..c935942 100755 --- a/tools/genexamples.sh +++ b/tools/genexamples.sh @@ -17,8 +17,8 @@ py() { python3 "$gensig" "$@"; } # --- Token request (runtime Ed25519 key) ----------------------------------- # #2 Content-Digest of the form body and #3 the signed token request. -# The runtime key is embedded as pub_key_a + keyid params (no Signature-Key -# header); the alg param is forced on by --runtime-key. +# The runtime key is embedded in the pub param; --runtime-key forces the alg +# param on and drops keyid. py "$ex/token-request.http" \ --key "$ex/token-key.jwk" \ --covered @method @target-uri content-digest authorization \ @@ -29,12 +29,26 @@ py "$ex/token-request.http" \ --out-digest "$ex/content-digest.hdr" \ --out-signed "$ex/token-request-signed.http" -# --- Presenting the bound token (Ed25519, keyid only) ---------------------- -# #7 (also reused verbatim for #8). Token is already bound, so no key is -# embedded here. +# --- Token request (pre-registered Ed25519 key) ---------------------------- +# The key is selected by keyid from the client's registered key set, so no +# alg or pub parameter appears; the algorithm comes from the JWK. +py "$ex/token-request.http" \ + --key "$ex/token-key.jwk" \ + --key-id j-0Ny45NWmqGq6GQ \ + --covered @method @target-uri content-digest authorization \ + --content-digest \ + --tag httpsig-oauth-token-request \ + --nonce b3k2pp5k7z-50gnX1b06 \ + --created 1618884473 \ + --out-signed "$ex/token-request-prereg-signed.http" + +# --- Presenting the bound token (Ed25519) ---------------------------------- +# #7 (also reused verbatim for #8). The token is already bound, so neither the +# key nor an identifier for it appears on the wire. py "$ex/rs-request.http" \ --key "$ex/token-key.jwk" \ --covered @method @target-uri authorization \ + --no-keyid \ --tag httpsig-oauth \ --nonce k9Jyxempel2305Nmx7Rk \ --created 1776650875 \ @@ -46,6 +60,7 @@ py "$ex/rs-request.http" \ --key "$ex/test-key-ecdsa-p256.pem" \ --key-id test-key-ecdsa-p256 \ --covered @method @target-uri authorization \ + --no-keyid \ --tag httpsig-oauth \ --nonce k9Jyxempel2305Nmx7Rk \ --created 1776650875 \ diff --git a/tools/gensig.py b/tools/gensig.py index d86c199..5cd44f9 100755 --- a/tools/gensig.py +++ b/tools/gensig.py @@ -70,7 +70,8 @@ def _wrap_at_column(text: str, max_len: int) -> str: indent = CONTINUATION_INDENT lines = [] while len(text) > max_len: - bp = _adjust_for_short_tail(max_len, text) + # -1 leaves room for the trailing "\" that joins the fold + bp = _adjust_for_short_tail(max_len - 1, text) if bp >= len(text): break # single trailing character: don't break lines.append(text[:bp]) @@ -142,7 +143,8 @@ def wrap_structured_field(line: str, max_len: int = 69) -> str: indent = CONTINUATION_INDENT parts = [] while len(line) > max_len: - bp = _adjust_for_short_tail(_find_structured_break(line, max_len), line) + # -1 leaves room for the trailing "\" that joins the fold + bp = _adjust_for_short_tail(_find_structured_break(line, max_len - 1), line) if bp >= len(line): break # single trailing character: don't break parts.append(line[:bp]) @@ -479,30 +481,23 @@ def parse_http_request(text: str) -> HTTPRequest: # --------------------------------------------------------------------------- def runtime_key_params(public_pem: bytes, algorithm: type) -> "collections.OrderedDict": - """Map a public key into HTTP Message Signature parameters per {#embed-keys}. + """Map a public key into the `pub` signature parameter per {#embed-keys}. Returns an OrderedDict of parameter name -> bytes (serialized as a Byte - Sequence by http_sfv). Covers EC (pub_key_x/pub_key_y), Ed25519 - (pub_key_a), and RSA (pub_key_n/pub_key_e). + Sequence by http_sfv). EC keys use the SEC1 uncompressed point form + (0x04 || X || Y); Ed25519 uses the raw 32-octet public key A. RSA is out + of scope for the inline representation. """ - from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa + from cryptography.hazmat.primitives.asymmetric import ec, ed25519 pub = load_pem_public_key(public_pem) params: "collections.OrderedDict[str, bytes]" = collections.OrderedDict() - if algorithm in (sig_algorithms.ECDSA_P256_SHA256,) or isinstance(pub, ec.EllipticCurvePublicKey): - numbers = pub.public_numbers() - size = (pub.curve.key_size + 7) // 8 - params["pub_key_x"] = numbers.x.to_bytes(size, "big") - params["pub_key_y"] = numbers.y.to_bytes(size, "big") + if isinstance(pub, ec.EllipticCurvePublicKey): + params["pub"] = pub.public_bytes(Encoding.X962, + PublicFormat.UncompressedPoint) elif isinstance(pub, ed25519.Ed25519PublicKey): - params["pub_key_a"] = pub.public_bytes(Encoding.Raw, PublicFormat.Raw) - elif isinstance(pub, rsa.RSAPublicKey): - numbers = pub.public_numbers() - n_len = (numbers.n.bit_length() + 7) // 8 - e_len = (numbers.e.bit_length() + 7) // 8 - params["pub_key_n"] = numbers.n.to_bytes(n_len, "big") - params["pub_key_e"] = numbers.e.to_bytes(e_len, "big") + params["pub"] = pub.public_bytes(Encoding.Raw, PublicFormat.Raw) else: raise ValueError(f"Cannot embed public key of type {type(pub).__name__}") @@ -515,6 +510,10 @@ class InspectableSigner(HTTPMessageSigner): If ``extra_params`` is set, those parameters are merged into every signature's parameters so they appear in Signature-Input and are covered by the signature base (used for runtime public-key embedding). + + If ``drop_keyid`` is set, the keyid parameter the library always emits is + removed. The draft omits keyid for runtime key introduction and for token + presentation. """ def __init__(self, *args, **kwargs): @@ -522,8 +521,11 @@ def __init__(self, *args, **kwargs): self.last_sig_base: Optional[str] = None self.last_sig_elements: Optional[dict] = None self.extra_params: Optional[dict] = None + self.drop_keyid: bool = False def _build_signature_base(self, message, *, covered_component_ids, signature_params): + if self.drop_keyid: + signature_params.pop("keyid", None) if self.extra_params: signature_params.update(self.extra_params) result = super()._build_signature_base( @@ -637,9 +639,11 @@ def build_arg_parser() -> argparse.ArgumentParser: p.add_argument("--include-alg-param", action="store_true", help="Include the 'alg' parameter in Signature-Input") p.add_argument("--runtime-key", action="store_true", - help="Embed the public key as signature parameters per the " - "draft's Embedding a Public Key Value section " - "(pub_key_*). Implies --include-alg-param.") + help="Embed the public key in the 'pub' signature parameter " + "per the draft's Embedding a Public Key Value section. " + "Implies --include-alg-param and --no-keyid.") + p.add_argument("--no-keyid", action="store_true", + help="Omit the 'keyid' parameter from Signature-Input") p.add_argument("--output", "-o", type=Path, default=None, help="Output file (default: stdout)") p.add_argument("--width", "-w", type=int, default=69, @@ -693,6 +697,11 @@ def main() -> None: key_resolver=key.as_resolver(), ) + # The draft omits keyid whenever the key is not identified by reference: + # runtime key introduction carries pub instead, and presentation to the RS + # relies on the access token binding. + signer.drop_keyid = args.no_keyid or args.runtime_key + if args.runtime_key: if key.public_pem is None: raise SystemExit("--runtime-key requires a key with public material")