-
Notifications
You must be signed in to change notification settings - Fork 112
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
Address signature reuse vulnerability #223
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1500,6 +1500,30 @@ validation of domain names. If ACME is extended in the future to support other | |
types of identifier, there will need to be new Challenge types, and they will | ||
need to specify which types of identifier they apply to. | ||
|
||
## Authorized Keys Objects | ||
|
||
Several of the challenges in this document makes use of an "authorized keys" | ||
object. Such an object is a JSON array of objects, where each object encodes an | ||
authorization for a specific account key to fulfill a specific challenge. | ||
|
||
token (required, string): | ||
: The "token" field from the challenge object | ||
|
||
key (required, JWK): | ||
: The account key being authorized | ||
|
||
~~~~~~~~~~ | ||
[ | ||
{ | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA", | ||
"key": /* account key, as a JWK object */ | ||
} | ||
] | ||
~~~~~~~~~~ | ||
|
||
A JSON array is used so that a client can use a single authorized keys object to | ||
satisfy several challenges at once. | ||
|
||
## Simple HTTP | ||
|
||
With Simple HTTP validation, the client in an ACME transaction proves its | ||
|
@@ -1527,30 +1551,19 @@ It MUST NOT contain any characters outside the URL-safe Base64 alphabet. | |
} | ||
~~~~~~~~~~ | ||
|
||
A client responds to this challenge by signing a JWS object and provisioning it | ||
as a resource on the HTTP server for the domain in question. The payload of the | ||
JWS MUST be a JSON dictionary containing the fields "type", "token", and | ||
"tls" from the ACME challenge and response (see below), and no other fields. If | ||
the "tls" field is not included in the response, then validation object MUST | ||
have its "tls" field set to "true". The JWS MUST be signed with the client's | ||
account key pair. This JWS is NOT REQUIRED to have a "nonce" header parameter | ||
(as with the JWS objects that carry ACME request objects), but MUST otherwise | ||
meet the guidelines laid out in {{terminology}}. | ||
A client responds to this challenge by creating an authorized keys object for | ||
this challenge and the client's account key and provisioning it as a resource on | ||
the HTTP server for the domain in question. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Goes back to previous comment. Shouldn't that provisioned resource be JWS signed as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You say above that the client provisions a JWS, but that's not so now |
||
|
||
~~~~~~~~~~ | ||
{ | ||
"type": "simpleHttp", | ||
[{ | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA", | ||
"tls": false | ||
} | ||
"key": /* account key, as a JWK object */ | ||
}] | ||
~~~~~~~~~~ | ||
|
||
The path at which the resource is provisioned is comprised of the fixed prefix | ||
".well-known/acme-challenge/", followed by the "token" value in the challenge. | ||
|
||
~~~~~~~~~~ | ||
.well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA | ||
~~~~~~~~~~ | ||
The resource MUST be provisioned under the fixed path | ||
".well-known/acme-challenge/". | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ending slash confusingly suggests a directory, how about dropping it? |
||
|
||
The client's response to this challenge indicates whether it would prefer for | ||
the validation request to be sent over TLS: | ||
|
@@ -1575,25 +1588,19 @@ Given a Challenge/Response pair, the server verifies the client's control of the | |
domain by verifying that the resource was provisioned as expected. | ||
|
||
1. Form a URI by populating the URI template {{RFC6570}} | ||
"{scheme}://{domain}/.well-known/acme-challenge/{token}", where: | ||
"{scheme}://{domain}/.well-known/acme-challenge/", where: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
* the scheme field is set to "http" if the "tls" field in the response is | ||
present and set to false, and "https" otherwise; | ||
* the domain field is set to the domain name being verified; and | ||
* the token field is the token provided in the challenge. | ||
* the domain field is set to the domain name being verified | ||
2. Verify that the resulting URI is well-formed. | ||
3. Dereference the URI using an HTTP or HTTPS GET request. If using HTTPS, the | ||
ACME server MUST ignore the certificate provided by the HTTPS server. | ||
4. Verify that the Content-Type header of the response is either absent, or has | ||
the value "application/jose+json". | ||
5. Verify that the body of the response is a valid JWS, signed with the client's | ||
account key. | ||
6. Verify that the payload of the JWS meets the following criteria: | ||
* it is a valid JSON dictionary; | ||
* it has exactly three fields; | ||
* its "type" field is set to "simpleHttp"; | ||
* its "token" field is equal to the "token" field in the challenge; | ||
* its "tls" field is equal to the "tls" field in the response, or "true" if | ||
the "tls" field was absent. | ||
the value "application/json". | ||
5. Verify that the body of the response is well-formed authorized keys object. | ||
6. Verify that for at least one of the entries in the authorized keys object | ||
* the "token" value is the same as the "token" value for this challenge | ||
* the "key" value is the account key used to issue this challenge | ||
|
||
Comparisons of the "token" field MUST be performed in terms of | ||
Unicode code points, taking into account the encodings of the stored nonce and | ||
|
@@ -1621,37 +1628,25 @@ token (required, string): | |
least 128 bits of entropy, in order to prevent an attacker from guessing it. | ||
It MUST NOT contain any characters outside the URL-safe Base64 alphabet. | ||
|
||
~~~~~~~~~~ | ||
{ | ||
"type": "dvsni", | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA" | ||
} | ||
~~~~~~~~~~ | ||
|
||
In response to the challenge, the client uses its account private key to sign a | ||
JWS over a JSON object describing the challenge. The validation object covered | ||
by the signature MUST have the following fields and no others: | ||
|
||
type (required, string): | ||
: The string "dvsni" | ||
|
||
token (required, string): | ||
: The token value from the server-provided challenge object | ||
authorizedKeys (required, string): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's really odd for the server-provided challenge to already have an |
||
: A serialized authorized keys object, base64-encoded. This object MUST have | ||
only one entry, whose token value matches the "token" value in the challenge, | ||
and "key" value matches the client's account key. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe I see what you are saying here, but I think it should be crisper. The point isn't that you must have one matching key, but rather that it must ahve one key and it must match. "The object MUST have exactly one entry. That entry's token value must match..." |
||
|
||
~~~~~~~~~~ | ||
{ | ||
"type": "dvsni", | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA" | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA", | ||
"authorizedKeys": "odyHtABZt47RZfacMq3zL...xIWRXBCCvl61bYo7ATU6Z4" | ||
} | ||
~~~~~~~~~~ | ||
|
||
The client serializes the validation object to UTF-8, then uses its account | ||
private key to sign a JWS with the serialized JSON object as its payload. This | ||
JWS is NOT REQUIRED to have the "nonce" header parameter. | ||
|
||
The client will compute Z, the SHA-256 of the "signature" value from the JWS. | ||
The hash is calculated over the base64-encoded signature string. Z is encoded | ||
in hexadecimal form. | ||
In response to the challenge, the client MUST decode and parse the authorized | ||
keys object and verify that it contains exactly one entry, whose "token" and | ||
"key" attributes match the token for this challenge and the client's account | ||
key. The client then computes the SHA-256 digest Z of the JSON-encoded | ||
authorized keys object (without base64-encoding), and encodes Z in hexadecimal | ||
form. | ||
|
||
The client will generate a self-signed certificate with the | ||
subjectAlternativeName extension containing the dNSName | ||
|
@@ -1660,37 +1655,26 @@ server at the domain such that when a handshake is initiated with the Server | |
Name Indication extension set to "\<Z[0:32]\>.\<Z[32:64]\>.acme.invalid", the | ||
generated test certificate is presented. | ||
|
||
The response to the DVSNI challenge provides the validation JWS to the server. | ||
The response to the DVSNI challenge simply acknowledges that the client is ready | ||
to fulfill this challenge. | ||
|
||
type (required, string): | ||
: The string "dvsni" | ||
|
||
validation (required, string): | ||
: The JWS object computed with the validation object and the account key | ||
|
||
~~~~~~~~~~ | ||
{ | ||
"type": "dvsni", | ||
"validation": { | ||
"header": { "alg": "RS256" }, | ||
"payload": "qzu9...6bjn", | ||
"signature": "gfj9XqFv07e1wU66hSLYkiFqYakPSjAu8TsyXRg85nM" | ||
} | ||
"type": "dvsni" | ||
} | ||
~~~~~~~~~~ | ||
|
||
Given a Challenge/Response pair, the ACME server verifies the client's control | ||
of the domain by verifying that the TLS server was configured appropriately. | ||
|
||
1. Verify the validation JWS using the account key for which the challenge | ||
was issued. | ||
2. Decode the payload of the JWS as UTF-8 encoded JSON. | ||
3. Verify that there are exactly two fields in the decoded object, and that: | ||
* The "type" field is set to "dvsni" | ||
* The "token" field matches the "token" value in the challenge | ||
1. Compute the Z-value from the authorized keys object in the same way as the | ||
client. | ||
4. Open a TLS connection to the domain name being validated on port 443, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: Use 2. 3. rather than 4. 5. Sorry I missed this before. |
||
presenting the value "\<Z[0:32]\>.\<Z[32:64]\>.acme.invalid" in the SNI | ||
field. | ||
field (where the comparison is case-insensitive). | ||
5. Verify that the certificate contains a subjectAltName extension with the | ||
dNSName of "\<Z[0:32]\>.\<Z[32:64]\>.acme.invalid". | ||
|
||
|
@@ -1833,76 +1817,53 @@ in DNS. This value MUST have at least 128 bits of entropy, in order to | |
prevent an attacker from guessing it. It MUST NOT contain any characters | ||
outside the URL-safe Base64 alphabet. | ||
|
||
~~~~~~~~~~ | ||
{ | ||
"type": "dns", | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA" | ||
} | ||
~~~~~~~~~~ | ||
|
||
In response to this challenge, the client uses its account private key to sign a | ||
JWS over a JSON object describing the challenge. The validation object covered | ||
by the signature MUST have the following fields and no others: | ||
|
||
type (required, string): | ||
: The string "dns" | ||
|
||
token (required, string): | ||
: The token value from the server-provided challenge object | ||
authorizedKeys (required, string): | ||
: A serialized authorized keys object, base64-encoded. This object MUST have | ||
only one entry, whose token value matches the "token" value in the challenge, | ||
and "key" value matches the client's account key. | ||
|
||
~~~~~~~~~~ | ||
{ | ||
"type": "dns", | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA" | ||
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA", | ||
"authorizedKeys": "odyHtABZt47RZfacMq3zL...xIWRXBCCvl61bYo7ATU6Z4" | ||
} | ||
~~~~~~~~~~ | ||
|
||
The client serializes the validation object to UTF-8, then uses its account | ||
private key to sign a JWS with the serialized JSON object as its payload. This | ||
JWS is NOT REQUIRED to have the "nonce" header parameter. | ||
In response to the challenge, the client MUST decode and parse the authorized | ||
keys object and verify that it contains exactly one entry, whose "token" and | ||
"key" attributes match the token for this challenge and the client's account | ||
key. The client then computes the SHA-256 digest of the JSON-encoded | ||
authorized keys object (without base64-encoding). | ||
|
||
The record provisioned to the DNS is the "signature" value from the JWS, i.e., | ||
the base64-encoded signature value. The client constructs the validation domain | ||
name by appending the label "_acme-challenge" to the domain name being | ||
validated, then provisions a TXT record with the signature value under that | ||
name. For example, if the domain name being validated is "example.com", then the | ||
client would provision the following DNS record: | ||
The record provisioned to the DNS is the base64 encoding of this digest. The | ||
client constructs the validation domain name by prepending the label | ||
"_acme-challenge" to the domain name being validated, then provisions a TXT | ||
record with the digest value under that name. For example, if the domain name | ||
being validated is "example.com", then the client would provision the following | ||
DNS record: | ||
|
||
~~~~~~~~~~ | ||
_acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM" | ||
~~~~~~~~~~ | ||
|
||
The response to a DNS challenge provides the validation JWS to the server. | ||
The response to the DNS challenge simply acknowledges that the client is ready | ||
to fulfill this challenge. | ||
|
||
type (required, string): | ||
: The string "dns" | ||
|
||
validation (required, JWS): | ||
: The JWS object computed with the validation object and the account key | ||
|
||
~~~~~~~~~~ | ||
{ | ||
"type": "dns" | ||
"clientPublicKey": { "kty": "EC", ... }, | ||
"validation": { | ||
"header": { "alg": "HS256" }, | ||
"payload": "qzu9...6bjn", | ||
"signature": "gfj9XqFv07e1wU66hSLYkiFqYakPSjAu8TsyXRg85nM" | ||
} | ||
} | ||
~~~~~~~~~~ | ||
|
||
To validate a DNS challenge, the server performs the following steps: | ||
|
||
1. Verify the validation JWS using the account key for which this challenge was | ||
issued | ||
2. Decode the payload of the JWS as UTF-8 encoded JSON | ||
3. Verify that there are exactly two fields in the decoded object, and that: | ||
* The "type" field is set to "dns" | ||
* The "token" field matches the "token" value in the challenge | ||
4. Query for TXT records under the validation domain name | ||
5. Verify that the contents of one of the TXT records match the "signature" | ||
value in the "validation" JWS | ||
1. Compute the SHA-256 digest of the authorized keys object | ||
2. Query for TXT records under the validation domain name | ||
3. Verify that the contents of one of the TXT records matches the digest value | ||
|
||
If all of the above verifications succeed, then the validation is successful. | ||
If no DNS record is found, or DNS record and response payload do not pass these | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of
[{token: A, key: K1}, {token: B, key: K2}]
... which key do I use for the JWS? :O In other part of the spec we require that "JWS MUST use the Flattened JSON Serialization", which implies one signature (and key) per JWS. Actually, do we ever sign this object?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The object is never signed, so there's no problem.