@@ -108,25 +108,11 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy
108108
109109 // Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
110110 // the Relying Party's origin.
111- var fqOrigin string
112111
113- if fqOrigin , err = FullyQualifiedOrigin (c .Origin ); err != nil {
114- return ErrParsingData .WithDetails ("Error decoding clientData origin as URL" ).WithError (err )
115- }
116-
117- found := false
118-
119- for _ , origin := range rpOrigins {
120- if strings .EqualFold (fqOrigin , origin ) {
121- found = true
122- break
123- }
124- }
125-
126- if ! found {
112+ if ! IsOriginInHaystack (c .Origin , rpOrigins ) {
127113 return ErrVerification .
128114 WithDetails ("Error validating origin" ).
129- WithInfo (fmt .Sprintf ("Expected Values: %s, Received: %s" , rpOrigins , fqOrigin ))
115+ WithInfo (fmt .Sprintf ("Expected Values: %s, Received: %s" , rpOrigins , c . Origin ))
130116 }
131117
132118 if rpTopOriginsVerify != TopOriginIgnoreVerificationMode {
@@ -145,10 +131,6 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy
145131 possibleTopOrigins []string
146132 )
147133
148- if fqTopOrigin , err = FullyQualifiedOrigin (c .TopOrigin ); err != nil {
149- return ErrParsingData .WithDetails ("Error decoding clientData topOrigin as URL" ).WithError (err )
150- }
151-
152134 switch rpTopOriginsVerify {
153135 case TopOriginExplicitVerificationMode :
154136 possibleTopOrigins = rpTopOrigins
@@ -160,16 +142,7 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy
160142 return ErrNotImplemented .WithDetails ("Error handling unknown Top Origin verification mode" )
161143 }
162144
163- found = false
164-
165- for _ , origin := range possibleTopOrigins {
166- if strings .EqualFold (fqTopOrigin , origin ) {
167- found = true
168- break
169- }
170- }
171-
172- if ! found {
145+ if ! IsOriginInHaystack (c .TopOrigin , possibleTopOrigins ) {
173146 return ErrVerification .
174147 WithDetails ("Error validating top origin" ).
175148 WithInfo (fmt .Sprintf ("Expected Values: %s, Received: %s" , possibleTopOrigins , fqTopOrigin ))
@@ -221,3 +194,138 @@ const (
221194 // Top Origin is verified against the allowed Top Origins values.
222195 TopOriginExplicitVerificationMode
223196)
197+
198+ // IsOriginInHaystack checks if the needle is in the haystack using the mechanism to determine origin equality defined
199+ // in HTML5 Section 5.3 and RFC3986 Section 6.2.1.
200+ //
201+ // Specifically if the needle value includes the '://' character sequence and can be parsed as URL's;
202+ // we check each item in the haystack to see if it matches the same rules, and then if the scheme and host components
203+ // match case-insensitively and check all other components with simple string comparison. The query arguments have a
204+ // special match condition where we check if the values are equal in length, then iteratively check if each key value
205+ // pair are matches. This allows the query arguments to have a different order.
206+ //
207+ // If the needle value does not include the '://' character sequence or can't be parsed as a URL equality is determined
208+ // using simple string comparison.
209+ //
210+ // See (Origin Definition): https://www.w3.org/TR/2011/WD-html5-20110525/origin-0.html
211+ //
212+ // See (Simple String Comparison Definition): https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.1
213+ func IsOriginInHaystack (needle string , haystack []string ) bool {
214+ needleURI := parseNormalizedURI (needle )
215+
216+ if needleURI != nil {
217+ for _ , hay := range haystack {
218+ if hayURI := parseNormalizedURI (hay ); hayURI != nil {
219+ if isOriginEqual (needleURI , hayURI ) {
220+ return true
221+ }
222+ }
223+ }
224+ } else {
225+ for _ , hay := range haystack {
226+ if needle == hay {
227+ return true
228+ }
229+ }
230+ }
231+
232+ return false
233+ }
234+
235+ func isOriginEqual (a * url.URL , b * url.URL ) bool {
236+ if ! strings .EqualFold (a .Scheme , b .Scheme ) {
237+ return false
238+ }
239+
240+ if ! strings .EqualFold (a .Host , b .Host ) {
241+ return false
242+ }
243+
244+ if a .Path != b .Path || a .RawPath != b .RawPath {
245+ return false
246+ }
247+
248+ if a .Fragment != b .Fragment || a .RawFragment != b .RawFragment {
249+ return false
250+ }
251+
252+ if a .Opaque != b .Opaque {
253+ return false
254+ }
255+
256+ if (len (a .RawQuery ) != 0 || len (b .RawQuery ) != 0 ) && ! isURLValuesEqual (a .Query (), b .Query ()) {
257+ return false
258+ }
259+
260+ if ! isURLUserEqual (a .User , b .User ) {
261+ return false
262+ }
263+
264+ return true
265+ }
266+
267+ func isURLUserEqual (a , b * url.Userinfo ) bool {
268+ if a == nil && b == nil {
269+ return true
270+ }
271+
272+ if a .Username () != b .Username () {
273+ return false
274+ }
275+
276+ ap , aok := a .Password ()
277+ bp , bok := b .Password ()
278+
279+ if ! aok && ! bok {
280+ return true
281+ }
282+
283+ if aok != bok {
284+ return false
285+ }
286+
287+ if ap != bp {
288+ return false
289+ }
290+
291+ return true
292+ }
293+
294+ func isURLValuesEqual (a , b url.Values ) bool {
295+ if len (a ) != len (b ) {
296+ return false
297+ }
298+
299+ for k := range a {
300+ if a .Get (k ) != b .Get (k ) {
301+ return false
302+ }
303+ }
304+
305+ return true
306+ }
307+
308+ func parseNormalizedURI (raw string ) * url.URL {
309+ if ! strings .Contains (raw , "://" ) {
310+ return nil
311+ }
312+
313+ // We can ignore the error here because it's effectively not a FQDN if this fails.
314+ uri , _ := url .Parse (raw )
315+
316+ if uri == nil {
317+ return nil
318+ }
319+
320+ // Perform Path Normalization.
321+ if uri .Path == "/" || uri .RawPath == "/" {
322+ uri .Path = ""
323+ uri .RawPath = ""
324+ }
325+
326+ // Perform Scheme/Host Normalization.
327+ uri .Scheme = strings .ToLower (uri .Scheme )
328+ uri .Host = strings .ToLower (uri .Host )
329+
330+ return uri
331+ }
0 commit comments