diff --git a/va/proto/va.pb.go b/va/proto/va.pb.go index 7ab4220452e..f187b853c5e 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"` + ChallengeType string `protobuf:"bytes,2,opt,name=challengeType,proto3" json:"challengeType,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) GetChallengeType() string { + if x != nil { + return x.ChallengeType + } + return "" +} + +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,7 +608,26 @@ 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, + 0x6f, 0x6e, 0x22, 0xb7, 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, 0x24, 0x0a, 0x0d, 0x63, 0x68, 0x61, 0x6c, + 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 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, @@ -475,15 +636,18 @@ var file_va_proto_rawDesc = []byte{ 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, + 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, 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, - 0x33, + 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, 0x33, } var ( @@ -498,7 +662,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 +670,36 @@ 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 + 8, // 8: va.CheckCAAResult.problem:type_name -> core.ProblemDetails + 2, // 9: va.VA.PerformValidation:input_type -> va.PerformValidationRequest + 5, // 10: va.VA.ValidateChallenge:input_type -> va.ValidationRequest + 0, // 11: va.CAA.IsCAAValid:input_type -> va.IsCAAValidRequest + 6, // 12: va.CAA.CheckCAA:input_type -> va.CheckCAARequest + 4, // 13: va.VA.PerformValidation:output_type -> va.ValidationResult + 4, // 14: va.VA.ValidateChallenge:output_type -> va.ValidationResult + 1, // 15: va.CAA.IsCAAValid:output_type -> va.IsCAAValidResponse + 7, // 16: va.CAA.CheckCAA:output_type -> va.CheckCAAResult + 13, // [13:17] is the sub-list for method output_type + 9, // [9:13] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_va_proto_init() } @@ -610,6 +780,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 +811,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..54dc1ab1c0f 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; + string challengeType = 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..c118dd0d7c8 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,39 @@ 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) { //nolint: unparam features.Reset() fc := clock.NewFake() mockLog := blog.NewMock() - if ua == "" { - ua = "user agent 1.0" + + var dnsClient bdns.Client + if dnsClientMock != nil { + dnsClient = dnsClientMock + } else { + dnsClient = dnsClientForUA(ua, mockLog) } va, err := NewValidationAuthorityImpl( - &bdns.MockClient{Log: mockLog}, + dnsClient, nil, 0, ua, @@ -762,10 +794,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 +812,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,197 +857,258 @@ func createValidationRequest(domain string, challengeType core.AcmeChallenge) *v } } -func TestValidateChallengeInvalid(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil, nil) - va, mockLog := setupVA(nil, "", rvas, nil) - - req := createValidationRequest("foo.com", core.ChallengeTypeDNS01) - - res, err := va.ValidateChallenge(context.Background(), req) - test.AssertNotError(t, err, "ValidateChallenge failed, expected success") - test.Assert(t, res.Problems != nil, "validation succeeded, expected failure") - resultLog := mockLog.GetAllMatching(`Challenge validation result`) - test.AssertNotNil(t, resultLog, "ValidateChallenge didn't log validation result.") - test.AssertContains(t, resultLog[0], `"Identifier":"foo.com"`) - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": "challenge", - "perspective": "primary", - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": string(probs.UnauthorizedProblem), - "result": fail, - }, 1) -} - -func TestValidateChallengeInternalErrorLogged(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil, nil) - va, mockLog := setupVA(nil, "", rvas, nil) - - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) - defer cancel() - - req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01) - - _, err := va.ValidateChallenge(ctx, req) - test.AssertNotError(t, err, "Failed validation should be a prob but not an error") - resultLog := mockLog.GetAllMatching( - `Challenge validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`) - test.AssertEquals(t, len(resultLog), 1) - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": challenge, - "perspective": PrimaryPerspective, - "challenge_type": string(core.ChallengeTypeHTTP01), - "problem_type": string(probs.ConnectionProblem), - "result": fail, - }, 1) -} - -func TestValidateChallengeValid(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}, {rir: "APNIC"}}, nil, nil) - va, mockLog := setupVA(nil, "", rvas, nil) - - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - - res, err := va.ValidateChallenge(context.Background(), req) - test.AssertNotError(t, err, "validating challenge resulted in unexpected error") - test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed: %#v", res.Problems)) - resultLog := mockLog.GetAllMatching(`Challenge validation result`) - test.AssertNotNil(t, resultLog, "ValidateChallenge didn't log validation result.") - test.AssertContains(t, resultLog[0], `"Identifier":"good-dns01.com"`) - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": challenge, - "perspective": PrimaryPerspective, - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": "", - "result": pass, - }, 1) -} - -// 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) - va, mockLog := setupVA(nil, "", rvas, nil) - - req := createValidationRequest("*.good-dns01.com", core.ChallengeTypeDNS01) - - res, _ := va.ValidateChallenge(context.Background(), req) - test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed: %#v", res.Problems)) - resultLog := mockLog.GetAllMatching(`Challenge validation result`) - test.AssertNotNil(t, resultLog, "ValidateChallenge didn't log validation result.") - - // The top level Identifier will reflect the wildcard name. - test.AssertContains(t, resultLog[0], `"Identifier":"*.good-dns01.com"`) - - // The ValidationRecord will contain the non-wildcard name. - test.AssertContains(t, resultLog[0], `"hostname":"good-dns01.com"`) - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": challenge, - "perspective": PrimaryPerspective, - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": "", - "result": pass, - }, 1) +// parseValidationAuditLog extracts ... from JSON={ ... } in a +// ValidateChallenge or CheckCAA log and returns it as an auditLog struct. +func parseValidationAuditLog(t *testing.T, log []string) validateChallengeAuditLog { + re := regexp.MustCompile(`JSON=\{.*\}`) + var audit validateChallengeAuditLog + for _, line := range log { + match := re.FindString(line) + if match != "" { + jsonStr := match[len(`JSON=`):] + if err := json.Unmarshal([]byte(jsonStr), &audit); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + return audit + } + } + t.Fatal("JSON not found in log") + return audit } -func TestValidateChallengeValidWithBrokenRVA(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}}, nil, nil) - brokenRVA := RemoteClients{VAClient: brokenRemoteVA{}, CAAClient: brokenRemoteVA{}} - rvas = append(rvas, RemoteVA{brokenRVA, "broken"}) - va, _ := setupVA(nil, "", rvas, nil) +func TestValidateChallenge(t *testing.T) { + t.Parallel() - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) + brokenRVA := RemoteVA{RemoteClients{VAClient: brokenRemoteVA{}}, "broken"} + canceledRVA := RemoteVA{RemoteClients{VAClient: canceledVA{}}, "canceled"} - res, err := va.ValidateChallenge(context.Background(), req) - test.AssertNotError(t, err, "validating challenge resulted in unexpected error") - test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed: %#v", res.Problems)) -} - -func TestValidateChallengeValidWithCancelledRVA(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}, {rir: "RIPE"}}, nil, nil) - cancelledRVA := RemoteClients{VAClient: canceledVA{}, CAAClient: canceledVA{}} - rvas = append(rvas, RemoteVA{cancelledRVA, "cancelled"}) - va, _ := setupVA(nil, "", rvas, nil) + testCases := []struct { + name string + identifier string + challengeType core.AcmeChallenge + rvas []rvaConf + appendRemoteVAs []RemoteVA + primaryUA string + ctxTimeout time.Duration + expectError bool + expectProbType probs.ProblemType + expectProbContains string + expectLogIdentifier string + expectValidationRecordDnsName string + expectedMetricLabels prometheus.Labels + }{ + { + name: "UnauthorizedProblem", + identifier: "foo.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + primaryUA: pass, + expectError: false, + expectProbType: probs.UnauthorizedProblem, + expectProbContains: "", + expectLogIdentifier: "foo.com", + expectValidationRecordDnsName: "", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.UnauthorizedProblem), + "result": fail, + }, + }, + { + name: "ConnectionProblem", + identifier: "nonexistent.com", + challengeType: core.ChallengeTypeHTTP01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + primaryUA: pass, + ctxTimeout: 1 * time.Millisecond, + expectError: false, + expectProbType: probs.ConnectionProblem, + expectProbContains: "Timeout after connect", + expectLogIdentifier: "nonexistent.com", + expectValidationRecordDnsName: "", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeHTTP01), + "problem_type": string(probs.ConnectionProblem), + "result": fail, + }, + }, + { + name: "Valid", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "good-dns01.com", + expectValidationRecordDnsName: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "ValidWithWildcard", + identifier: "*.good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "*.good-dns01.com", + expectValidationRecordDnsName: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "ValidWithBrokenRVA", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}}, + appendRemoteVAs: []RemoteVA{brokenRVA}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "good-dns01.com", + expectValidationRecordDnsName: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "ValidWithCanceledRVA", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}}, + appendRemoteVAs: []RemoteVA{canceledRVA}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "good-dns01.com", + expectValidationRecordDnsName: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "InvalidWithTooManyBrokenRVAs", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}}, + appendRemoteVAs: []RemoteVA{brokenRVA, brokenRVA}, + primaryUA: pass, + expectError: false, + expectProbType: probs.ServerInternalProblem, + expectProbContains: "During secondary domain validation: Secondary domain validation RPC failed", + expectLogIdentifier: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.ServerInternalProblem), + "result": fail, + }, + }, + { + name: "InvalidWithTooManyCanceledRVAs", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}}, + appendRemoteVAs: []RemoteVA{canceledRVA, canceledRVA}, + primaryUA: pass, + expectError: false, + expectProbType: probs.ServerInternalProblem, + expectProbContains: "During secondary domain validation: Secondary domain validation RPC canceled", + expectLogIdentifier: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opChallenge, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.ServerInternalProblem), + "result": fail, + }, + }, + } - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - res, err := va.ValidateChallenge(context.Background(), req) - test.AssertNotError(t, err, "validating challenge resulted in unexpected error") - test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed: %#v", res.Problems)) -} + srv := httpMultiSrv(t, expectedToken, map[string]bool{"pass": true, "fail": false}) + defer srv.Close() -func TestValidateChallengeFailsWithTooManyBrokenRVAs(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}}, nil, nil) - brokenRVA := RemoteClients{VAClient: brokenRemoteVA{}, CAAClient: brokenRemoteVA{}} - rvas = append(rvas, RemoteVA{brokenRVA, "broken"}, RemoteVA{brokenRVA, "broken"}) - va, _ := setupVA(nil, "", rvas, nil) - - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - - res, err := va.ValidateChallenge(context.Background(), req) - test.AssertNotError(t, err, "Failed validation should be a prob but not an error") - test.AssertContains(t, res.Problems.Detail, "During secondary domain validation: Secondary domain validation RPC failed") - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": challenge, - "perspective": PrimaryPerspective, - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": string(probs.ServerInternalProblem), - "result": fail, - }, 1) -} + rvas := setupRVAs(tc.rvas, srv.Server) + if len(tc.appendRemoteVAs) > 0 { + rvas = append(rvas, tc.appendRemoteVAs...) + } + va, mockLog := setupVA(srv.Server, tc.primaryUA, rvas, nil) + req := createValidationRequest(tc.identifier, tc.challengeType) + + ctx := context.Background() + if tc.ctxTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), tc.ctxTimeout) + defer cancel() + } -func TestValidateChallengeFailsWithTooManyCanceledRVAs(t *testing.T) { - rvas := setupRVAs([]rvaConf{{rir: "ARIN"}}, nil, nil) - canceledRVA := RemoteClients{VAClient: canceledVA{}, CAAClient: canceledVA{}} - rvas = append(rvas, RemoteVA{canceledRVA, "canceled"}, RemoteVA{canceledRVA, "canceled"}) - va, _ := setupVA(nil, "", rvas, nil) - - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - - res, err := va.ValidateChallenge(context.Background(), req) - test.AssertNotError(t, err, "Failed validation should be a prob but not an error") - test.AssertContains(t, res.Problems.Detail, "During secondary domain validation: Secondary domain validation RPC canceled") - test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ - "operation": challenge, - "perspective": PrimaryPerspective, - "challenge_type": string(core.ChallengeTypeDNS01), - "problem_type": string(probs.ServerInternalProblem), - "result": fail, - }, 1) -} + res, err := va.ValidateChallenge(ctx, req) + if tc.expectError { + test.AssertError(t, err, "Expected an error but got none") + } else { + test.AssertNotError(t, err, "ValidateChallenge failed, expected success") + } -// parseMPICSummary extracts ... from "MPICSummary":{ ... } in a -// ValidateChallenge log and returns it as an mpicSummary struct. -func parseMPICSummary(t *testing.T, log []string) mpicSummary { - re := regexp.MustCompile(`"MPICSummary":\{.*\}`) + resultLog := mockLog.GetAllMatching("Challenge validation result") + test.AssertNotNil(t, resultLog, "ValidateChallenge didn't log validation result.") + auditLog := parseValidationAuditLog(t, resultLog) + if tc.expectLogIdentifier != "" { + test.AssertEquals(t, tc.expectLogIdentifier, auditLog.Identifier) + } + if tc.expectValidationRecordDnsName != "" { + if auditLog.Challenge.ValidationRecord == nil { + t.Fatalf("Expected ValidationRecord in audit log but got none") + } + test.AssertEquals(t, tc.expectValidationRecordDnsName, auditLog.Challenge.ValidationRecord[0].DnsName) + } - var summary mpicSummary - for _, line := range log { - match := re.FindString(line) - if match != "" { - jsonStr := strings.TrimSuffix(match[len(`"MPICSummary":`):], "}") - if err := json.Unmarshal([]byte(jsonStr), &summary); err != nil { - t.Fatalf("Failed to parse MPICSummary: %v", err) + if tc.expectProbType != "" { + test.Assert(t, res.Problems != nil, "validation succeeded, expected failure") + test.AssertEquals(t, string(tc.expectProbType), res.Problems.ProblemType) + if tc.expectProbContains != "" { + test.AssertContains(t, res.Problems.Detail, tc.expectProbContains) + } + } else { + test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed unexpectedly: %#v", res.Problems)) } - return summary - } + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedMetricLabels, 1) + }) } - - t.Fatal("MPICSummary JSON not found in log") - return summary } func TestValidateChallengeMPIC(t *testing.T) { - req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) + t.Parallel() - // srv is used for the Primary VA and the Remote VAs. The srv.Server - // produced will be used to mock the challenge recipient. When a VA (primary - // or remote) with a user-agent (UA) of "pass" attempt to validate a - // challenge, it will succeed. When a VA with a UA of "fail" attempts to - // validate a challenge it will fail with probs.Unauthorized. By controlling - // which VA or Remote VA(s) are configured with which UA, we can control the - // conditions of each case. - srv := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) - defer srv.Close() + req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) testCases := []struct { name string @@ -1112,7 +1191,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 +1206,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 +1221,15 @@ func TestValidateChallengeMPIC(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - rvas := setupRVAs(tc.rvas, nil, srv.Server) + t.Parallel() + + // srv mocks the challenge recipient for the Primary and Remote VAs. + // A VA/RVA with a user-agent (UA) of pass succeeds, while fail + // triggers a failure with probs.Unauthorized. + srv := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) + defer srv.Close() + + rvas := setupRVAs(tc.rvas, srv.Server) primaryVA, mockLog := setupVA(srv.Server, tc.primaryUA, rvas, nil) res, err := primaryVA.ValidateChallenge(ctx, req) @@ -1159,11 +1246,425 @@ func TestValidateChallengeMPIC(t *testing.T) { 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) + got := parseValidationAuditLog(t, mockLog.GetAll()) + test.AssertDeepEquals(t, tc.expectQuorumResult, got.MPICSummary.QuorumResult) if tc.expectPassedRIRs != nil { - test.AssertDeepEquals(t, tc.expectPassedRIRs, got.RIRs) + test.AssertDeepEquals(t, tc.expectPassedRIRs, got.MPICSummary.RIRs) } }) } } + +func createCheckCAARequest(domain string, challengeType core.AcmeChallenge, recheck bool) *vapb.CheckCAARequest { + return &vapb.CheckCAARequest{ + Identifier: &corepb.Identifier{ + Type: string(identifier.TypeDNS), + Value: domain, + }, + ChallengeType: string(challengeType), + RegID: 1, + AuthzID: "1", + IsRecheck: recheck, + } +} + +// parseValidationAuditLog extracts ... from JSON={ ... } in a +// ValidateChallenge or CheckCAA log and returns it as an auditLog struct. +func parseCheckCAAAuditLog(t *testing.T, log []string) checkCAAAuditLog { + re := regexp.MustCompile(`JSON=\{.*\}`) + var audit checkCAAAuditLog + for _, line := range log { + match := re.FindString(line) + if match != "" { + jsonStr := match[len(`JSON=`):] + if err := json.Unmarshal([]byte(jsonStr), &audit); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + return audit + } + } + t.Fatal("JSON not found in log") + return audit +} + +func TestCheckCAA(t *testing.T) { + t.Parallel() + + brokenRVA := RemoteVA{RemoteClients{CAAClient: brokenRemoteVA{}}, "broken"} + canceledRVA := RemoteVA{RemoteClients{CAAClient: canceledVA{}}, "canceled"} + + testCases := []struct { + name string + identifier string + challengeType core.AcmeChallenge + rvas []rvaConf + appendRemoteVAs []RemoteVA + primaryUA string + expectError bool + expectProbType probs.ProblemType + expectProbContains string + expectLogIdentifier string + expectedMetricLabels prometheus.Labels + }{ + { + name: "DNSProblem", + identifier: "present.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + primaryUA: brokenDNS, + expectError: false, + expectProbType: probs.DNSProblem, + expectProbContains: "", + expectLogIdentifier: "present.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.DNSProblem), + "result": fail, + }, + }, + { + name: "Valid", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "ValidWithBrokenRVA", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}}, + appendRemoteVAs: []RemoteVA{brokenRVA}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "ValidWithCanceledRVA", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}}, + appendRemoteVAs: []RemoteVA{canceledRVA}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "InvalidWithTooManyBrokenRVAs", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}}, + appendRemoteVAs: []RemoteVA{brokenRVA, brokenRVA}, + primaryUA: pass, + expectError: false, + expectProbType: probs.ServerInternalProblem, + expectProbContains: "During secondary CAA check: Secondary CAA check RPC failed", + expectLogIdentifier: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.ServerInternalProblem), + "result": fail, + }, + }, + { + name: "InvalidWithTooManyCanceledRVAs", + identifier: "good-dns01.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}}, + appendRemoteVAs: []RemoteVA{canceledRVA, canceledRVA}, + primaryUA: opCAA, + expectError: false, + expectProbType: probs.ServerInternalProblem, + expectProbContains: "During secondary CAA check: Secondary CAA check RPC canceled", + expectLogIdentifier: "good-dns01.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.ServerInternalProblem), + "result": fail, + }, + }, + { + name: "ValidWithOneConflictingRVA", + identifier: "present.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", hijackedDNS}}, + primaryUA: pass, + expectError: false, + expectProbType: "", + expectLogIdentifier: "present.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "RemoteCAAProblem", + identifier: "present.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", hijackedDNS}, {"APNIC", hijackedDNS}}, + primaryUA: pass, + expectError: false, + expectProbType: probs.CAAProblem, + expectProbContains: "During secondary CAA check: CAA record for present.com prevents issuance", + expectLogIdentifier: "present.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.CAAProblem), + "result": fail, + }, + }, + { + name: "LocalCAAProblem", + identifier: "present.com", + challengeType: core.ChallengeTypeDNS01, + rvas: []rvaConf{{"ARIN", pass}, {"RIPE", pass}, {"APNIC", pass}}, + primaryUA: hijackedDNS, + expectError: false, + expectProbType: probs.CAAProblem, + expectProbContains: "CAA record for present.com prevents issuance", + expectLogIdentifier: "present.com", + expectedMetricLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": PrimaryPerspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.CAAProblem), + "result": fail, + }, + }, + } + + for _, tc := range testCases { + for _, isRecheck := range []bool{false, true} { + if isRecheck { + tc.name = tc.name + "Recheck" + } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + srv := httpMultiSrv(t, expectedToken, map[string]bool{"pass": true, "fail": false}) + defer srv.Close() + + rvas := setupRVAs(tc.rvas, srv.Server) + if len(tc.appendRemoteVAs) > 0 { + rvas = append(rvas, tc.appendRemoteVAs...) + } + va, mockLog := setupVA(srv.Server, tc.primaryUA, rvas, nil) + req := createCheckCAARequest(tc.identifier, tc.challengeType, isRecheck) + + res, err := va.CheckCAA(ctx, req) + if tc.expectError { + test.AssertError(t, err, "Expected CheckCAA to error but got none") + } else { + test.AssertNotError(t, err, "CheckCAA failed, expected success") + } + + resultLog := mockLog.GetAllMatching("CAA check result") + test.AssertNotNil(t, resultLog, "CheckCAA didn't log check result.") + auditLog := parseCheckCAAAuditLog(t, resultLog) + if tc.expectLogIdentifier != "" { + test.AssertEquals(t, tc.expectLogIdentifier, auditLog.Identifier) + } + + if tc.expectProbType != "" { + fmt.Printf("\nauditLog:\n %#v\n", auditLog) + test.Assert(t, res.Problem != nil, "CheckCAA succeeded, expected failure") + test.AssertEquals(t, string(tc.expectProbType), res.Problem.ProblemType) + if tc.expectProbContains != "" { + test.AssertContains(t, res.Problem.Detail, tc.expectProbContains) + } + } else { + test.Assert(t, res.Problem == nil, fmt.Sprintf("CheckCAA failed unexpectedly: %#v", res.Problem)) + } + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedMetricLabels, 1) + }) + } + } +} + +func TestCheckCAAMPIC(t *testing.T) { + t.Parallel() + + req := createCheckCAARequest("localhost", core.ChallengeTypeHTTP01, false) + testCases := []struct { + name string + primaryUA string + rvas []rvaConf + 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): pass, 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 _, tc := range testCases { + for _, isRecheck := range []bool{false, true} { + req.IsRecheck = isRecheck + if isRecheck { + tc.name = tc.name + "Recheck" + } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + 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 := parseCheckCAAAuditLog(t, mockLog.GetAll()) + test.AssertDeepEquals(t, tc.expectQuorumResult, got.MPICSummary.QuorumResult) + if tc.expectPassedRIRs != nil { + test.AssertDeepEquals(t, tc.expectPassedRIRs, got.MPICSummary.RIRs) + } + }) + } + + } +} diff --git a/va/vampic.go b/va/vampic.go index e7e67d678ab..aadfe665f16 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,13 +20,23 @@ 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" + all = "all" + + opChallenge = "challenge" + opCAA = "caa" - challenge = "challenge" - caa = "caa" - all = "all" - pass = "pass" - fail = "fail" + pass = "pass" + fail = "fail" ) // observeLatency records entries in the validationLatency histogram of the @@ -133,11 +144,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") } @@ -279,10 +287,10 @@ func (va *ValidationAuthorityImpl) ValidateChallenge(ctx context.Context, req *v auditLog.Challenge.Status = core.StatusValid } // Observe local validation latency (primary|remote). - va.observeLatency(challenge, va.perspective, string(chall.Type), probType, outcome, localLatency) + va.observeLatency(opChallenge, va.perspective, string(chall.Type), probType, outcome, localLatency) if va.isPrimaryVA() { // Observe total validation latency (primary+remote). - va.observeLatency(challenge, all, string(chall.Type), probType, outcome, va.clk.Since(start)) + va.observeLatency(opChallenge, all, string(chall.Type), probType, outcome, va.clk.Since(start)) auditLog.MPICSummary = summary } // Log the total validation latency. @@ -318,3 +326,243 @@ 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 +} + +// checkCAAAuditLog contains multiple fields that are exported for logging +// purposes. +type checkCAAAuditLog struct { + AuthzID string `json:",omitempty"` + Requester int64 `json:",omitempty"` + Identifier string `json:",omitempty"` + ChallengeType core.AcmeChallenge `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.ChallengeType, req.RegID, req.AuthzID) { + return nil, berrors.InternalServerError("Incomplete CAA check request") + } + + acmeIdent := identifier.NewDNS(req.Identifier.Value) + challType := core.AcmeChallenge(req.ChallengeType) + if !challType.IsValid() { + return nil, berrors.InternalServerError("Invalid challenge type") + } + + auditLog := checkCAAAuditLog{ + AuthzID: req.AuthzID, + Requester: req.RegID, + Identifier: req.Identifier.Value, + ChallengeType: challType, + } + + 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(opCAA, va.perspective, string(challType), probType, outcome, localLatency) + if va.isPrimaryVA() { + // Observe total check latency (primary+remote). + va.observeLatency(opCAA, all, string(challType), 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, challType}) + }() + + 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, challType}) + + // 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) +}