diff --git a/bdns/mocks.go b/bdns/mocks.go index 36bf2e88d29..5c0b159631d 100644 --- a/bdns/mocks.go +++ b/bdns/mocks.go @@ -19,35 +19,37 @@ type MockClient struct { // LookupTXT is a mock func (mock *MockClient) LookupTXT(_ context.Context, hostname string) ([]string, ResolverAddrs, error) { - if hostname == "_acme-challenge.servfail.com" { + // The hostname prefix of "_vrr7uudrklshxb6l._acme-host-challenge" + // corresponds to `host` Scope dns-account-01 validation with an + // Account URL of `https://example.com/acme/acct/1`. + + switch hostname { + case "_acme-challenge.servfail.com": return nil, ResolverAddrs{"MockClient"}, fmt.Errorf("SERVFAIL") - } - if hostname == "_acme-challenge.good-dns01.com" { + case "_acme-challenge.good-dns01.com", + "_vrr7uudrklshxb6l._acme-host-challenge.good-dns01.com": // base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" // + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI")) // expected token + test account jwk thumbprint return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil - } - if hostname == "_acme-challenge.wrong-dns01.com" { + case "_acme-challenge.wrong-dns01.com", + "_vrr7uudrklshxb6l._acme-host-challenge.wrong-dns01.com": return []string{"a"}, ResolverAddrs{"MockClient"}, nil - } - if hostname == "_acme-challenge.wrong-many-dns01.com" { + case "_acme-challenge.wrong-many-dns01.com": return []string{"a", "b", "c", "d", "e"}, ResolverAddrs{"MockClient"}, nil - } - if hostname == "_acme-challenge.long-dns01.com" { + case "_acme-challenge.long-dns01.com": return []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ResolverAddrs{"MockClient"}, nil - } - if hostname == "_acme-challenge.no-authority-dns01.com" { + case "_acme-challenge.no-authority-dns01.com": // base64(sha256("LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" // + "." + "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI")) // expected token + test account jwk thumbprint return []string{"LPsIwTo7o8BoG0-vjCyGQGBWSVIPxI-i_X336eUOQZo"}, ResolverAddrs{"MockClient"}, nil - } - // empty-txts.com always returns zero TXT records - if hostname == "_acme-challenge.empty-txts.com" { + case "_acme-challenge.empty-txts.com", + "_vrr7uudrklshxb6l._acme-host-challenge.empty-txts.com": return []string{}, ResolverAddrs{"MockClient"}, nil + default: + return []string{"hostname"}, ResolverAddrs{"MockClient"}, nil } - return []string{"hostname"}, ResolverAddrs{"MockClient"}, nil } // makeTimeoutError returns a a net.OpError for which Timeout() returns true. diff --git a/cmd/akamai-purger/main.go b/cmd/akamai-purger/main.go index 3911deec9c1..8581eb11c4f 100644 --- a/cmd/akamai-purger/main.go +++ b/cmd/akamai-purger/main.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "os" + "slices" "strings" "sync" "time" @@ -193,6 +194,8 @@ func (ap *akamaiPurger) purgeBatch(batch [][]string) error { return nil } +// takeBatch returns a slice containing the next batch of entries from the purge stack. +// It copies at most entriesPerBatch entries from the top of the stack into a new slice which is returned. func (ap *akamaiPurger) takeBatch() [][]string { ap.Lock() defer ap.Unlock() @@ -211,7 +214,11 @@ func (ap *akamaiPurger) takeBatch() [][]string { } batchBegin := stackSize - batchSize - batch := ap.toPurge[batchBegin:] + batchEnd := stackSize + batch := make([][]string, batchSize) + for i, entry := range ap.toPurge[batchBegin:batchEnd] { + batch[i] = slices.Clone(entry) + } ap.toPurge = ap.toPurge[:batchBegin] return batch } diff --git a/cmd/config.go b/cmd/config.go index 114e2397ddc..5316922d2b5 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -99,7 +99,7 @@ type SMTPConfig struct { // it should offer. type PAConfig struct { DBConfig `validate:"-"` - Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01,endkeys"` + Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 dns-account-01 tls-alpn-01,endkeys"` } // CheckChallenges checks whether the list of challenges in the PA config diff --git a/core/challenges.go b/core/challenges.go index d5e7a87295e..e1171572927 100644 --- a/core/challenges.go +++ b/core/challenges.go @@ -20,6 +20,11 @@ func DNSChallenge01(token string) Challenge { return newChallenge(ChallengeTypeDNS01, token) } +// DNSAccountChallenge01 constructs a dns-account-01 challenge. +func DNSAccountChallenge01(token string) Challenge { + return newChallenge(ChallengeTypeDNSAccount01, token) +} + // TLSALPNChallenge01 constructs a tls-alpn-01 challenge. func TLSALPNChallenge01(token string) Challenge { return newChallenge(ChallengeTypeTLSALPN01, token) @@ -33,6 +38,8 @@ func NewChallenge(kind AcmeChallenge, token string) (Challenge, error) { return HTTPChallenge01(token), nil case ChallengeTypeDNS01: return DNSChallenge01(token), nil + case ChallengeTypeDNSAccount01: + return DNSAccountChallenge01(token), nil case ChallengeTypeTLSALPN01: return TLSALPNChallenge01(token), nil default: diff --git a/core/core_test.go b/core/core_test.go index 6d4dbe369c0..118899e29b6 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -36,6 +36,7 @@ func TestChallenges(t *testing.T) { test.Assert(t, ChallengeTypeHTTP01.IsValid(), "Refused valid challenge") test.Assert(t, ChallengeTypeDNS01.IsValid(), "Refused valid challenge") + test.Assert(t, ChallengeTypeDNSAccount01.IsValid(), "Refused valid challenge") test.Assert(t, ChallengeTypeTLSALPN01.IsValid(), "Refused valid challenge") test.Assert(t, !AcmeChallenge("nonsense-71").IsValid(), "Accepted invalid challenge") } diff --git a/core/objects.go b/core/objects.go index c58aaac0cb7..8156eb38eb7 100644 --- a/core/objects.go +++ b/core/objects.go @@ -53,21 +53,31 @@ type AcmeChallenge string // These types are the available challenges const ( - ChallengeTypeHTTP01 = AcmeChallenge("http-01") - ChallengeTypeDNS01 = AcmeChallenge("dns-01") - ChallengeTypeTLSALPN01 = AcmeChallenge("tls-alpn-01") + ChallengeTypeHTTP01 = AcmeChallenge("http-01") + ChallengeTypeDNS01 = AcmeChallenge("dns-01") + ChallengeTypeDNSAccount01 = AcmeChallenge("dns-account-01") + ChallengeTypeTLSALPN01 = AcmeChallenge("tls-alpn-01") ) // IsValid tests whether the challenge is a known challenge func (c AcmeChallenge) IsValid() bool { switch c { - case ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01: + case ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeDNSAccount01, ChallengeTypeTLSALPN01: return true default: return false } } +// AuthorizationScope defines the scope of an authorization. +// This is used to determine challenge validation behavior. +type AuthorizationScope string + +const ( + AuthorizationScopeHost = AuthorizationScope("host") + AuthorizationScopeWildcard = AuthorizationScope("wildcard") +) + // OCSPStatus defines the state of OCSP for a domain type OCSPStatus string @@ -192,9 +202,15 @@ type Challenge struct { // For the V2 API the "URI" field is deprecated in favour of URL. URL string `json:"url,omitempty"` - // Used by http-01, tls-sni-01, tls-alpn-01 and dns-01 challenges + // Used by http-01, tls-sni-01, tls-alpn-01, dns-01 and dns-account-01 challenges Token string `json:"token,omitempty"` + // The scope of the authorization. Used by dns-account-01 challenge. + Scope AuthorizationScope `json:"scope,omitempty"` + + // Account URL. Used by dns-account-01 challenge. + AccountURL string `json:"accountUrl,omitempty"` + // The expected KeyAuthorization for validation of the challenge. Populated by // the RA prior to passing the challenge to the VA. For legacy reasons this // field is called "ProvidedKeyAuthorization" because it was initially set by @@ -256,7 +272,7 @@ func (ch Challenge) RecordsSane() bool { ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 { return false } - case ChallengeTypeDNS01: + case ChallengeTypeDNS01, ChallengeTypeDNSAccount01: if len(ch.ValidationRecord) > 1 { return false } @@ -372,6 +388,11 @@ type Authorization struct { // as part of the authorization, the identifier we store in the database // can contain an asterisk. Wildcard bool `json:"wildcard,omitempty" db:"-"` + + // The scope of the authorization. This is used internally for challenge + // validation (e.g. dns-account-01) but not stored in the database + // or represented externally. + Scope AuthorizationScope `json:"scope,omitempty" db:"-"` } // FindChallengeByStringID will look for a challenge matching the given ID inside diff --git a/core/objects_test.go b/core/objects_test.go index 61ee2c5c87a..317abe5167a 100644 --- a/core/objects_test.go +++ b/core/objects_test.go @@ -59,7 +59,7 @@ func TestChallengeSanityCheck(t *testing.T) { }`), &accountKey) test.AssertNotError(t, err, "Error unmarshaling JWK") - types := []AcmeChallenge{ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01} + types := []AcmeChallenge{ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeDNSAccount01, ChallengeTypeTLSALPN01} for _, challengeType := range types { chall := Challenge{ Type: challengeType, diff --git a/core/proto/core.pb.go b/core/proto/core.pb.go index 50852a4be29..291c9bd83ac 100644 --- a/core/proto/core.pb.go +++ b/core/proto/core.pb.go @@ -26,7 +26,7 @@ type Challenge struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Next unused field number: 13 + // Next unused field number: 15 Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` @@ -36,6 +36,8 @@ type Challenge struct { Validationrecords []*ValidationRecord `protobuf:"bytes,10,rep,name=validationrecords,proto3" json:"validationrecords,omitempty"` Error *ProblemDetails `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` Validated *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=validated,proto3" json:"validated,omitempty"` + Scope string `protobuf:"bytes,13,opt,name=scope,proto3" json:"scope,omitempty"` + AccountURL string `protobuf:"bytes,14,opt,name=accountURL,proto3" json:"accountURL,omitempty"` } func (x *Challenge) Reset() { @@ -133,6 +135,20 @@ func (x *Challenge) GetValidated() *timestamppb.Timestamp { return nil } +func (x *Challenge) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *Challenge) GetAccountURL() string { + if x != nil { + return x.AccountURL + } + return "" +} + type ValidationRecord struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -612,13 +628,14 @@ type Authorization struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Next unused field number: 10 + // Next unused field number: 11 Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Identifier string `protobuf:"bytes,2,opt,name=identifier,proto3" json:"identifier,omitempty"` RegistrationID int64 `protobuf:"varint,3,opt,name=registrationID,proto3" json:"registrationID,omitempty"` Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` Expires *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expires,proto3" json:"expires,omitempty"` Challenges []*Challenge `protobuf:"bytes,6,rep,name=challenges,proto3" json:"challenges,omitempty"` + Scope string `protobuf:"bytes,10,opt,name=scope,proto3" json:"scope,omitempty"` } func (x *Authorization) Reset() { @@ -695,6 +712,13 @@ func (x *Authorization) GetChallenges() []*Challenge { return nil } +func (x *Authorization) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + type Order struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -893,7 +917,7 @@ var file_core_proto_rawDesc = []byte{ 0x0a, 0x0a, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0xd9, 0x02, 0x0a, 0x09, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, + 0x6f, 0x74, 0x6f, 0x22, 0x8f, 0x03, 0x0a, 0x09, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, @@ -913,150 +937,155 @@ var file_core_proto_rawDesc = []byte{ 0x72, 0x6f, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, 0x4a, 0x04, 0x08, - 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x0b, 0x10, 0x0c, 0x22, - 0x94, 0x02, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, - 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x55, 0x73, 0x65, - 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x55, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, - 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x65, 0x73, 0x54, 0x72, 0x69, 0x65, 0x64, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0c, - 0x52, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x54, 0x72, 0x69, 0x65, 0x64, - 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, - 0x72, 0x41, 0x64, 0x64, 0x72, 0x73, 0x22, 0x6a, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, - 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x62, - 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, - 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, - 0x69, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x06, - 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6d, 0x70, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, + 0x4c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x55, 0x52, 0x4c, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, + 0x04, 0x08, 0x0b, 0x10, 0x0c, 0x22, 0x94, 0x02, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x55, 0x73, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, + 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x26, + 0x0a, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x54, 0x72, 0x69, 0x65, 0x64, + 0x18, 0x07, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, + 0x73, 0x54, 0x72, 0x69, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x73, 0x22, 0x6a, 0x0a, 0x0e, + 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x20, + 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x74, 0x74, 0x70, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x68, 0x74, + 0x74, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, + 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, + 0x65, 0x72, 0x12, 0x32, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x05, + 0x10, 0x06, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0xd5, 0x03, 0x0a, 0x11, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x44, + 0x0a, 0x0f, 0x6f, 0x63, 0x73, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x6f, 0x63, 0x73, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x12, 0x3c, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, + 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, + 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x15, 0x6c, 0x61, 0x73, 0x74, + 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x67, 0x53, 0x65, 0x6e, + 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x67, 0x53, 0x65, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x08, 0x6e, 0x6f, + 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, - 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x06, - 0x10, 0x07, 0x22, 0xd5, 0x03, 0x0a, 0x11, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x44, 0x0a, 0x0f, 0x6f, 0x63, 0x73, 0x70, - 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x6f, - 0x63, 0x73, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x3c, - 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0d, - 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x67, 0x53, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x15, 0x6c, - 0x61, 0x73, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x67, - 0x53, 0x65, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, - 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, + 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, + 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x02, + 0x10, 0x03, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, + 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, + 0x22, 0x88, 0x02, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x12, 0x28, 0x0a, + 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, + 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x67, 0x72, 0x65, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x67, 0x72, 0x65, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, + 0x49, 0x50, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x6c, 0x49, 0x50, 0x12, 0x38, 0x0a, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, - 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, - 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, - 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x04, - 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, - 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x22, 0x88, 0x02, 0x0a, 0x0c, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x18, 0x0a, - 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x63, 0x74, 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, - 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x67, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x67, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x50, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x50, 0x12, 0x38, 0x0a, - 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4a, - 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xf8, 0x01, 0x0a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x2f, 0x0a, - 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, - 0x67, 0x65, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x73, 0x4a, 0x04, - 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, - 0x22, 0xd3, 0x03, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, - 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, - 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x12, 0x28, 0x0a, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, - 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, - 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x36, 0x0a, 0x16, - 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, - 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, - 0x4e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, - 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x22, 0x7a, 0x0a, 0x08, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, 0x08, 0x03, - 0x10, 0x04, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, - 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0x8e, 0x02, 0x0a, 0x0d, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, + 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x26, 0x0a, + 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, + 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, + 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, + 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, + 0x6e, 0x67, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, + 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0xd3, 0x03, 0x0a, + 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, + 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, + 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, + 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, + 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, + 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x0b, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, 0x0a, + 0x10, 0x0b, 0x22, 0x7a, 0x0a, 0x08, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x38, + 0x0a, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x42, 0x2b, + 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, + 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, + 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/core/proto/core.proto b/core/proto/core.proto index 38608f92019..9f0530f579d 100644 --- a/core/proto/core.proto +++ b/core/proto/core.proto @@ -6,7 +6,7 @@ option go_package = "github.com/letsencrypt/boulder/core/proto"; import "google/protobuf/timestamp.proto"; message Challenge { - // Next unused field number: 13 + // Next unused field number: 15 int64 id = 1; string type = 2; string status = 6; @@ -19,6 +19,8 @@ message Challenge { reserved 8; // Unused and accidentally skipped during initial commit. reserved 11; // Previously validatedNS google.protobuf.Timestamp validated = 12; + string scope = 13; + string accountURL = 14; } message ValidationRecord { @@ -88,7 +90,7 @@ message Registration { } message Authorization { - // Next unused field number: 10 + // Next unused field number: 11 string id = 1; string identifier = 2; int64 registrationID = 3; @@ -98,6 +100,7 @@ message Authorization { repeated core.Challenge challenges = 6; reserved 7; // previously ACMEv1 combinations reserved 8; // previously v2 + string scope = 10; } message Order { diff --git a/grpc/pb-marshalling.go b/grpc/pb-marshalling.go index 33c2a5b63e5..81d001fa875 100644 --- a/grpc/pb-marshalling.go +++ b/grpc/pb-marshalling.go @@ -87,6 +87,8 @@ func ChallengeToPB(challenge core.Challenge) (*corepb.Challenge, error) { Error: prob, Validationrecords: recordAry, Validated: validated, + Scope: string(challenge.Scope), + AccountURL: challenge.AccountURL, }, nil } @@ -123,6 +125,8 @@ func PBToChallenge(in *corepb.Challenge) (challenge core.Challenge, err error) { Error: prob, ValidationRecord: recordAry, Validated: validated, + Scope: core.AuthorizationScope(in.Scope), + AccountURL: in.AccountURL, } if in.KeyAuthorization != "" { ch.ProvidedKeyAuthorization = in.KeyAuthorization @@ -323,6 +327,7 @@ func AuthzToPB(authz core.Authorization) (*corepb.Authorization, error) { Status: string(authz.Status), Expires: expires, Challenges: challs, + Scope: string(authz.Scope), }, nil } @@ -347,6 +352,7 @@ func PBToAuthz(pb *corepb.Authorization) (core.Authorization, error) { Status: core.AcmeStatus(pb.Status), Expires: expires, Challenges: challs, + Scope: core.AuthorizationScope(pb.Scope), } return authz, nil } diff --git a/grpc/pb-marshalling_test.go b/grpc/pb-marshalling_test.go index 6f287690082..636effc20a5 100644 --- a/grpc/pb-marshalling_test.go +++ b/grpc/pb-marshalling_test.go @@ -60,6 +60,8 @@ func TestChallenge(t *testing.T) { Token: "asd", ProvidedKeyAuthorization: "keyauth", Validated: &validated, + Scope: core.AuthorizationScopeHost, + AccountURL: "https://example.com/acme/acct/1", } pb, err := ChallengeToPB(chall) @@ -261,6 +263,7 @@ func TestAuthz(t *testing.T) { Status: core.StatusPending, Expires: nil, Challenges: []core.Challenge{challA, challB}, + Scope: core.AuthorizationScopeHost, } pbAuthz2, err := AuthzToPB(inAuthzNilExpires) test.AssertNotError(t, err, "AuthzToPB failed") diff --git a/policy/pa.go b/policy/pa.go index d872d5cbef9..4c2dc4aa105 100644 --- a/policy/pa.go +++ b/policy/pa.go @@ -516,18 +516,26 @@ func (pa *AuthorityImpl) checkHostLists(domain string) error { func (pa *AuthorityImpl) challengeTypesFor(identifier identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) { var challenges []core.AcmeChallenge + // Provide DNS-based challenges based on what is enabled. + if pa.ChallengeTypeEnabled(core.ChallengeTypeDNS01) { + challenges = append(challenges, core.ChallengeTypeDNS01) + } + if pa.ChallengeTypeEnabled(core.ChallengeTypeDNSAccount01) { + challenges = append(challenges, core.ChallengeTypeDNSAccount01) + } + // If the identifier is for a DNS wildcard name we only - // provide a DNS-01 challenge as a matter of CA policy. + // allow a DNS-based challenge as a matter of CA policy. if strings.HasPrefix(identifier.Value, "*.") { - // We must have the DNS-01 challenge type enabled to create challenges for - // a wildcard identifier per LE policy. + // We must have a DNS-based challenge type enabled to create challenges for + // a wildcard identifier per CA policy. if !pa.ChallengeTypeEnabled(core.ChallengeTypeDNS01) { - return nil, fmt.Errorf( - "Challenges requested for wildcard identifier but DNS-01 " + - "challenge type is not enabled") + if !pa.ChallengeTypeEnabled(core.ChallengeTypeDNSAccount01) { + return nil, fmt.Errorf( + "Challenges requested for wildcard identifier but a DNS-01 " + + "or DNS-ACCOUNT-01 challenge type is not enabled") + } } - // Only provide a DNS-01-Wildcard challenge - challenges = []core.AcmeChallenge{core.ChallengeTypeDNS01} } else { // Otherwise we collect up challenges based on what is enabled. if pa.ChallengeTypeEnabled(core.ChallengeTypeHTTP01) { @@ -537,10 +545,6 @@ func (pa *AuthorityImpl) challengeTypesFor(identifier identifier.ACMEIdentifier) if pa.ChallengeTypeEnabled(core.ChallengeTypeTLSALPN01) { challenges = append(challenges, core.ChallengeTypeTLSALPN01) } - - if pa.ChallengeTypeEnabled(core.ChallengeTypeDNS01) { - challenges = append(challenges, core.ChallengeTypeDNS01) - } } return challenges, nil diff --git a/policy/pa_test.go b/policy/pa_test.go index f754c43585a..ba3c0eb376c 100644 --- a/policy/pa_test.go +++ b/policy/pa_test.go @@ -394,28 +394,65 @@ func TestChallengesForWildcard(t *testing.T) { Value: "*.zombo.com", } - // First try to get a challenge for the wildcard ident without the - // DNS-01 challenge type enabled. This should produce an error + // Try to get a challenge for the wildcard ident without + // DNS challenges enabled. This should error. var enabledChallenges = map[core.AcmeChallenge]bool{ core.ChallengeTypeHTTP01: true, - core.ChallengeTypeDNS01: false, } pa := must.Do(New(enabledChallenges, blog.NewMock())) _, err := pa.ChallengesFor(wildcardIdent) test.AssertError(t, err, "ChallengesFor did not error for a wildcard ident "+ - "when DNS-01 was disabled") + "when DNS challenge types were disabled") test.AssertEquals(t, err.Error(), "Challenges requested for wildcard "+ - "identifier but DNS-01 challenge type is not enabled") + "identifier but a DNS-01 or DNS-ACCOUNT-01 challenge type is not enabled") - // Try again with DNS-01 enabled. It should not error and + // Enable DNS-01 and HTTP-01. It should not error and // should return only one DNS-01 type challenge - enabledChallenges[core.ChallengeTypeDNS01] = true + enabledChallenges = map[core.AcmeChallenge]bool{ + core.ChallengeTypeHTTP01: true, + core.ChallengeTypeDNS01: true, + } pa = must.Do(New(enabledChallenges, blog.NewMock())) challenges, err := pa.ChallengesFor(wildcardIdent) test.AssertNotError(t, err, "ChallengesFor errored for a wildcard ident "+ - "unexpectedly") + "unexpectedly with DNS-01 enabled") test.AssertEquals(t, len(challenges), 1) test.AssertEquals(t, challenges[0].Type, core.ChallengeTypeDNS01) + + // Enable DNS-ACCOUNT-01 and HTTP-01. It should not error and + // should return only one DNS-ACCOUNT-01 type challenge + enabledChallenges = map[core.AcmeChallenge]bool{ + core.ChallengeTypeHTTP01: true, + core.ChallengeTypeDNSAccount01: true, + } + pa = must.Do(New(enabledChallenges, blog.NewMock())) + challenges, err = pa.ChallengesFor(wildcardIdent) + test.AssertNotError(t, err, "ChallengesFor errored for a wildcard ident "+ + "unexpectedly with DNS-ACCOUNT-01 enabled") + test.AssertEquals(t, len(challenges), 1) + test.AssertEquals(t, challenges[0].Type, core.ChallengeTypeDNSAccount01) + + // Enable DNS-01, DNS-ACCOUNT-01 and HTTP-01. It should not error and + // should return one DNS-01 and one DNS-ACCOUNT-01 type challenge + enabledChallenges = map[core.AcmeChallenge]bool{ + core.ChallengeTypeHTTP01: true, + core.ChallengeTypeDNS01: true, + core.ChallengeTypeDNSAccount01: true, + } + pa = must.Do(New(enabledChallenges, blog.NewMock())) + challenges, err = pa.ChallengesFor(wildcardIdent) + test.AssertNotError(t, err, "ChallengesFor errored for a wildcard ident "+ + "unexpectedly with DNS-01 and DNS-ACCOUNT-01 enabled") + test.AssertEquals(t, len(challenges), 2) + + challengeTypes := make(map[string]bool) + for _, challenge := range challenges { + challengeTypes[string(challenge.Type)] = true + } + test.Assert(t, challengeTypes[string(core.ChallengeTypeDNS01)], + "Expected challenge type DNS-01 not found") + test.Assert(t, challengeTypes[string(core.ChallengeTypeDNSAccount01)], + "Expected challenge type DNS-ACCOUNT-01 not found") } // TestMalformedExactBlocklist tests that loading a YAML policy file with an diff --git a/ra/ra.go b/ra/ra.go index ea609da8f8b..2a8bc955edf 100644 --- a/ra/ra.go +++ b/ra/ra.go @@ -2535,7 +2535,9 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New // that doesn't meet this criteria from SA.GetAuthorizations but we verify // again to be safe. if strings.HasPrefix(name, "*.") && - len(authz.Challenges) == 1 && core.AcmeChallenge(authz.Challenges[0].Type) == core.ChallengeTypeDNS01 { + len(authz.Challenges) == 1 && + (core.AcmeChallenge(authz.Challenges[0].Type) == core.ChallengeTypeDNS01 || + core.AcmeChallenge(authz.Challenges[0].Type) == core.ChallengeTypeDNSAccount01) { authzID, err := strconv.ParseInt(authz.Id, 10, 64) if err != nil { return nil, err diff --git a/ra/ra_test.go b/ra/ra_test.go index 20dfc579f13..42bad759916 100644 --- a/ra/ra_test.go +++ b/ra/ra_test.go @@ -90,6 +90,11 @@ func createPendingAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, do Type: core.ChallengeTypeDNS01, Status: core.StatusPending, }, + { + Token: core.NewToken(), + Type: core.ChallengeTypeDNSAccount01, + Status: core.StatusPending, + }, { Token: core.NewToken(), Type: core.ChallengeTypeTLSALPN01, diff --git a/sa/model.go b/sa/model.go index 099f6f8bf0b..1360d69e410 100644 --- a/sa/model.go +++ b/sa/model.go @@ -502,16 +502,22 @@ func modelToOrderv2(om *orderModelv2) (*corepb.Order, error) { return order, nil } +// challTypeToUint maps challenge types to integers. These integers are used as inputs to a bitmap. +// They provide shift distances on the bitmap and must be between 0 and 7 (inclusive). var challTypeToUint = map[string]uint8{ - "http-01": 0, - "dns-01": 1, - "tls-alpn-01": 2, + "http-01": 0, + "dns-01": 1, + "tls-alpn-01": 2, + "dns-account-01": 3, } +// uintToChallType is the reverse mapping of challTypeToUint. It maps integers to challenge types. +// The integers are used as inputs to a bitmap and must be between 0 and 7 (inclusive). var uintToChallType = map[uint8]string{ 0: "http-01", 1: "dns-01", 2: "tls-alpn-01", + 3: "dns-account-01", } var identifierTypeToUint = map[string]uint8{ diff --git a/test/config-next/ca.json b/test/config-next/ca.json index cb1f3f34155..772e8095688 100644 --- a/test/config-next/ca.json +++ b/test/config-next/ca.json @@ -124,6 +124,7 @@ "challenges": { "http-01": true, "dns-01": true, + "dns-account-01": true, "tls-alpn-01": true } }, diff --git a/test/config-next/ra.json b/test/config-next/ra.json index beb13c22f3a..62685aa5ca0 100644 --- a/test/config-next/ra.json +++ b/test/config-next/ra.json @@ -133,6 +133,7 @@ "challenges": { "http-01": true, "dns-01": true, + "dns-account-01": true, "tls-alpn-01": true } }, diff --git a/test/integration/challenge_test.go b/test/integration/challenge_test.go new file mode 100644 index 00000000000..b082b34c91d --- /dev/null +++ b/test/integration/challenge_test.go @@ -0,0 +1,74 @@ +//go:build integration + +package integration + +import ( + "crypto/sha256" + "encoding/base32" + "fmt" + "os" + "strings" + "testing" + + "github.com/eggsampler/acme/v3" + "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/test" +) + +func TestDNSAccountChallenge(t *testing.T) { + t.Parallel() + + // Skip this test if not in the config-next directory. + if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { + t.Skip("Skipping test in config") + } + + // Create an account. + client, err := makeClient("mailto:example@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + + // Create one non-wildcard and one wildcard domain. + hostDomain := random_domain() + wildDomain := fmt.Sprintf("*.%s", random_domain()) + + // Create an order with the domains. + ids := []acme.Identifier{ + {Type: "dns", Value: hostDomain}, + {Type: "dns", Value: wildDomain}, + } + order, err := client.Client.NewOrder(client.Account, ids) + test.AssertNotError(t, err, "failed to create order") + + // Iterate over the authorizations and complete the DNS-ACCOUNT-01 challenges. + for _, authUrl := range order.Authorizations { + auth, err := client.Client.FetchAuthorization(client.Account, authUrl) + test.AssertNotError(t, err, "failed to fetch authorization") + + // Find the DNS-ACCOUNT-01 challenge. + chal, ok := auth.ChallengeMap[acme.ChallengeTypeDNSAccount01] + test.Assert(t, ok, fmt.Sprintf("no DNS-ACCOUNT-01 challenge at %s", authUrl)) + + // Construct the validation domain name and add to the DNS. + chalHost := getValidationDomainName(client.Account.URL, auth.Wildcard, auth.Identifier.Value) + err = addDNSResponse(chalHost, chal.KeyAuthorization) + test.AssertNotError(t, err, "failed adding DNS-ACCOUNT-01 response") + + // Complete the challenge and remove the DNS entry. + chal, err = client.Client.UpdateChallenge(client.Account, chal) + delDNSResponse(chalHost) + test.AssertNotError(t, err, "failed updating challenge") + } +} + +// Implements acme-scoped-dns-challenges validation domain name construction per: +// "_" || base32(SHA-256()[0:10]) || "._acme-" || || "-challenge" +func getValidationDomainName(accountURL string, wildcard bool, domain string) string { + scope := core.AuthorizationScopeHost + if wildcard { + scope = core.AuthorizationScopeWildcard + } + acctHash := sha256.Sum256([]byte(accountURL)) + acctLabel := strings.ToLower(base32.StdEncoding.EncodeToString(acctHash[0:10])) + chalDomain := fmt.Sprintf("_%s._acme-%s-challenge.%s.", acctLabel, scope, domain) + return chalDomain +} diff --git a/test/integration/common_test.go b/test/integration/common_test.go index 289f55b241d..4ae9b6df975 100644 --- a/test/integration/common_test.go +++ b/test/integration/common_test.go @@ -7,9 +7,11 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" + "encoding/base64" "encoding/hex" "fmt" "net/http" @@ -57,6 +59,41 @@ func makeClient(contacts ...string) (*client, error) { return &client{account, c}, nil } +func addDNSResponse(host, keyAuthorization string) error { + // Encode the key authorization for the DNS TXT record. + h := sha256.Sum256([]byte(keyAuthorization)) + txt := base64.RawURLEncoding.EncodeToString(h[:]) + resp, err := http.Post("http://boulder.service.consul:8055/set-txt", "", + bytes.NewBufferString(fmt.Sprintf(`{ + "host": "%s", + "value": "%s" + }`, host, txt))) + if err != nil { + return fmt.Errorf("adding dns response: %s", err) + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("adding dns response: status %d", resp.StatusCode) + } + resp.Body.Close() + return nil +} + +func delDNSResponse(host string) error { + resp, err := http.Post("http://boulder.service.consul:8055/clear-txt", "", + bytes.NewBufferString(fmt.Sprintf(`{ + "host": "%s" + }`, host))) + if err != nil { + return fmt.Errorf("deleting dns response: %s", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("deleting dns response: status %d", resp.StatusCode) + } + return nil +} + func addHTTP01Response(token, keyAuthorization string) error { resp, err := http.Post("http://boulder.service.consul:8055/add-http01", "", bytes.NewBufferString(fmt.Sprintf(`{ diff --git a/test/load-generator/acme/challenge.go b/test/load-generator/acme/challenge.go index 47e8d861d96..518cb019ea6 100644 --- a/test/load-generator/acme/challenge.go +++ b/test/load-generator/acme/challenge.go @@ -21,9 +21,10 @@ const ( RandomChallengeStrategy = "RANDOM" // The following challenge strategies will always pick the named challenge // type or return an error if there isn't a challenge of that type to pick. - HTTP01ChallengeStrategy = "HTTP-01" - DNS01ChallengeStrategy = "DNS-01" - TLSALPN01ChallengeStrategy = "TLS-ALPN-01" + HTTP01ChallengeStrategy = "HTTP-01" + DNS01ChallengeStrategy = "DNS-01" + DNSACCOUNT01ChallengeStrategy = "DNS-ACCOUNT-01" + TLSALPN01ChallengeStrategy = "TLS-ALPN-01" ) // NewChallengeStrategy returns the ChallengeStrategy for the given @@ -37,6 +38,8 @@ func NewChallengeStrategy(rawName string) (ChallengeStrategy, error) { preferredType = core.ChallengeTypeHTTP01 case DNS01ChallengeStrategy: preferredType = core.ChallengeTypeDNS01 + case DNSACCOUNT01ChallengeStrategy: + preferredType = core.ChallengeTypeDNSAccount01 case TLSALPN01ChallengeStrategy: preferredType = core.ChallengeTypeTLSALPN01 default: diff --git a/test/load-generator/acme/challenge_test.go b/test/load-generator/acme/challenge_test.go index 68b713866c6..67be8406558 100644 --- a/test/load-generator/acme/challenge_test.go +++ b/test/load-generator/acme/challenge_test.go @@ -30,6 +30,11 @@ func TestNewChallengeStrategy(t *testing.T) { InputName: "DNS-01", ExpectedStratType: "*acme.preferredTypeChallengeStrategy", }, + { + Name: "known name, DNS-ACCOUNT-01", + InputName: "DNS-ACCOUNT-01", + ExpectedStratType: "*acme.preferredTypeChallengeStrategy", + }, { Name: "known name, TLS-ALPN-01", InputName: "TLS-ALPN-01", diff --git a/test/load-generator/main.go b/test/load-generator/main.go index 1baed067388..f660886b2bc 100644 --- a/test/load-generator/main.go +++ b/test/load-generator/main.go @@ -34,7 +34,7 @@ type Config struct { Results string // path to save metrics to MaxRegs int // maximum number of registrations to create MaxNamesPerCert int // maximum number of names on one certificate/order - ChallengeStrategy string // challenge selection strategy ("random", "http-01", "dns-01", "tls-alpn-01") + ChallengeStrategy string // challenge selection strategy ("random", "http-01", "dns-01", "dns-account-01", "tls-alpn-01") RevokeChance float32 // chance of revoking certificate after issuance, between 0.0 and 1.0 } diff --git a/va/dns.go b/va/dns.go index 93ce38511f6..1aec6e1abaa 100644 --- a/va/dns.go +++ b/va/dns.go @@ -4,9 +4,11 @@ import ( "context" "crypto/sha256" "crypto/subtle" + "encoding/base32" "encoding/base64" "fmt" "net" + "strings" "github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/core" @@ -48,19 +50,15 @@ func availableAddresses(allAddrs []net.IP) (v4 []net.IP, v6 []net.IP) { return } -func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident identifier.ACMEIdentifier, challenge core.Challenge) ([]core.ValidationRecord, error) { - if ident.Type != identifier.DNS { - va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident) - return nil, berrors.MalformedError("Identifier type for DNS was not itself DNS") - } - - // Compute the digest of the key authorization file - h := sha256.New() - h.Write([]byte(challenge.ProvidedKeyAuthorization)) - authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) - +// validateTXT will query for all TXT records associated with challengeSubdomain and +// return a ValidationRecord if the authorizedKeysDigest is found in the TXT records. +func (va *ValidationAuthorityImpl) validateTXT( + ctx context.Context, + ident identifier.ACMEIdentifier, + authorizedKeysDigest string, + challengeSubdomain string, +) ([]core.ValidationRecord, error) { // Look for the required record in the DNS - challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, ident.Value) txts, resolvers, err := va.dnsClient.LookupTXT(ctx, challengeSubdomain) if err != nil { return nil, berrors.DNSError("%s", err) @@ -91,3 +89,73 @@ func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident iden return nil, berrors.UnauthorizedError("Incorrect TXT record %q%s found at %s", invalidRecord, andMore, challengeSubdomain) } + +func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident identifier.ACMEIdentifier, challenge core.Challenge) ([]core.ValidationRecord, error) { + if ident.Type != identifier.DNS { + va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident) + return nil, berrors.MalformedError("Identifier type for DNS was not itself DNS") + } + + // Compute the digest of the key authorization file + h := sha256.New() + h.Write([]byte(challenge.ProvidedKeyAuthorization)) + authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + + // Look for the required record in the DNS + challengeSubdomain := fmt.Sprintf("%s.%s", core.DNSPrefix, ident.Value) + + // Return the validation record if the correct TXT record is found + return va.validateTXT(ctx, ident, authorizedKeysDigest, challengeSubdomain) +} + +// Compute the DNS-ACCOUNT-01 challenge subdomain per the +// acme-scoped-dns-challenges specification +func getDNSAccountChallengeSubdomain( + accountResourceURL string, + scope core.AuthorizationScope, + domain string, +) string { + // "_" || base32(SHA-256()[0:10]) || "._acme-" || || "-challenge" + acctHash := sha256.Sum256([]byte(accountResourceURL)) + acctLabel := strings.ToLower(base32.StdEncoding.EncodeToString(acctHash[0:10])) + challengeSubdomain := fmt.Sprintf("_%s._acme-%s-challenge.%s", + acctLabel, scope, domain) + + return challengeSubdomain +} + +// validateDNSAccount01 validates a DNS-ACCOUNT-01 challenge using the account's URI +// (derived from the accountID) and the authorization scope. +func (va *ValidationAuthorityImpl) validateDNSAccount01(ctx context.Context, + ident identifier.ACMEIdentifier, + challenge core.Challenge, +) ([]core.ValidationRecord, error) { + if ident.Type != identifier.DNS { + va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident) + return nil, berrors.MalformedError("Identifier type for DNS was not itself DNS") + } + + // Reject unsupported scopes + if challenge.Scope != core.AuthorizationScopeHost && challenge.Scope != core.AuthorizationScopeWildcard { + va.log.Infof("Unsupported scope for DNS-ACCOUNT-01 challenge: %s", challenge.Scope) + return nil, berrors.MalformedError("Unsupported scope for DNS-ACCOUNT-01 challenge") + } + + // Compute the digest of the key authorization file + h := sha256.New() + h.Write([]byte(challenge.ProvidedKeyAuthorization)) + authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + + // Compute the challenge subdomain for this account + challengeSubdomain := getDNSAccountChallengeSubdomain(challenge.AccountURL, challenge.Scope, ident.Value) + + // Look for the required record in the DNS + validationRecords, err := va.validateTXT(ctx, ident, authorizedKeysDigest, challengeSubdomain) + if err == nil { + // Successful challenge validation + return validationRecords, nil + } + + // Return error from last accountURIPrefix attempted + return nil, err +} diff --git a/va/dns_test.go b/va/dns_test.go index edac03b8429..6f3995c9b35 100644 --- a/va/dns_test.go +++ b/va/dns_test.go @@ -22,6 +22,10 @@ func dnsChallenge() core.Challenge { return createChallenge(core.ChallengeTypeDNS01) } +func dnsAccountChallenge() core.Challenge { + return createChallenge(core.ChallengeTypeDNSAccount01) +} + func TestDNSValidationEmpty(t *testing.T) { va, _ := setup(nil, 0, "", nil, nil) @@ -39,6 +43,23 @@ func TestDNSValidationEmpty(t *testing.T) { }, 1) } +func TestDNSAccountValidationEmpty(t *testing.T) { + va, _ := setup(nil, 0, "", nil, nil) + + // This test calls PerformValidation directly, because that is where the + // metrics checked below are incremented. + req := createValidationRequest("empty-txts.com", core.ChallengeTypeDNSAccount01) + res, _ := va.PerformValidation(context.Background(), req) + test.AssertEquals(t, res.Problems.ProblemType, "unauthorized") + test.AssertEquals(t, res.Problems.Detail, "No TXT record found at _vrr7uudrklshxb6l._acme-host-challenge.empty-txts.com") + + test.AssertMetricWithLabelsEquals(t, va.metrics.validationTime, prometheus.Labels{ + "type": "dns-account-01", + "result": "invalid", + "problem_type": "unauthorized", + }, 1) +} + func TestDNSValidationWrong(t *testing.T) { va, _ := setup(nil, 0, "", nil, nil) _, err := va.validateDNS01(context.Background(), dnsi("wrong-dns01.com"), dnsChallenge()) @@ -49,6 +70,17 @@ func TestDNSValidationWrong(t *testing.T) { test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" found at _acme-challenge.wrong-dns01.com") } +func TestDNSAccountValidationWrong(t *testing.T) { + va, _ := setup(nil, 0, "", nil, nil) + + _, err := va.validateDNSAccount01(context.Background(), dnsi("wrong-dns01.com"), dnsAccountChallenge()) + if err == nil { + t.Fatalf("Successful DNS validation with wrong TXT record") + } + prob := detailedError(err) + test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" found at _vrr7uudrklshxb6l._acme-host-challenge.wrong-dns01.com") +} + func TestDNSValidationWrongMany(t *testing.T) { va, _ := setup(nil, 0, "", nil, nil) @@ -80,6 +112,15 @@ func TestDNSValidationFailure(t *testing.T) { test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) } +func TestDNSAccountValidationFailure(t *testing.T) { + va, _ := setup(nil, 0, "", nil, nil) + + _, err := va.validateDNSAccount01(ctx, dnsi("localhost"), dnsAccountChallenge()) + prob := detailedError(err) + + test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) +} + func TestDNSValidationInvalid(t *testing.T) { var notDNS = identifier.ACMEIdentifier{ Type: identifier.IdentifierType("iris"), @@ -94,6 +135,32 @@ func TestDNSValidationInvalid(t *testing.T) { test.AssertEquals(t, prob.Type, probs.MalformedProblem) } +func TestDNSAccountValidationInvalid(t *testing.T) { + var notDNS = identifier.ACMEIdentifier{ + Type: identifier.IdentifierType("iris"), + Value: "790DB180-A274-47A4-855F-31C428CB1072", + } + + va, _ := setup(nil, 0, "", nil, nil) + + _, err := va.validateDNS01(ctx, notDNS, dnsAccountChallenge()) + prob := detailedError(err) + + test.AssertEquals(t, prob.Type, probs.MalformedProblem) +} + +func TestDNSAccountValidationUnsupportedScope(t *testing.T) { + va, _ := setup(nil, 0, "", nil, nil) + + chall := dnsAccountChallenge() + chall.Scope = core.AuthorizationScope("invalid") + + _, err := va.validateDNSAccount01(ctx, dnsi("localhost"), chall) + prob := detailedError(err) + + test.AssertEquals(t, prob.Type, probs.MalformedProblem) +} + func TestDNSValidationNotSane(t *testing.T) { va, _ := setup(nil, 0, "", nil, nil) @@ -169,6 +236,14 @@ func TestDNSValidationOK(t *testing.T) { test.Assert(t, prob == nil, "Should be valid.") } +func TestDNSAccountValidationOK(t *testing.T) { + va, _ := setup(nil, 0, "", nil, nil) + + _, prob := va.validateChallenge(ctx, dnsi("good-dns01.com"), dnsAccountChallenge()) + + test.Assert(t, prob == nil, "Should be valid.") +} + func TestDNSValidationNoAuthorityOK(t *testing.T) { va, _ := setup(nil, 0, "", nil, nil) @@ -251,3 +326,15 @@ func TestAvailableAddresses(t *testing.T) { } } } + +func TestGetDNSAccountChallengeSubdomain(t *testing.T) { + // Test that the DNS account challenge subdomain is correctly generated + // using example values from: + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-scoped-dns-challenges-00 + const accountResourceURL = "https://example.com/acme/acct/ExampleAccount" + const baseValidationDomain = "example.org" + const validationScope = core.AuthorizationScopeWildcard + const expectedSubdomain = "_ujmmovf2vn55tgye._acme-wildcard-challenge.example.org" + subdomain := getDNSAccountChallengeSubdomain(accountResourceURL, validationScope, baseValidationDomain) + test.AssertEquals(t, subdomain, expectedSubdomain) +} diff --git a/va/va.go b/va/va.go index c97539083b1..28dd5275eba 100644 --- a/va/va.go +++ b/va/va.go @@ -430,6 +430,9 @@ func (va *ValidationAuthorityImpl) validate( challenge core.Challenge, ) ([]core.ValidationRecord, error) { + // Set default scope for host-only. + challenge.Scope = core.AuthorizationScopeHost + // If the identifier is a wildcard domain we need to validate the base // domain by removing the "*." wildcard prefix. We create a separate // `baseIdentifier` here before starting the `va.checkCAA` goroutine with the @@ -437,6 +440,8 @@ func (va *ValidationAuthorityImpl) validate( baseIdentifier := identifier if strings.HasPrefix(identifier.Value, "*.") { baseIdentifier.Value = strings.TrimPrefix(identifier.Value, "*.") + // Set the authorization scope for wildcard. + challenge.Scope = core.AuthorizationScopeWildcard } validationRecords, err := va.validateChallenge(ctx, baseIdentifier, challenge) @@ -465,6 +470,8 @@ func (va *ValidationAuthorityImpl) validateChallenge(ctx context.Context, identi return va.validateHTTP01(ctx, identifier, challenge) case core.ChallengeTypeDNS01: return va.validateDNS01(ctx, identifier, challenge) + case core.ChallengeTypeDNSAccount01: + return va.validateDNSAccount01(ctx, identifier, challenge) case core.ChallengeTypeTLSALPN01: return va.validateTLSALPN01(ctx, identifier, challenge) } diff --git a/va/va_test.go b/va/va_test.go index 2aa26daa677..019b9f22bfa 100644 --- a/va/va_test.go +++ b/va/va_test.go @@ -83,6 +83,7 @@ func TestMain(m *testing.M) { } var accountURIPrefixes = []string{"http://boulder.service.consul:4000/acme/reg/"} +var accountKeyID = string("https://example.com/acme/acct/1") func createValidationRequest(domain string, challengeType core.AcmeChallenge) *vapb.PerformValidationRequest { return &vapb.PerformValidationRequest{ @@ -93,6 +94,7 @@ func createValidationRequest(domain string, challengeType core.AcmeChallenge) *v Token: expectedToken, Validationrecords: nil, KeyAuthorization: expectedKeyAuthorization, + AccountURL: accountKeyID, }, Authz: &vapb.AuthzMeta{ Id: "", @@ -108,6 +110,8 @@ func createChallenge(challengeType core.AcmeChallenge) core.Challenge { Token: expectedToken, ValidationRecord: []core.ValidationRecord{}, ProvidedKeyAuthorization: expectedKeyAuthorization, + Scope: core.AuthorizationScopeHost, + AccountURL: accountKeyID, } } diff --git a/wfe2/wfe.go b/wfe2/wfe.go index f4eef8f8b91..67d3dcf402f 100644 --- a/wfe2/wfe.go +++ b/wfe2/wfe.go @@ -1217,9 +1217,11 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz // Update the challenge URL to be relative to the HTTP request Host challenge.URL = web.RelativeEndpoint(request, fmt.Sprintf("%s%s/%s", challengePath, authz.ID, challenge.StringID())) - // Ensure the challenge URI isn't written by setting it to - // a value that the JSON omitempty tag considers empty + // Ensure the challenge URI, AccountURL and Scope aren't written by + // setting them to a value that the JSON omitempty tag considers empty challenge.URI = "" + challenge.Scope = "" + challenge.AccountURL = "" // ACMEv2 never sends the KeyAuthorization back in a challenge object. challenge.ProvidedKeyAuthorization = "" @@ -1247,6 +1249,7 @@ func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(request *http.Request, a } authz.ID = "" authz.RegistrationID = 0 + authz.Scope = "" // The ACME spec forbids allowing "*" in authorization identifiers. Boulder // allows this internally as a means of tracking when an authorization @@ -1290,7 +1293,7 @@ func (wfe *WebFrontEndImpl) postChallenge( authz core.Authorization, challengeIndex int, logEvent *web.RequestEvent) { - body, _, currAcct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) + body, jws, currAcct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) addRequesterHeader(response, logEvent.Requester) if prob != nil { // validPOSTForAccount handles its own setting of logEvent.Errors @@ -1337,6 +1340,15 @@ func (wfe *WebFrontEndImpl) postChallenge( return } + // The JWS `kid` is needed for AccountURL-scoped Challenge validation. + kid := jws.Signatures[0].Protected.KeyID + if kid == "" { + wfe.sendError(response, logEvent, probs.Malformed("No JWS kid field"), nil) + return + } + ch := &authz.Challenges[challengeIndex] + ch.AccountURL = kid + authzPB, err := bgrpc.AuthzToPB(authz) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to serialize authz"), err)