Add CAAIdentities and Website to /directory "meta". (#3588)
This commit updates the WFE and WFE2 to have configuration support for setting a value for the `/directory` object's "meta" field's optional "caaIdentities" and "website" fields. The config-next wfe/wfe2 configuration are updated with values for these fields. Unit tests are updated to check that they are sent when expected and not otherwise. Bonus content: The `test.AssertUnmarshaledEquals` function had a bug where it would consider two inputs equal when the # of keys differed. This commit also fixes that bug.
This commit is contained in:
parent
866627ee29
commit
17922a6d2d
|
@ -43,6 +43,14 @@ type config struct {
|
||||||
SAService *cmd.GRPCClientConfig
|
SAService *cmd.GRPCClientConfig
|
||||||
|
|
||||||
Features map[string]bool
|
Features map[string]bool
|
||||||
|
|
||||||
|
// DirectoryCAAIdentity is used for the /directory response's "meta"
|
||||||
|
// element's "caaIdentities" field. It should match the VA's "issuerDomain"
|
||||||
|
// configuration value (this value is the one used to enforce CAA)
|
||||||
|
DirectoryCAAIdentity string
|
||||||
|
// DirectoryWebsite is used for the /directory response's "meta" element's
|
||||||
|
// "website" field.
|
||||||
|
DirectoryWebsite string
|
||||||
}
|
}
|
||||||
|
|
||||||
Syslog cmd.SyslogConfig
|
Syslog cmd.SyslogConfig
|
||||||
|
@ -99,6 +107,8 @@ func main() {
|
||||||
wfe.AllowOrigins = c.WFE.AllowOrigins
|
wfe.AllowOrigins = c.WFE.AllowOrigins
|
||||||
wfe.AcceptRevocationReason = c.WFE.AcceptRevocationReason
|
wfe.AcceptRevocationReason = c.WFE.AcceptRevocationReason
|
||||||
wfe.AllowAuthzDeactivation = c.WFE.AllowAuthzDeactivation
|
wfe.AllowAuthzDeactivation = c.WFE.AllowAuthzDeactivation
|
||||||
|
wfe.DirectoryCAAIdentity = c.WFE.DirectoryCAAIdentity
|
||||||
|
wfe.DirectoryWebsite = c.WFE.DirectoryWebsite
|
||||||
|
|
||||||
wfe.IssuerCert, err = cmd.LoadCert(c.Common.IssuerCert)
|
wfe.IssuerCert, err = cmd.LoadCert(c.Common.IssuerCert)
|
||||||
cmd.FailOnError(err, fmt.Sprintf("Couldn't read issuer cert [%s]", c.Common.IssuerCert))
|
cmd.FailOnError(err, fmt.Sprintf("Couldn't read issuer cert [%s]", c.Common.IssuerCert))
|
||||||
|
|
|
@ -52,6 +52,14 @@ type config struct {
|
||||||
CertificateChains map[string][]string
|
CertificateChains map[string][]string
|
||||||
|
|
||||||
Features map[string]bool
|
Features map[string]bool
|
||||||
|
|
||||||
|
// DirectoryCAAIdentity is used for the /directory response's "meta"
|
||||||
|
// element's "caaIdentities" field. It should match the VA's "issuerDomain"
|
||||||
|
// configuration value (this value is the one used to enforce CAA)
|
||||||
|
DirectoryCAAIdentity string
|
||||||
|
// DirectoryWebsite is used for the /directory response's "meta" element's
|
||||||
|
// "website" field.
|
||||||
|
DirectoryWebsite string
|
||||||
}
|
}
|
||||||
|
|
||||||
Syslog cmd.SyslogConfig
|
Syslog cmd.SyslogConfig
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
"acceptRevocationReason": true,
|
"acceptRevocationReason": true,
|
||||||
"allowAuthzDeactivation": true,
|
"allowAuthzDeactivation": true,
|
||||||
"debugAddr": ":8000",
|
"debugAddr": ":8000",
|
||||||
|
"directoryCAAIdentity": "happy-hacker-ca.invalid",
|
||||||
|
"directoryWebsite": "https://github.com/letsencrypt/boulder",
|
||||||
"tls": {
|
"tls": {
|
||||||
"caCertFile": "test/grpc-creds/minica.pem",
|
"caCertFile": "test/grpc-creds/minica.pem",
|
||||||
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",
|
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
"acceptRevocationReason": true,
|
"acceptRevocationReason": true,
|
||||||
"allowAuthzDeactivation": true,
|
"allowAuthzDeactivation": true,
|
||||||
"debugAddr": ":8013",
|
"debugAddr": ":8013",
|
||||||
|
"directoryCAAIdentity": "happy-hacker-ca.invalid",
|
||||||
|
"directoryWebsite": "https://github.com/letsencrypt/boulder",
|
||||||
"tls": {
|
"tls": {
|
||||||
"caCertFile": "test/grpc-creds/minica.pem",
|
"caCertFile": "test/grpc-creds/minica.pem",
|
||||||
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",
|
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",
|
||||||
|
|
|
@ -100,6 +100,9 @@ func AssertUnmarshaledEquals(t *testing.T, got, expected string) {
|
||||||
AssertNotError(t, err, "Could not unmarshal 'got'")
|
AssertNotError(t, err, "Could not unmarshal 'got'")
|
||||||
err = json.Unmarshal([]byte(expected), &expectedMap)
|
err = json.Unmarshal([]byte(expected), &expectedMap)
|
||||||
AssertNotError(t, err, "Could not unmarshal 'expected'")
|
AssertNotError(t, err, "Could not unmarshal 'expected'")
|
||||||
|
if len(gotMap) != len(expectedMap) {
|
||||||
|
t.Errorf("Expected had %d keys, got had %d", len(gotMap), len(expectedMap))
|
||||||
|
}
|
||||||
for k, v := range expectedMap {
|
for k, v := range expectedMap {
|
||||||
if !reflect.DeepEqual(v, gotMap[k]) {
|
if !reflect.DeepEqual(v, gotMap[k]) {
|
||||||
t.Errorf("Field %q: Expected \"%v\", got \"%v\"", k, v, gotMap[k])
|
t.Errorf("Field %q: Expected \"%v\", got \"%v\"", k, v, gotMap[k])
|
||||||
|
|
29
wfe/wfe.go
29
wfe/wfe.go
|
@ -75,6 +75,15 @@ type WebFrontEndImpl struct {
|
||||||
// URL to the current subscriber agreement (should contain some version identifier)
|
// URL to the current subscriber agreement (should contain some version identifier)
|
||||||
SubscriberAgreementURL string
|
SubscriberAgreementURL string
|
||||||
|
|
||||||
|
// DirectoryCAAIdentity is used for the /directory response's "meta"
|
||||||
|
// element's "caaIdentities" field. It should match the VA's issuerDomain
|
||||||
|
// field value.
|
||||||
|
DirectoryCAAIdentity string
|
||||||
|
|
||||||
|
// DirectoryWebsite is used for the /directory response's "meta" element's
|
||||||
|
// "website" field.
|
||||||
|
DirectoryWebsite string
|
||||||
|
|
||||||
// Register of anti-replay nonces
|
// Register of anti-replay nonces
|
||||||
nonceService *nonce.NonceService
|
nonceService *nonce.NonceService
|
||||||
|
|
||||||
|
@ -351,14 +360,28 @@ func (wfe *WebFrontEndImpl) Directory(ctx context.Context, logEvent *web.Request
|
||||||
// expected set of keys. This ensures that we can properly extend the directory when we
|
// expected set of keys. This ensures that we can properly extend the directory when we
|
||||||
// need to add a new endpoint or meta element.
|
// need to add a new endpoint or meta element.
|
||||||
directoryEndpoints[core.RandomString(8)] = randomDirKeyExplanationLink
|
directoryEndpoints[core.RandomString(8)] = randomDirKeyExplanationLink
|
||||||
}
|
|
||||||
if !clientDirChangeIntolerant {
|
|
||||||
// ACME since draft-02 describes an optional "meta" directory entry. The
|
// ACME since draft-02 describes an optional "meta" directory entry. The
|
||||||
// meta entry may optionally contain a "terms-of-service" URI for the
|
// meta entry may optionally contain a "terms-of-service" URI for the
|
||||||
// current ToS.
|
// current ToS.
|
||||||
directoryEndpoints["meta"] = map[string]string{
|
metaMap := map[string]interface{}{
|
||||||
"terms-of-service": wfe.SubscriberAgreementURL,
|
"terms-of-service": wfe.SubscriberAgreementURL,
|
||||||
}
|
}
|
||||||
|
// The "meta" directory entry may also include a []string of CAA identities
|
||||||
|
if wfe.DirectoryCAAIdentity != "" {
|
||||||
|
// The specification says caaIdentities is an array of strings. In
|
||||||
|
// practice Boulder's VA only allows configuring ONE CAA identity. Given
|
||||||
|
// that constraint it doesn't make sense to allow multiple directory CAA
|
||||||
|
// identities so we use just the `wfe.DirectoryCAAIdentity` alone.
|
||||||
|
metaMap["caaIdentities"] = []string{
|
||||||
|
wfe.DirectoryCAAIdentity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The "meta" directory entry may also include a string with a website URL
|
||||||
|
if wfe.DirectoryWebsite != "" {
|
||||||
|
metaMap["website"] = wfe.DirectoryWebsite
|
||||||
|
}
|
||||||
|
directoryEndpoints["meta"] = metaMap
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Header().Set("Content-Type", "application/json")
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
|
|
@ -730,7 +730,39 @@ func TestDirectory(t *testing.T) {
|
||||||
body = replaceRandomKey(responseWriter.Body.Bytes())
|
body = replaceRandomKey(responseWriter.Body.Bytes())
|
||||||
assertJSONEquals(t, string(body), fmt.Sprintf(`{"key-change":"http://localhost:4300/acme/key-change","meta":{"terms-of-service":"http://example.invalid/terms"},"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","%s":"%s","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`, randomKey, randomDirKeyExplanationLink))
|
assertJSONEquals(t, string(body), fmt.Sprintf(`{"key-change":"http://localhost:4300/acme/key-change","meta":{"terms-of-service":"http://example.invalid/terms"},"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","%s":"%s","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`, randomKey, randomDirKeyExplanationLink))
|
||||||
|
|
||||||
// if the UA is LetsEncryptPythonClient we expect to *not* see the meta entry.
|
// Configure a caaIdentity and website for the /directory meta
|
||||||
|
wfe.DirectoryCAAIdentity = "Radiant Lock"
|
||||||
|
wfe.DirectoryWebsite = "zombo.com"
|
||||||
|
responseWriter = httptest.NewRecorder()
|
||||||
|
url, _ = url.Parse("/directory")
|
||||||
|
mux.ServeHTTP(responseWriter, &http.Request{
|
||||||
|
Method: "GET",
|
||||||
|
URL: url,
|
||||||
|
Host: "localhost:4300",
|
||||||
|
})
|
||||||
|
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json")
|
||||||
|
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
|
||||||
|
// The directory response should include the CAAIdentities and Website meta
|
||||||
|
// elements as expected
|
||||||
|
body = replaceRandomKey(responseWriter.Body.Bytes())
|
||||||
|
assertJSONEquals(t, string(body), fmt.Sprintf(`{
|
||||||
|
"key-change": "http://localhost:4300/acme/key-change",
|
||||||
|
"meta": {
|
||||||
|
"caaIdentities": [
|
||||||
|
"Radiant Lock"
|
||||||
|
],
|
||||||
|
"terms-of-service": "http://example.invalid/terms",
|
||||||
|
"website": "zombo.com"
|
||||||
|
},
|
||||||
|
"%s": "%s",
|
||||||
|
"new-authz": "http://localhost:4300/acme/new-authz",
|
||||||
|
"new-cert": "http://localhost:4300/acme/new-cert",
|
||||||
|
"new-reg": "http://localhost:4300/acme/new-reg",
|
||||||
|
"revoke-cert": "http://localhost:4300/acme/revoke-cert"
|
||||||
|
}`, randomKey, randomDirKeyExplanationLink))
|
||||||
|
|
||||||
|
// if the UA is LetsEncryptPythonClient we expect to *not* see the meta entry,
|
||||||
|
// even with the DirectoryCAAIdentity and DirectoryWebsite configured.
|
||||||
responseWriter.Body.Reset()
|
responseWriter.Body.Reset()
|
||||||
url, _ = url.Parse("/directory")
|
url, _ = url.Parse("/directory")
|
||||||
headers := map[string][]string{
|
headers := map[string][]string{
|
||||||
|
@ -745,6 +777,7 @@ func TestDirectory(t *testing.T) {
|
||||||
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json")
|
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json")
|
||||||
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
|
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
|
||||||
assertJSONEquals(t, responseWriter.Body.String(), `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`)
|
assertJSONEquals(t, responseWriter.Body.String(), `{"new-authz":"http://localhost:4300/acme/new-authz","new-cert":"http://localhost:4300/acme/new-cert","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert"}`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRandomDirectoryKey(t *testing.T) {
|
func TestRandomDirectoryKey(t *testing.T) {
|
||||||
|
|
26
wfe2/wfe.go
26
wfe2/wfe.go
|
@ -82,6 +82,15 @@ type WebFrontEndImpl struct {
|
||||||
// URL to the current subscriber agreement (should contain some version identifier)
|
// URL to the current subscriber agreement (should contain some version identifier)
|
||||||
SubscriberAgreementURL string
|
SubscriberAgreementURL string
|
||||||
|
|
||||||
|
// DirectoryCAAIdentity is used for the /directory response's "meta"
|
||||||
|
// element's "caaIdentities" field. It should match the VA's issuerDomain
|
||||||
|
// field value.
|
||||||
|
DirectoryCAAIdentity string
|
||||||
|
|
||||||
|
// DirectoryWebsite is used for the /directory response's "meta" element's
|
||||||
|
// "website" field.
|
||||||
|
DirectoryWebsite string
|
||||||
|
|
||||||
// Register of anti-replay nonces
|
// Register of anti-replay nonces
|
||||||
nonceService *nonce.NonceService
|
nonceService *nonce.NonceService
|
||||||
|
|
||||||
|
@ -371,9 +380,24 @@ func (wfe *WebFrontEndImpl) Directory(
|
||||||
// ACME since draft-02 describes an optional "meta" directory entry. The
|
// ACME since draft-02 describes an optional "meta" directory entry. The
|
||||||
// meta entry may optionally contain a "termsOfService" URI for the
|
// meta entry may optionally contain a "termsOfService" URI for the
|
||||||
// current ToS.
|
// current ToS.
|
||||||
directoryEndpoints["meta"] = map[string]string{
|
metaMap := map[string]interface{}{
|
||||||
"termsOfService": wfe.SubscriberAgreementURL,
|
"termsOfService": wfe.SubscriberAgreementURL,
|
||||||
}
|
}
|
||||||
|
// The "meta" directory entry may also include a []string of CAA identities
|
||||||
|
if wfe.DirectoryCAAIdentity != "" {
|
||||||
|
// The specification says caaIdentities is an array of strings. In
|
||||||
|
// practice Boulder's VA only allows configuring ONE CAA identity. Given
|
||||||
|
// that constraint it doesn't make sense to allow multiple directory CAA
|
||||||
|
// identities so we use just the `wfe.DirectoryCAAIdentity` alone.
|
||||||
|
metaMap["caaIdentities"] = []string{
|
||||||
|
wfe.DirectoryCAAIdentity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The "meta" directory entry may also include a string with a website URL
|
||||||
|
if wfe.DirectoryWebsite != "" {
|
||||||
|
metaMap["website"] = wfe.DirectoryWebsite
|
||||||
|
}
|
||||||
|
directoryEndpoints["meta"] = metaMap
|
||||||
|
|
||||||
response.Header().Set("Content-Type", "application/json")
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
|
|
@ -721,6 +721,40 @@ func TestDirectory(t *testing.T) {
|
||||||
test.AssertEquals(t,
|
test.AssertEquals(t,
|
||||||
randomDirectoryKeyPresent(t, responseWriter.Body.Bytes()),
|
randomDirectoryKeyPresent(t, responseWriter.Body.Bytes()),
|
||||||
true)
|
true)
|
||||||
|
|
||||||
|
// Configure a caaIdentity and website for the /directory meta
|
||||||
|
wfe.DirectoryCAAIdentity = "Radiant Lock"
|
||||||
|
wfe.DirectoryWebsite = "zombo.com"
|
||||||
|
|
||||||
|
// Expect directory with a key change endpoint and a meta entry that has both
|
||||||
|
// a website and a caaIdentity
|
||||||
|
metaJSON = `{
|
||||||
|
"AAAAAAAAAAA": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
|
||||||
|
"keyChange": "http://localhost:4300/acme/key-change",
|
||||||
|
"meta": {
|
||||||
|
"caaIdentities": [
|
||||||
|
"Radiant Lock"
|
||||||
|
],
|
||||||
|
"termsOfService": "http://example.invalid/terms",
|
||||||
|
"website": "zombo.com"
|
||||||
|
},
|
||||||
|
"newAccount": "http://localhost:4300/acme/new-acct",
|
||||||
|
"newNonce": "http://localhost:4300/acme/new-nonce",
|
||||||
|
"newOrder": "http://localhost:4300/acme/new-order",
|
||||||
|
"revokeCert": "http://localhost:4300/acme/revoke-cert"
|
||||||
|
}`
|
||||||
|
// Serve the /directory response for this request into a recorder
|
||||||
|
responseWriter = httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(responseWriter, req)
|
||||||
|
// We expect all directory requests to return a json object with a good HTTP status
|
||||||
|
test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json")
|
||||||
|
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
|
||||||
|
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), metaJSON)
|
||||||
|
// Check if there is a random directory key present and if so, that it is
|
||||||
|
// expected to be present
|
||||||
|
test.AssertEquals(t,
|
||||||
|
randomDirectoryKeyPresent(t, responseWriter.Body.Bytes()),
|
||||||
|
true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRelativeDirectory(t *testing.T) {
|
func TestRelativeDirectory(t *testing.T) {
|
||||||
|
|
Loading…
Reference in New Issue