@@ -47,19 +47,23 @@ func (va *ValidationAuthorityImpl) observeLatency(op, perspective, challType, pr
4747 va .metrics .validationLatency .With (labels ).Observe (latency .Seconds ())
4848}
4949
50- func (va * ValidationAuthorityImpl ) onPrimaryVA () bool {
50+ // isPrimaryVA returns true if the VA is the primary validation perspective.
51+ func (va * ValidationAuthorityImpl ) isPrimaryVA () bool {
5152 return va .perspective == PrimaryPerspective
5253}
5354
5455// mpicSummary contains multiple fields that are exported for logging purposes.
56+ // To initialize an empty mpicSummary, use newSummary(). The prepare a final
57+ // summary, use prepareSummary().
5558type mpicSummary struct {
56- // Passed is the list of distinct perspectives that Passed validation.
59+ // Passed are the distinct perspectives that Passed validation.
5760 Passed []string `json:"passedPerspectives"`
5861
59- // Failed is the list of distinct perspectives that Failed validation.
62+ // Failed are the disctint perspectives that Failed validation.
6063 Failed []string `json:"failedPerspectives"`
6164
62- // RIRs is the list of distinct RIRs that passing perspectives belonged to.
65+ // RIRs are the distinct Regional Internet Registries that passing
66+ // perspectives belonged to.
6367 RIRs []string `json:"passedRIRs"`
6468
6569 // QuorumResult is the Multi-Perspective Issuance Corroboration quorum
@@ -70,14 +74,10 @@ type mpicSummary struct {
7074 QuorumResult string `json:"quorumResult"`
7175}
7276
73- // newSummary returns a new mpicSummary with empty slices to avoid "null" in
74- // JSON output.
77+ // newSummary returns a new mpicSummary with empty slices to avoid "null" in the
78+ // JSON output. Fields are exported for logging purposes.
7579func newSummary () mpicSummary {
76- return mpicSummary {
77- Passed : []string {},
78- Failed : []string {},
79- RIRs : []string {},
80- }
80+ return mpicSummary {[]string {}, []string {}, []string {}, "" }
8181}
8282
8383// prepareSummary prepares a summary of the validation results for logging
@@ -107,7 +107,7 @@ type validateChallengeAuditLog struct {
107107 Error string `json:",omitempty"`
108108 InternalError string `json:",omitempty"`
109109 Latency float64 `json:",omitempty"`
110- MPICSummary mpicSummary `json:",omitempty"`
110+ MPICSummary mpicSummary
111111}
112112
113113// determineMaxAllowedFailures returns the maximum number of allowed failures
@@ -118,11 +118,11 @@ type validateChallengeAuditLog struct {
118118// | --- | --- |
119119// | 2-5 | 1 |
120120// | 6+ | 2 |
121- func determineMaxAllowedFailures (perspectives int ) int {
122- if perspectives < 2 {
121+ func determineMaxAllowedFailures (perspectiveCount int ) int {
122+ if perspectiveCount < 2 {
123123 return 0
124124 }
125- if perspectives < 6 {
125+ if perspectiveCount < 6 {
126126 return 1
127127 }
128128 return 2
@@ -133,70 +133,62 @@ func determineMaxAllowedFailures(perspectives int) int {
133133// validation results and a problem if the validation failed. The summary is
134134// mandatory and must be returned even if the validation failed.
135135func (va * ValidationAuthorityImpl ) remoteValidateChallenge (ctx context.Context , req * vapb.ValidationRequest ) (mpicSummary , * probs.ProblemDetails ) {
136+ // Mar 15, 2026: MUST implement using at least 3 perspectives
137+ // Jun 15, 2026: MUST implement using at least 4 perspectives
138+ // Dec 15, 2026: MUST implement using at least 5 perspectives
136139 remoteVACount := len (va .remoteVAs )
137140 if remoteVACount < 3 {
138141 return mpicSummary {}, probs .ServerInternal ("Insufficient remote perspectives: need at least 3" )
139142 }
140143
141- type remoteResult struct {
142- // rvaAddr is only used for logging.
143- rvaAddr string
144- response * vapb.ValidationResult
145- err error
144+ type response struct {
145+ addr string
146+ result * vapb.ValidationResult
147+ err error
146148 }
147149
148- responses := make (chan * remoteResult , remoteVACount )
150+ responses := make (chan * response , remoteVACount )
149151 for _ , i := range rand .Perm (remoteVACount ) {
150- rva := va .remoteVAs [i ]
151-
152152 go func (rva RemoteVA ) {
153153 res , err := rva .ValidateChallenge (ctx , req )
154- responses <- & remoteResult {
155- rvaAddr : rva .Address ,
156- response : res ,
157- err : err ,
158- }
159- }(rva )
154+ responses <- & response {rva .Address , res , err }
155+ }(va .remoteVAs [i ])
160156 }
161157
162- passed := []string {}
163- failed := []string {}
158+ var passed []string
159+ var failed []string
164160 passedRIRs := make (map [string ]struct {})
165161
166- maxRemoteFailures := determineMaxAllowedFailures (remoteVACount )
167- required := remoteVACount - maxRemoteFailures
168-
169162 var firstProb * probs.ProblemDetails
170163 for i := 0 ; i < remoteVACount ; i ++ {
171- res := <- responses
164+ resp := <- responses
172165
173166 var currProb * probs.ProblemDetails
174- if res .err != nil {
175- // The remote VA failed to respond. With no response, we cannot know
176- // the perspective name, so we use the remote VA address.
177- failed = append (failed , res .rvaAddr )
178- if errors .Is (res .err , context .Canceled ) {
167+ if resp .err != nil {
168+ // Failed to communicate with the remote VA.
169+ failed = append (failed , resp .addr )
170+ if errors .Is (resp .err , context .Canceled ) {
179171 currProb = probs .ServerInternal ("Secondary domain validation RPC canceled" )
180172 } else {
181- va .log .Errf ("Remote VA %q.ValidateChallenge failed: %s" , res . rvaAddr , res .err )
173+ va .log .Errf ("Remote VA %q.ValidateChallenge failed: %s" , resp . addr , resp .err )
182174 currProb = probs .ServerInternal ("Secondary domain validation RPC failed" )
183175 }
184176
185- } else if res . response .Problems != nil {
177+ } else if resp . result .Problems != nil {
186178 // The remote VA returned a problem.
187- failed = append (failed , res . response .Perspective )
179+ failed = append (failed , resp . result .Perspective )
188180
189181 var err error
190- currProb , err = bgrpc .PBToProblemDetails (res . response .Problems )
182+ currProb , err = bgrpc .PBToProblemDetails (resp . result .Problems )
191183 if err != nil {
192- va .log .Errf ("Remote VA %q.ValidateChallenge returned malformed problem: %s" , res . rvaAddr , err )
184+ va .log .Errf ("Remote VA %q.ValidateChallenge returned a malformed problem: %s" , resp . addr , err )
193185 currProb = probs .ServerInternal ("Secondary domain validation RPC returned malformed result" )
194186 }
195187
196188 } else {
197- // The remote VA returned a successful response .
198- passed = append (passed , res . response .Perspective )
199- passedRIRs [res . response .Rir ] = struct {}{}
189+ // The remote VA returned a successful result .
190+ passed = append (passed , resp . result .Perspective )
191+ passedRIRs [resp . result .Rir ] = struct {}{}
200192 }
201193
202194 if firstProb == nil && currProb != nil {
@@ -208,30 +200,36 @@ func (va *ValidationAuthorityImpl) remoteValidateChallenge(ctx context.Context,
208200 // Prepare the summary, this MUST be returned even if the validation failed.
209201 summary := prepareSummary (passed , failed , passedRIRs , remoteVACount )
210202
211- if len (passed ) >= required {
212- // We may have enough successful responses.
213- if len (passedRIRs ) < 2 {
214- if firstProb != nil {
215- firstProb .Detail = fmt .Sprintf ("During secondary domain validation: %s" , firstProb .Detail )
216- return summary , firstProb
217- }
218- return summary , probs .Unauthorized ("Secondary domain validation failed to receive enough responses from disctinct RIRs" )
203+ maxRemoteFailures := determineMaxAllowedFailures (remoteVACount )
204+ if len (failed ) > maxRemoteFailures {
205+ // Too many failures to reach quorum.
206+ if firstProb != nil {
207+ firstProb .Detail = fmt .Sprintf ("During secondary domain validation: %s" , firstProb .Detail )
208+ return summary , firstProb
219209 }
220- // We have enough successful responses from distinct perspectives.
221- return summary , nil
210+ return summary , probs .ServerInternal ("Secondary domain validation failed due to too many failures" )
222211 }
223212
224- if len (failed ) > maxRemoteFailures {
225- // We have too many failed responses.
213+ if len (passed ) < (remoteVACount - maxRemoteFailures ) {
214+ // Too few successful responses to reach quorum.
215+ if firstProb != nil {
216+ firstProb .Detail = fmt .Sprintf ("During secondary domain validation: %s" , firstProb .Detail )
217+ return summary , firstProb
218+ }
219+ return summary , probs .ServerInternal ("Secondary domain validation failed due to insufficient successful responses" )
220+ }
221+
222+ if len (passedRIRs ) < 2 {
223+ // Too few successful responses from distinct RIRs to reach quorum.
226224 if firstProb != nil {
227225 firstProb .Detail = fmt .Sprintf ("During secondary domain validation: %s" , firstProb .Detail )
228226 return summary , firstProb
229227 }
228+ return summary , probs .Unauthorized ("Secondary domain validation failed to receive enough corroborations from distinct RIRs" )
230229 }
231- // This return is unreachable because for any number of remote VAs (n),
232- // either at least (n - maxFailures) perspectives pass, or more than
233- // maxFailures fail. Thus, one of the above conditions is always satisfied.
234- return summary , probs .ServerInternal ("Secondary domain validation failed to receive all responses" )
230+
231+ // Enough successful responses from distinct RIRs to reach quorum.
232+ return summary , nil
235233}
236234
237235// ValidateChallenge performs a local validation of a challenge using the
@@ -254,81 +252,69 @@ func (va *ValidationAuthorityImpl) ValidateChallenge(ctx context.Context, req *v
254252 return nil , berrors .MalformedError ("challenge failed consistency check: %s" , err )
255253 }
256254
257- var prob * probs.ProblemDetails
258- var localLatency time.Duration
259- var latency time.Duration
260- var summary = newSummary ()
261- start := va .clk .Now ()
262-
263255 auditLog := validateChallengeAuditLog {
264256 AuthzID : req .AuthzID ,
265257 Requester : req .RegID ,
266258 Identifier : req .Identifier .Value ,
267259 Challenge : chall ,
268260 }
269261
262+ var prob * probs.ProblemDetails
263+ var localLatency time.Duration
264+ var summary = newSummary ()
265+ start := va .clk .Now ()
266+
270267 defer func () {
271268 probType := ""
272269 outcome := fail
273270 if prob != nil {
274- // Validation failed .
271+ // Failed to validate the challenge .
275272 probType = string (prob .Type )
276273 auditLog .Error = prob .Error ()
277274 auditLog .Challenge .Error = prob
278275 auditLog .Challenge .Status = core .StatusInvalid
279-
280276 } else {
281- // Validation passed .
277+ // Successfully validated the challenge .
282278 outcome = pass
283279 auditLog .Challenge .Status = core .StatusValid
284280 }
285- // Always observe local latency (primary|remote).
281+ // Observe local validation latency (primary|remote).
286282 va .observeLatency (challenge , va .perspective , string (chall .Type ), probType , outcome , localLatency )
287- if va .onPrimaryVA () {
288- // Log the MPIC summary.
283+ if va .isPrimaryVA () {
284+ // Observe total validation latency (primary+remote).
285+ va .observeLatency (challenge , all , string (chall .Type ), probType , outcome , va .clk .Since (start ))
289286 auditLog .MPICSummary = summary
290-
291- if latency > 0 {
292- // Observe total latency (primary+remote).
293- va .observeLatency (challenge , all , string (chall .Type ), probType , outcome , va .clk .Since (start ))
294- }
295287 }
296-
297- // No matter what, log the audit log.
288+ // Log the total validation latency.
298289 auditLog .Latency = va .clk .Since (start ).Round (time .Millisecond ).Seconds ()
299290 va .log .AuditObject ("Challenge validation result" , auditLog )
300291 }()
301292
302- // Perform local validation .
293+ // Validate the challenge locally .
303294 records , localErr := va .validateChallenge (ctx , identifier , chall .Type , chall .Token , req .KeyAuthorization )
304295
305- // Stop the clock for local validation latency (this may be remote) .
296+ // Stop the clock for local validation latency.
306297 localLatency = va .clk .Since (start )
307298
308- // Log the validation records, even if validation failed.
309- auditLog .Challenge .ValidationRecord = records
310-
311- // The following checks are in a specific order to ensure that the most
312- // pertinent problems are returned first.
299+ // The following checks are performed in a specific order to ensure that the
300+ // most relevant problem is returned to the subscriber.
313301
302+ auditLog .Challenge .ValidationRecord = records
314303 if localErr == nil && ! auditLog .Challenge .RecordsSane () {
315- // Validation was successful, but the records failed sanity check.
316304 localErr = errors .New ("records from local validation failed sanity check" )
317305 }
318306
319307 if localErr != nil {
320- // Validation failed locally.
308+ // Failed to validate the challenge locally.
321309 auditLog .InternalError = localErr .Error ()
322310 prob = detailedError (localErr )
323311 return bgrpc .ValidationResultToPB (records , filterProblemDetails (prob ), va .perspective , va .rir )
324312 }
325313
326- if va .onPrimaryVA () {
327- // Perform remote validation .
314+ if va .isPrimaryVA () {
315+ // Attempt to validate the challenge remotely .
328316 summary , prob = va .remoteValidateChallenge (ctx , req )
329-
330- // Stop the clock for total validation latency.
331- latency = va .clk .Since (start )
332317 }
318+
333319 return bgrpc .ValidationResultToPB (records , filterProblemDetails (prob ), va .perspective , va .rir )
334320}
0 commit comments