diff --git a/va/proto/va.pb.go b/va/proto/va.pb.go index 7ab4220452e..523cb95e638 100644 --- a/va/proto/va.pb.go +++ b/va/proto/va.pb.go @@ -409,6 +409,148 @@ func (x *ValidationRequest) GetKeyAuthorization() string { return "" } +type CheckCAARequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Identifier *proto.Identifier `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"` + Challenge *proto.Challenge `protobuf:"bytes,2,opt,name=challenge,proto3" json:"challenge,omitempty"` + RegID int64 `protobuf:"varint,3,opt,name=regID,proto3" json:"regID,omitempty"` + AuthzID string `protobuf:"bytes,4,opt,name=authzID,proto3" json:"authzID,omitempty"` + IsRecheck bool `protobuf:"varint,5,opt,name=isRecheck,proto3" json:"isRecheck,omitempty"` +} + +func (x *CheckCAARequest) Reset() { + *x = CheckCAARequest{} + if protoimpl.UnsafeEnabled { + mi := &file_va_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckCAARequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckCAARequest) ProtoMessage() {} + +func (x *CheckCAARequest) ProtoReflect() protoreflect.Message { + mi := &file_va_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckCAARequest.ProtoReflect.Descriptor instead. +func (*CheckCAARequest) Descriptor() ([]byte, []int) { + return file_va_proto_rawDescGZIP(), []int{6} +} + +func (x *CheckCAARequest) GetIdentifier() *proto.Identifier { + if x != nil { + return x.Identifier + } + return nil +} + +func (x *CheckCAARequest) GetChallenge() *proto.Challenge { + if x != nil { + return x.Challenge + } + return nil +} + +func (x *CheckCAARequest) GetRegID() int64 { + if x != nil { + return x.RegID + } + return 0 +} + +func (x *CheckCAARequest) GetAuthzID() string { + if x != nil { + return x.AuthzID + } + return "" +} + +func (x *CheckCAARequest) GetIsRecheck() bool { + if x != nil { + return x.IsRecheck + } + return false +} + +type CheckCAAResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Problem *proto.ProblemDetails `protobuf:"bytes,1,opt,name=problem,proto3" json:"problem,omitempty"` + Perspective string `protobuf:"bytes,3,opt,name=perspective,proto3" json:"perspective,omitempty"` + Rir string `protobuf:"bytes,4,opt,name=rir,proto3" json:"rir,omitempty"` +} + +func (x *CheckCAAResult) Reset() { + *x = CheckCAAResult{} + if protoimpl.UnsafeEnabled { + mi := &file_va_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckCAAResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckCAAResult) ProtoMessage() {} + +func (x *CheckCAAResult) ProtoReflect() protoreflect.Message { + mi := &file_va_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckCAAResult.ProtoReflect.Descriptor instead. +func (*CheckCAAResult) Descriptor() ([]byte, []int) { + return file_va_proto_rawDescGZIP(), []int{7} +} + +func (x *CheckCAAResult) GetProblem() *proto.ProblemDetails { + if x != nil { + return x.Problem + } + return nil +} + +func (x *CheckCAAResult) GetPerspective() string { + if x != nil { + return x.Perspective + } + return "" +} + +func (x *CheckCAAResult) GetRir() string { + if x != nil { + return x.Rir + } + return "" +} + var File_va_proto protoreflect.FileDescriptor var file_va_proto_rawDesc = []byte{ @@ -466,20 +608,43 @@ var file_va_proto_rawDesc = []byte{ 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x12, 0x2a, 0x0a, 0x10, 0x6b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x32, 0x93, 0x01, 0x0a, 0x02, 0x56, 0x41, 0x12, 0x49, 0x0a, 0x11, 0x50, 0x65, 0x72, - 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, - 0x2e, 0x76, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, - 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x11, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x32, 0x44, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, - 0x3d, 0x0a, 0x0a, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x15, 0x2e, - 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x29, + 0x6f, 0x6e, 0x22, 0xc0, 0x01, 0x0a, 0x0f, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x41, 0x41, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x2d, 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, + 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x09, 0x63, 0x68, + 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, 0x18, 0x0a, + 0x07, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x52, 0x65, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x52, 0x65, + 0x63, 0x68, 0x65, 0x63, 0x6b, 0x22, 0x74, 0x0a, 0x0e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x41, + 0x41, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, + 0x65, 0x6d, 0x18, 0x01, 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, 0x07, + 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, + 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, + 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x93, 0x01, 0x0a, 0x02, + 0x56, 0x41, 0x12, 0x49, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x76, 0x61, 0x2e, 0x50, 0x65, 0x72, + 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12, 0x42, 0x0a, + 0x11, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, + 0x67, 0x65, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, + 0x00, 0x32, 0x7b, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x3d, 0x0a, 0x0a, 0x49, 0x73, 0x43, 0x41, + 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, + 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x08, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x43, 0x41, 0x41, 0x12, 0x13, 0x2e, 0x76, 0x61, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x41, + 0x41, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x76, 0x61, 0x2e, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x43, 0x41, 0x41, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x42, 0x29, 0x5a, 0x27, 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, 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, @@ -498,7 +663,7 @@ func file_va_proto_rawDescGZIP() []byte { return file_va_proto_rawDescData } -var file_va_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_va_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_va_proto_goTypes = []interface{}{ (*IsCAAValidRequest)(nil), // 0: va.IsCAAValidRequest (*IsCAAValidResponse)(nil), // 1: va.IsCAAValidResponse @@ -506,30 +671,37 @@ var file_va_proto_goTypes = []interface{}{ (*AuthzMeta)(nil), // 3: va.AuthzMeta (*ValidationResult)(nil), // 4: va.ValidationResult (*ValidationRequest)(nil), // 5: va.ValidationRequest - (*proto.ProblemDetails)(nil), // 6: core.ProblemDetails - (*proto.Challenge)(nil), // 7: core.Challenge - (*proto.ValidationRecord)(nil), // 8: core.ValidationRecord - (*proto.Identifier)(nil), // 9: core.Identifier + (*CheckCAARequest)(nil), // 6: va.CheckCAARequest + (*CheckCAAResult)(nil), // 7: va.CheckCAAResult + (*proto.ProblemDetails)(nil), // 8: core.ProblemDetails + (*proto.Challenge)(nil), // 9: core.Challenge + (*proto.ValidationRecord)(nil), // 10: core.ValidationRecord + (*proto.Identifier)(nil), // 11: core.Identifier } var file_va_proto_depIdxs = []int32{ - 6, // 0: va.IsCAAValidResponse.problem:type_name -> core.ProblemDetails - 7, // 1: va.PerformValidationRequest.challenge:type_name -> core.Challenge + 8, // 0: va.IsCAAValidResponse.problem:type_name -> core.ProblemDetails + 9, // 1: va.PerformValidationRequest.challenge:type_name -> core.Challenge 3, // 2: va.PerformValidationRequest.authz:type_name -> va.AuthzMeta - 8, // 3: va.ValidationResult.records:type_name -> core.ValidationRecord - 6, // 4: va.ValidationResult.problems:type_name -> core.ProblemDetails - 9, // 5: va.ValidationRequest.identifier:type_name -> core.Identifier - 7, // 6: va.ValidationRequest.challenge:type_name -> core.Challenge - 2, // 7: va.VA.PerformValidation:input_type -> va.PerformValidationRequest - 5, // 8: va.VA.ValidateChallenge:input_type -> va.ValidationRequest - 0, // 9: va.CAA.IsCAAValid:input_type -> va.IsCAAValidRequest - 4, // 10: va.VA.PerformValidation:output_type -> va.ValidationResult - 4, // 11: va.VA.ValidateChallenge:output_type -> va.ValidationResult - 1, // 12: va.CAA.IsCAAValid:output_type -> va.IsCAAValidResponse - 10, // [10:13] is the sub-list for method output_type - 7, // [7:10] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 10, // 3: va.ValidationResult.records:type_name -> core.ValidationRecord + 8, // 4: va.ValidationResult.problems:type_name -> core.ProblemDetails + 11, // 5: va.ValidationRequest.identifier:type_name -> core.Identifier + 9, // 6: va.ValidationRequest.challenge:type_name -> core.Challenge + 11, // 7: va.CheckCAARequest.identifier:type_name -> core.Identifier + 9, // 8: va.CheckCAARequest.challenge:type_name -> core.Challenge + 8, // 9: va.CheckCAAResult.problem:type_name -> core.ProblemDetails + 2, // 10: va.VA.PerformValidation:input_type -> va.PerformValidationRequest + 5, // 11: va.VA.ValidateChallenge:input_type -> va.ValidationRequest + 0, // 12: va.CAA.IsCAAValid:input_type -> va.IsCAAValidRequest + 6, // 13: va.CAA.CheckCAA:input_type -> va.CheckCAARequest + 4, // 14: va.VA.PerformValidation:output_type -> va.ValidationResult + 4, // 15: va.VA.ValidateChallenge:output_type -> va.ValidationResult + 1, // 16: va.CAA.IsCAAValid:output_type -> va.IsCAAValidResponse + 7, // 17: va.CAA.CheckCAA:output_type -> va.CheckCAAResult + 14, // [14:18] is the sub-list for method output_type + 10, // [10:14] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_va_proto_init() } @@ -610,6 +782,30 @@ func file_va_proto_init() { return nil } } + file_va_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckCAARequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_va_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckCAAResult); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -617,7 +813,7 @@ func file_va_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_va_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 8, NumExtensions: 0, NumServices: 2, }, diff --git a/va/proto/va.proto b/va/proto/va.proto index 9362ac6aa3b..888b5f3b95c 100644 --- a/va/proto/va.proto +++ b/va/proto/va.proto @@ -12,6 +12,7 @@ service VA { service CAA { rpc IsCAAValid(IsCAAValidRequest) returns (IsCAAValidResponse) {} + rpc CheckCAA(CheckCAARequest) returns (CheckCAAResult) {} } message IsCAAValidRequest { @@ -52,3 +53,17 @@ message ValidationRequest { string authzID = 4; string keyAuthorization = 5; } + +message CheckCAARequest { + core.Identifier identifier = 1; + core.Challenge challenge = 2; + int64 regID = 3; + string authzID = 4; + bool isRecheck = 5; +} + +message CheckCAAResult { + core.ProblemDetails problem = 1; + string perspective = 3; + string rir = 4; +} diff --git a/va/proto/va_grpc.pb.go b/va/proto/va_grpc.pb.go index 250ffa49657..bb6c5d3101d 100644 --- a/va/proto/va_grpc.pb.go +++ b/va/proto/va_grpc.pb.go @@ -149,6 +149,7 @@ var VA_ServiceDesc = grpc.ServiceDesc{ const ( CAA_IsCAAValid_FullMethodName = "/va.CAA/IsCAAValid" + CAA_CheckCAA_FullMethodName = "/va.CAA/CheckCAA" ) // CAAClient is the client API for CAA service. @@ -156,6 +157,7 @@ const ( // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type CAAClient interface { IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) + CheckCAA(ctx context.Context, in *CheckCAARequest, opts ...grpc.CallOption) (*CheckCAAResult, error) } type cAAClient struct { @@ -176,11 +178,22 @@ func (c *cAAClient) IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts return out, nil } +func (c *cAAClient) CheckCAA(ctx context.Context, in *CheckCAARequest, opts ...grpc.CallOption) (*CheckCAAResult, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckCAAResult) + err := c.cc.Invoke(ctx, CAA_CheckCAA_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // CAAServer is the server API for CAA service. // All implementations must embed UnimplementedCAAServer // for forward compatibility type CAAServer interface { IsCAAValid(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) + CheckCAA(context.Context, *CheckCAARequest) (*CheckCAAResult, error) mustEmbedUnimplementedCAAServer() } @@ -191,6 +204,9 @@ type UnimplementedCAAServer struct { func (UnimplementedCAAServer) IsCAAValid(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method IsCAAValid not implemented") } +func (UnimplementedCAAServer) CheckCAA(context.Context, *CheckCAARequest) (*CheckCAAResult, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckCAA not implemented") +} func (UnimplementedCAAServer) mustEmbedUnimplementedCAAServer() {} // UnsafeCAAServer may be embedded to opt out of forward compatibility for this service. @@ -222,6 +238,24 @@ func _CAA_IsCAAValid_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } +func _CAA_CheckCAA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckCAARequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CAAServer).CheckCAA(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CAA_CheckCAA_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CAAServer).CheckCAA(ctx, req.(*CheckCAARequest)) + } + return interceptor(ctx, in, info, handler) +} + // CAA_ServiceDesc is the grpc.ServiceDesc for CAA service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -233,6 +267,10 @@ var CAA_ServiceDesc = grpc.ServiceDesc{ MethodName: "IsCAAValid", Handler: _CAA_IsCAAValid_Handler, }, + { + MethodName: "CheckCAA", + Handler: _CAA_CheckCAA_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "va.proto", diff --git a/va/va_test.go b/va/va_test.go index 54d95af7106..35302336daa 100644 --- a/va/va_test.go +++ b/va/va_test.go @@ -213,6 +213,10 @@ func (v canceledVA) ValidateChallenge(_ context.Context, _ *vapb.ValidationReque return nil, context.Canceled } +func (v canceledVA) CheckCAA(_ context.Context, _ *vapb.CheckCAARequest, _ ...grpc.CallOption) (*vapb.CheckCAAResult, error) { + return nil, context.Canceled +} + // brokenRemoteVA is a mock for the VAClient and CAAClient interfaces that always return // errors. type brokenRemoteVA struct{} @@ -234,6 +238,10 @@ func (b brokenRemoteVA) ValidateChallenge(_ context.Context, _ *vapb.ValidationR return nil, errBrokenRemoteVA } +func (b brokenRemoteVA) CheckCAA(_ context.Context, _ *vapb.CheckCAARequest, _ ...grpc.CallOption) (*vapb.CheckCAAResult, error) { + return nil, errBrokenRemoteVA +} + // inMemVA is a wrapper which fulfills the VAClient and CAAClient // interfaces, but then forwards requests directly to its inner // ValidationAuthorityImpl rather than over the network. This lets a local @@ -254,6 +262,10 @@ func (inmem inMemVA) ValidateChallenge(_ context.Context, req *vapb.ValidationRe return inmem.rva.ValidateChallenge(ctx, req) } +func (inmem inMemVA) CheckCAA(_ context.Context, req *vapb.CheckCAARequest, _ ...grpc.CallOption) (*vapb.CheckCAAResult, error) { + return inmem.rva.CheckCAA(ctx, req) +} + func TestValidateMalformedChallenge(t *testing.T) { va, _ := setup(nil, 0, "", nil, nil) @@ -737,19 +749,37 @@ func TestLogRemoteDifferentials(t *testing.T) { } } +const ( + brokenDNS = "broken-dns" + hijackedDNS = "hijacked-dns" +) + +func dnsClientForUA(ua string, log blog.Logger) bdns.Client { + switch ua { + case brokenDNS: + return caaBrokenDNS{} + case hijackedDNS: + return caaHijackedDNS{} + default: + return &bdns.MockClient{Log: log} + } +} + // setup returns an in-memory VA and a mock logger. The default resolver client // is MockClient{}, but can be overridden. -func setupVA(srv *httptest.Server, ua string, rvas []RemoteVA, mockDNSClient bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) { +func setupVA(srv *httptest.Server, ua string, rvas []RemoteVA, dnsClientMock bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) { features.Reset() fc := clock.NewFake() mockLog := blog.NewMock() - if ua == "" { - ua = "user agent 1.0" + + var dnsClient bdns.Client + if dnsClientMock == nil { + dnsClient = dnsClientForUA(ua, mockLog) } va, err := NewValidationAuthorityImpl( - &bdns.MockClient{Log: mockLog}, + dnsClient, nil, 0, ua, @@ -762,10 +792,6 @@ func setupVA(srv *httptest.Server, ua string, rvas []RemoteVA, mockDNSClient bdn "ARIN", ) - if mockDNSClient != nil { - va.dnsClient = mockDNSClient - } - // Adjusting industry regulated ACME challenge port settings is fine during // testing if srv != nil { @@ -784,29 +810,19 @@ func setupVA(srv *httptest.Server, ua string, rvas []RemoteVA, mockDNSClient bdn } type rvaConf struct { - // rir is the Regional Internet Registry for the remote VA. rir string - - // ua if set to pass, the remote VA will always pass validation. If set to - // fail, the remote VA will always fail validation with probs.Unauthorized. - // This is set to pass by default. - ua string + ua string } // setupRVAs returns a slice of RemoteVA instances for testing. confs is a slice // of rir and user agent configurations for each RVA. mockDNSClient is optional, // it allows the DNS client to be overridden. srv is optional, it allows for a // test server to be specified. -func setupRVAs(confs []rvaConf, mockDNSClient bdns.Client, srv *httptest.Server) []RemoteVA { //nolint: unparam +func setupRVAs(confs []rvaConf, srv *httptest.Server) []RemoteVA { remoteVAs := make([]RemoteVA, 0, len(confs)) for i, c := range confs { - ua := "user agent 1.0" - if c.ua != "" { - ua = c.ua - } - // Configure the remote VA. - rva, _ := setupVA(srv, ua, nil, mockDNSClient) + rva, _ := setupVA(srv, c.ua, nil, nil) rva.perspective = fmt.Sprintf("dc-%d-%s", i, c.rir) rva.rir = c.rir @@ -839,8 +855,25 @@ func createValidationRequest(domain string, challengeType core.AcmeChallenge) *v } } +func createCheckCAARequest(domain string, challengeType core.AcmeChallenge, recheck bool) *vapb.CheckCAARequest { + return &vapb.CheckCAARequest{ + Identifier: &corepb.Identifier{ + Type: string(identifier.TypeDNS), + Value: domain, + }, + Challenge: &corepb.Challenge{ + Type: string(challengeType), + Status: string(core.StatusPending), + Token: expectedToken, + }, + RegID: 1, + AuthzID: "1", + IsRecheck: recheck, + } +} + func TestValidateChallengeInvalid(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil) va, mockLog := setupVA(nil, "", rvas, nil) req := createValidationRequest("foo.com", core.ChallengeTypeDNS01) @@ -861,7 +894,7 @@ func TestValidateChallengeInvalid(t *testing.T) { } func TestValidateChallengeInternalErrorLogged(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil) va, mockLog := setupVA(nil, "", rvas, nil) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) @@ -884,7 +917,7 @@ func TestValidateChallengeInternalErrorLogged(t *testing.T) { } func TestValidateChallengeValid(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil) va, mockLog := setupVA(nil, "", rvas, nil) req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) @@ -907,7 +940,7 @@ func TestValidateChallengeValid(t *testing.T) { // TestValidateChallengeWildcard tests that the VA properly strips the `*.` // prefix from a wildcard name provided to the ValidateChallenge function. func TestValidateChallengeWildcard(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil) va, mockLog := setupVA(nil, "", rvas, nil) req := createValidationRequest("*.good-dns01.com", core.ChallengeTypeDNS01) @@ -932,7 +965,7 @@ func TestValidateChallengeWildcard(t *testing.T) { } func TestValidateChallengeValidWithBrokenRVA(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}}, nil) brokenRVA := RemoteClients{VAClient: brokenRemoteVA{}, CAAClient: brokenRemoteVA{}} rvas = append(rvas, RemoteVA{brokenRVA, "broken"}) va, _ := setupVA(nil, "", rvas, nil) @@ -945,7 +978,7 @@ func TestValidateChallengeValidWithBrokenRVA(t *testing.T) { } func TestValidateChallengeValidWithCancelledRVA(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}}, nil) cancelledRVA := RemoteClients{VAClient: canceledVA{}, CAAClient: canceledVA{}} rvas = append(rvas, RemoteVA{cancelledRVA, "cancelled"}) va, _ := setupVA(nil, "", rvas, nil) @@ -958,7 +991,7 @@ func TestValidateChallengeValidWithCancelledRVA(t *testing.T) { } func TestValidateChallengeFailsWithTooManyBrokenRVAs(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}}, nil) brokenRVA := RemoteClients{VAClient: brokenRemoteVA{}, CAAClient: brokenRemoteVA{}} rvas = append(rvas, RemoteVA{brokenRVA, "broken"}, RemoteVA{brokenRVA, "broken"}) va, _ := setupVA(nil, "", rvas, nil) @@ -978,7 +1011,7 @@ func TestValidateChallengeFailsWithTooManyBrokenRVAs(t *testing.T) { } func TestValidateChallengeFailsWithTooManyCanceledRVAs(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}}, nil, nil) + rvas := setupRVAs([]rvaConf{{rir: "ARIN"}}, nil) canceledRVA := RemoteClients{VAClient: canceledVA{}, CAAClient: canceledVA{}} rvas = append(rvas, RemoteVA{canceledRVA, "canceled"}, RemoteVA{canceledRVA, "canceled"}) va, _ := setupVA(nil, "", rvas, nil) @@ -1019,6 +1052,7 @@ func parseMPICSummary(t *testing.T, log []string) mpicSummary { } func TestValidateChallengeMPIC(t *testing.T) { + t.Parallel() req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) // srv is used for the Primary VA and the Remote VAs. The srv.Server @@ -1112,7 +1146,7 @@ func TestValidateChallengeMPIC(t *testing.T) { { // If the primary passes and is configured with 6+ remote VAs which // return 3 or more failures, the validation will fail. - name: "VA: pass, rva1(ARIN): pass, rva2(APNIC): pass, rva3(ARIN): pass, rva4(ARIN): pass, rva5(ARIN): pass, rva6(ARIN): pass, rva7(ARIN): fail, rva8(ARIN): fail", + name: "VA: pass, rva1(ARIN): pass, rva2(APNIC): pass, rva3(ARIN): pass, rva4(ARIN): pass, rva5(ARIN): pass, rva6(ARIN): fail, rva7(ARIN): fail, rva8(ARIN): fail", primaryUA: pass, rvas: []rvaConf{ {"ARIN", pass}, {"APNIC", pass}, {"ARIN", pass}, {"ARIN", pass}, @@ -1127,7 +1161,7 @@ func TestValidateChallengeMPIC(t *testing.T) { // If the primary passes and is configured with 6+ remote VAs, then // the validation can succeed with up to 2 remote VA failures unless // one of the failed RVAs was the only one from a distinct RIR. - name: "VA: pass, rva1(ARIN): pass, rva2(APNIC): pass, rva3(ARIN): pass, rva4(ARIN): pass, rva5(ARIN): pass, rva6(ARIN): pass, rva7(ARIN): fail, rva8(ARIN): fail", + name: "VA: pass, rva1(ARIN): pass, rva2(APNIC): fail, rva3(ARIN): pass, rva4(ARIN): pass, rva5(ARIN): pass, rva6(ARIN): pass, rva7(ARIN): pass, rva8(ARIN): fail", primaryUA: pass, rvas: []rvaConf{ {"ARIN", pass}, {"APNIC", fail}, {"ARIN", pass}, {"ARIN", pass}, @@ -1142,7 +1176,7 @@ func TestValidateChallengeMPIC(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - rvas := setupRVAs(tc.rvas, nil, srv.Server) + rvas := setupRVAs(tc.rvas, srv.Server) primaryVA, mockLog := setupVA(srv.Server, tc.primaryUA, rvas, nil) res, err := primaryVA.ValidateChallenge(ctx, req) @@ -1167,3 +1201,151 @@ func TestValidateChallengeMPIC(t *testing.T) { }) } } + +func TestCheckCAAMPIC(t *testing.T) { + t.Parallel() + + req := createCheckCAARequest("localhost", core.ChallengeTypeHTTP01, false) + testCases := []struct { + name string + primaryUA string + rvas []rvaConf + isRecheck bool + expectedProbType probs.ProblemType + expectLogContains string + expectQuorumResult string + expectPassedRIRs []string + }{ + { + // If the primary and all remote VAs pass, the CAA check will + // succeed. + name: "VA: pass, remote1(ARIN): pass, remote2(RIPE): pass, remote3(APNIC): pass", + primaryUA: pass, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + expectedProbType: "", + expectLogContains: `Valid for issuance: true`, + expectQuorumResult: "3/3", + expectPassedRIRs: []string{"APNIC", "ARIN", "RIPE"}, + }, + { + // If the primary passes and just one remote VA fails, the CAA check + // will succeed. + name: "VA: pass, rva1(ARIN): pass, rva2(RIPE): pass, rva3(APNIC): brokenDNS", + primaryUA: pass, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", brokenDNS}}, + expectedProbType: "", + expectLogContains: `Valid for issuance: true`, + expectQuorumResult: "2/3", + expectPassedRIRs: []string{"ARIN", "RIPE"}, + }, + { + // If the primary passes and two remote VAs fail, the CAA check will + // fail. + name: "VA: pass, rva1(ARIN): pass, rva2(RIPE): brokenDNS, rva3(APNIC): brokenDNS", + primaryUA: pass, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", brokenDNS}, {"APNIC", brokenDNS}}, + expectedProbType: probs.DNSProblem, + expectLogContains: "During secondary CAA check: " + errCAABrokenDNSClient.Error(), + expectQuorumResult: "1/3", + expectPassedRIRs: []string{"ARIN"}, + }, + { + // If the primary fails, the remote VAs will not be queried, and the + // CAA check will fail. + name: "VA: brokenDNS, rva1(ARIN): pass, rva2(RIPE): pass, rva3(APNIC): pass", + primaryUA: brokenDNS, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + expectedProbType: probs.DNSProblem, + expectLogContains: errCAABrokenDNSClient.Error(), + expectQuorumResult: "", + expectPassedRIRs: nil, + }, + { + // If the primary passes and all of the passing RVAs are from the + // same RIR, the CAA check will fail and the error message will + // indicate the problem. + name: "VA: pass, rva1(ARIN): pass, rva2(ARIN): pass, rva3(APNIC): brokenDNS", + primaryUA: pass, + rvas: []rvaConf{{"ARIN", pass}, {"ARIN", pass}, {"APNIC", brokenDNS}}, + expectedProbType: probs.DNSProblem, + expectLogContains: errCAABrokenDNSClient.Error(), + expectQuorumResult: "2/3", + expectPassedRIRs: []string{"ARIN"}, + }, + { + // If the primary passes and is configured with 6+ remote VAs, then + // the CAA check can succeed with up to 2 remote VA failures and + // successes from at least 2 distinct RIRs. + name: "VA: pass, rva1(ARIN): pass, rva2(APNIC): pass, rva3(ARIN): pass, rva4(ARIN): pass, rva5(ARIN): pass, rva6(ARIN): pass, rva7(ARIN): brokenDNS, rva8(ARIN): brokenDNS", + primaryUA: pass, + rvas: []rvaConf{ + {"ARIN", pass}, {"APNIC", pass}, {"ARIN", pass}, {"ARIN", pass}, {"ARIN", brokenDNS}, {"ARIN", brokenDNS}, + }, + expectedProbType: "", + expectLogContains: `Valid for issuance: true`, + expectQuorumResult: "4/6", + expectPassedRIRs: []string{"APNIC", "ARIN"}, + }, + { + // If the primary passes and is configured with 6+ remote VAs which + // return 3 or more failures, the CAA check will fail. + name: "VA: pass, rva1(ARIN): pass, rva2(APNIC): pass, rva3(ARIN): pass, rva4(ARIN): pass, rva5(ARIN): pass, rva6(ARIN): brokenDNS, rva7(ARIN): brokenDNS, rva8(ARIN): brokenDNS", + primaryUA: pass, + rvas: []rvaConf{ + {"ARIN", pass}, {"APNIC", pass}, {"ARIN", pass}, {"ARIN", pass}, + {"ARIN", pass}, {"ARIN", brokenDNS}, {"ARIN", brokenDNS}, {"ARIN", brokenDNS}, + }, + expectedProbType: probs.DNSProblem, + expectLogContains: "During secondary CAA check: " + errCAABrokenDNSClient.Error(), + expectQuorumResult: "5/8", + expectPassedRIRs: []string{"APNIC", "ARIN"}, + }, + { + // If the primary passes and is configured with 6+ remote VAs, then + // the CAA check can succeed with up to 2 remote VA failures unless + // one of the failed RVAs was the only one from a distinct RIR. + name: "VA: pass, rva1(ARIN): pass, rva2(APNIC): brokenDNS, rva3(ARIN): pass, rva4(ARIN): pass, rva5(ARIN): pass, rva6(ARIN): pass, rva7(ARIN): fail, rva8(ARIN): brokenDNS", + primaryUA: pass, + rvas: []rvaConf{ + {"ARIN", pass}, {"APNIC", brokenDNS}, {"ARIN", pass}, {"ARIN", pass}, + {"ARIN", pass}, {"ARIN", pass}, {"ARIN", pass}, {"ARIN", brokenDNS}, + }, + expectedProbType: probs.DNSProblem, + expectLogContains: "During secondary CAA check: " + errCAABrokenDNSClient.Error(), + expectQuorumResult: "6/8", + expectPassedRIRs: []string{"ARIN"}, + }, + } + + for _, isRecheck := range []bool{false, true} { + for _, tc := range testCases { + tc.isRecheck = isRecheck + tc.name = fmt.Sprintf("%s, isRecheck: %v", tc.name, isRecheck) + t.Run(tc.name, func(t *testing.T) { + rvas := setupRVAs(tc.rvas, nil) + primaryVA, mockLog := setupVA(nil, tc.primaryUA, rvas, nil) + + res, err := primaryVA.CheckCAA(ctx, req) + test.AssertNotError(t, err, "These cases should only produce a probs, not errors") + + if tc.expectedProbType == "" { + // We expect validation to succeed. + test.Assert(t, res.Problem == nil, fmt.Sprintf("Unexpected CAA check failure: %#v", res.Problem)) + } else { + // We expect validation to fail. + test.AssertNotNil(t, res.Problem, "Expected CAA check failure but got success") + test.AssertEquals(t, string(tc.expectedProbType), res.Problem.ProblemType) + } + if tc.expectLogContains != "" { + test.AssertNotError(t, mockLog.ExpectMatch(tc.expectLogContains), "Expected log line not found") + } + got := parseMPICSummary(t, mockLog.GetAll()) + test.AssertDeepEquals(t, tc.expectQuorumResult, got.QuorumResult) + if tc.expectPassedRIRs != nil { + test.AssertDeepEquals(t, tc.expectPassedRIRs, got.RIRs) + } + }) + } + + } +} diff --git a/va/vampic.go b/va/vampic.go index e7e67d678ab..8539bca5291 100644 --- a/va/vampic.go +++ b/va/vampic.go @@ -7,6 +7,7 @@ import ( "maps" "math/rand/v2" "slices" + "sync" "time" "github.com/letsencrypt/boulder/core" @@ -19,6 +20,15 @@ import ( ) const ( + // requiredPerspectives is the minimum number of perspectives required to + // perform an MPIC-compliant validation. + // + // Timeline: + // - Mar 15, 2026: MUST implement using at least 3 perspectives + // - Jun 15, 2026: MUST implement using at least 4 perspectives + // - Dec 15, 2026: MUST implement using at least 5 perspectives + requiredPerspectives = 3 + PrimaryPerspective = "primary" challenge = "challenge" @@ -97,19 +107,6 @@ func prepareSummary(passed, failed []string, passedRIRs map[string]struct{}, rem return summary } -// validateChallengeAuditLog contains multiple fields that are exported for -// logging purposes. -type validateChallengeAuditLog struct { - AuthzID string `json:",omitempty"` - Requester int64 `json:",omitempty"` - Identifier string `json:",omitempty"` - Challenge core.Challenge `json:",omitempty"` - Error string `json:",omitempty"` - InternalError string `json:",omitempty"` - Latency float64 `json:",omitempty"` - MPICSummary mpicSummary -} - // determineMaxAllowedFailures returns the maximum number of allowed failures // for a given number of remote perspectives, according to the "Quorum // Requirements" table in BRs Section 3.2.2.9, as follows: @@ -133,11 +130,8 @@ func determineMaxAllowedFailures(perspectiveCount int) int { // validation results and a problem if the validation failed. The summary is // mandatory and must be returned even if the validation failed. func (va *ValidationAuthorityImpl) remoteValidateChallenge(ctx context.Context, req *vapb.ValidationRequest) (mpicSummary, *probs.ProblemDetails) { - // Mar 15, 2026: MUST implement using at least 3 perspectives - // Jun 15, 2026: MUST implement using at least 4 perspectives - // Dec 15, 2026: MUST implement using at least 5 perspectives remoteVACount := len(va.remoteVAs) - if remoteVACount < 3 { + if remoteVACount < requiredPerspectives { return mpicSummary{}, probs.ServerInternal("Insufficient remote perspectives: need at least 3") } @@ -232,6 +226,19 @@ func (va *ValidationAuthorityImpl) remoteValidateChallenge(ctx context.Context, return summary, nil } +// validateChallengeAuditLog contains multiple fields that are exported for +// logging purposes. +type validateChallengeAuditLog struct { + AuthzID string `json:",omitempty"` + Requester int64 `json:",omitempty"` + Identifier string `json:",omitempty"` + Challenge core.Challenge `json:",omitempty"` + Error string `json:",omitempty"` + InternalError string `json:",omitempty"` + Latency float64 `json:",omitempty"` + MPICSummary mpicSummary +} + // ValidateChallenge performs a local validation of a challenge using the // configured local VA. If the local validation passes, it will also perform an // MPIC-compliant validation of the challenge using the configured remote VAs. @@ -318,3 +325,240 @@ func (va *ValidationAuthorityImpl) ValidateChallenge(ctx context.Context, req *v return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) } + +// remoteValidateChallenge performs an MPIC-compliant remote validation of the +// challenge using the configured remote VAs. It returns a summary of the +// validation results and a problem if the validation failed. The summary is +// mandatory and must be returned even if the validation failed. +func (va *ValidationAuthorityImpl) remoteCheckCAA(ctx context.Context, req *vapb.CheckCAARequest) (mpicSummary, *probs.ProblemDetails) { + remoteVACount := len(va.remoteVAs) + if remoteVACount < requiredPerspectives { + return mpicSummary{}, probs.ServerInternal("Insufficient remote perspectives: need at least 3") + } + + type response struct { + addr string + result *vapb.CheckCAAResult + err error + } + + responses := make(chan *response, remoteVACount) + for _, i := range rand.Perm(remoteVACount) { + go func(rva RemoteVA) { + res, err := rva.CheckCAA(ctx, req) + responses <- &response{rva.Address, res, err} + }(va.remoteVAs[i]) + } + + var passed []string + var failed []string + passedRIRs := make(map[string]struct{}) + + var firstProb *probs.ProblemDetails + for i := 0; i < remoteVACount; i++ { + resp := <-responses + + var currProb *probs.ProblemDetails + if resp.err != nil { + // Failed to communicate with the remote VA. + failed = append(failed, resp.addr) + if errors.Is(resp.err, context.Canceled) { + currProb = probs.ServerInternal("Secondary CAA check RPC canceled") + } else { + va.log.Errf("Remote VA %q.CheckCAA failed: %s", resp.addr, resp.err) + currProb = probs.ServerInternal("Secondary CAA check RPC failed") + } + + } else if resp.result.Problem != nil { + // The remote VA returned a problem. + failed = append(failed, resp.result.Perspective) + + var err error + currProb, err = bgrpc.PBToProblemDetails(resp.result.Problem) + if err != nil { + va.log.Errf("Remote VA %q.CheckCAA returned a malformed problem: %s", resp.addr, err) + currProb = probs.ServerInternal("Secondary CAA check RPC returned malformed result") + } + + } else { + // The remote VA returned a successful result. + passed = append(passed, resp.result.Perspective) + passedRIRs[resp.result.Rir] = struct{}{} + } + + if firstProb == nil && currProb != nil { + // A problem was encountered for the first time. + firstProb = currProb + } + } + + // Prepare the summary, this MUST be returned even if the check failed. + summary := prepareSummary(passed, failed, passedRIRs, remoteVACount) + + maxRemoteFailures := determineMaxAllowedFailures(remoteVACount) + if len(failed) > maxRemoteFailures { + // Too many failures to reach quorum. + if firstProb != nil { + firstProb.Detail = fmt.Sprintf("During secondary CAA check: %s", firstProb.Detail) + return summary, firstProb + } + return summary, probs.ServerInternal("Secondary CAA check failed due to too many failures") + } + + if len(passed) < (remoteVACount - maxRemoteFailures) { + // Too few successful responses to reach quorum. + if firstProb != nil { + firstProb.Detail = fmt.Sprintf("During secondary CAA check: %s", firstProb.Detail) + return summary, firstProb + } + return summary, probs.ServerInternal("Secondary CAA check failed due to insufficient successful responses") + } + + if len(passedRIRs) < 2 { + // Too few successful responses from distinct RIRs to reach quorum. + if firstProb != nil { + firstProb.Detail = fmt.Sprintf("During secondary CAA check: %s", firstProb.Detail) + return summary, firstProb + } + return summary, probs.Unauthorized("Secondary CAA check failed to receive enough corroborations from distinct RIRs") + } + + // Enough successful responses from distinct RIRs to reach quorum. + return summary, nil +} + +type checkCAAAuditLog struct { + AuthzID string `json:",omitempty"` + Requester int64 `json:",omitempty"` + Identifier string `json:",omitempty"` + ValidationMethod string `json:",omitempty"` + Error string `json:",omitempty"` + InternalError string `json:",omitempty"` + Latency float64 `json:",omitempty"` + MPICSummary mpicSummary +} + +func prepareCAACheckResult(prob *probs.ProblemDetails, perspective, rir string) (*vapb.CheckCAAResult, error) { + pbProb, err := bgrpc.ProblemDetailsToPB(prob) + if err != nil { + return &vapb.CheckCAAResult{}, errors.New("failed to serialize problem") + } + return &vapb.CheckCAAResult{Problem: pbProb, Perspective: perspective, Rir: rir}, nil +} + +// CheckCAA performs a local CAA check using the configured local VA. If the +// local CAA check passes, it will also perform an MPIC-compliant CAA check +// using the configured remote VAs. +// +// Note: This method calls itself recursively to perform remote caa checks. +func (va *ValidationAuthorityImpl) CheckCAA(ctx context.Context, req *vapb.CheckCAARequest) (*vapb.CheckCAAResult, error) { + if core.IsAnyNilOrZero(req, req.Identifier, req.Challenge, req.RegID, req.AuthzID) { + return nil, berrors.InternalServerError("Incomplete CAA check request") + } + + acmeIdent := identifier.NewDNS(req.Identifier.Value) + chall, err := bgrpc.PBToChallenge(req.Challenge) + if err != nil { + return nil, fmt.Errorf("challenge failed to deserialize %s", err) + } + + auditLog := checkCAAAuditLog{ + AuthzID: req.AuthzID, + Requester: req.RegID, + Identifier: req.Identifier.Value, + ValidationMethod: string(chall.Type), + } + + var prob *probs.ProblemDetails + var localLatency time.Duration + var summary = newSummary() + start := va.clk.Now() + + defer func() { + probType := "" + outcome := fail + if prob != nil { + // CAA check failed. + probType = string(prob.Type) + auditLog.Error = prob.Error() + } else { + // CAA check passed. + outcome = pass + } + // Observe local check latency (primary|remote). + va.observeLatency(caa, va.perspective, string(chall.Type), probType, outcome, localLatency) + if va.isPrimaryVA() { + // Observe total check latency (primary+remote). + va.observeLatency(caa, all, string(chall.Type), probType, outcome, va.clk.Since(start)) + auditLog.MPICSummary = summary + } + // Log the total check latency. + auditLog.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds() + va.log.AuditObject("CAA check result", auditLog) + }() + + var localErr error + + if req.IsRecheck && va.isPrimaryVA() { + // Perform local and remote checks in parallel. + var localWG sync.WaitGroup + var remoteWG sync.WaitGroup + localWG.Add(1) + remoteWG.Add(1) + + var remoteProb *probs.ProblemDetails + var remoteSummary mpicSummary + + go func() { + defer localWG.Done() + localErr = va.checkCAA(ctx, acmeIdent, &caaParams{req.RegID, chall.Type}) + }() + + go func() { + defer remoteWG.Done() + remoteSummary, remoteProb = va.remoteCheckCAA(ctx, req) + }() + + // Wait for local check to complete. + localWG.Wait() + + // Stop the clock for local check latency. + localLatency = va.clk.Since(start) + + // Wait for remote check to complete. + remoteWG.Wait() + + if localErr != nil { + // Local check failed. + auditLog.InternalError = localErr.Error() + prob = detailedError(localErr) + return prepareCAACheckResult(prob, va.perspective, va.rir) + } + summary = remoteSummary + if remoteProb != nil { + // Remote check failed. + prob = remoteProb + } + + } else { + // Perform local check. + localErr = va.checkCAA(ctx, acmeIdent, &caaParams{req.RegID, chall.Type}) + + // Stop the clock for local check latency. + localLatency = va.clk.Since(start) + + if localErr != nil { + // Local check failed. + auditLog.InternalError = localErr.Error() + prob = detailedError(localErr) + return prepareCAACheckResult(prob, va.perspective, va.rir) + } + + if va.isPrimaryVA() { + // Attempt to check CAA remotely. + summary, prob = va.remoteCheckCAA(ctx, req) + } + } + + return prepareCAACheckResult(prob, va.perspective, va.rir) +}