Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 92 additions & 30 deletions protocol/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,25 +108,11 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy

// Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
// the Relying Party's origin.
var fqOrigin string

if fqOrigin, err = FullyQualifiedOrigin(c.Origin); err != nil {
return ErrParsingData.WithDetails("Error decoding clientData origin as URL").WithError(err)
}

found := false

for _, origin := range rpOrigins {
if strings.EqualFold(fqOrigin, origin) {
found = true
break
}
}

if !found {
if !IsOriginInHaystack(c.Origin, rpOrigins) {
return ErrVerification.
WithDetails("Error validating origin").
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", rpOrigins, fqOrigin))
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", rpOrigins, c.Origin))
}

if rpTopOriginsVerify != TopOriginIgnoreVerificationMode {
Expand All @@ -145,10 +131,6 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy
possibleTopOrigins []string
)

if fqTopOrigin, err = FullyQualifiedOrigin(c.TopOrigin); err != nil {
return ErrParsingData.WithDetails("Error decoding clientData topOrigin as URL").WithError(err)
}

switch rpTopOriginsVerify {
case TopOriginExplicitVerificationMode:
possibleTopOrigins = rpTopOrigins
Expand All @@ -160,16 +142,7 @@ func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyTy
return ErrNotImplemented.WithDetails("Error handling unknown Top Origin verification mode")
}

found = false

for _, origin := range possibleTopOrigins {
if strings.EqualFold(fqTopOrigin, origin) {
found = true
break
}
}

if !found {
if !IsOriginInHaystack(c.TopOrigin, possibleTopOrigins) {
return ErrVerification.
WithDetails("Error validating top origin").
WithInfo(fmt.Sprintf("Expected Values: %s, Received: %s", possibleTopOrigins, fqTopOrigin))
Expand Down Expand Up @@ -221,3 +194,92 @@ const (
// Top Origin is verified against the allowed Top Origins values.
TopOriginExplicitVerificationMode
)

// IsOriginInHaystack checks if the needle is in the haystack using the mechanism to determine origin equality defined
// in HTML5 Section 5.3 and RFC3986 Section 6.2.1.
//
// Specifically if the needle value has the 'http://' or 'https://' prefix (case-insensitive) and can be parsed as a
// URL; we check each item in the haystack to see if it matches the same rules, and then if the scheme and host (with
// a normalized port) components match case-insensitively then they're considered a match.
//
// If the needle value does not have the 'http://' or 'https://' prefix (case-insensitive) or can't be parsed as a URL
// equality is determined using simple string comparison.
//
// It is important to note that this function completely ignores Apple Associated Domains entirely as Apple is using
// an unassigned Well-Known URI in breech of Well-Known Uniform Resource Identifiers (RFC8615).
//
// See (Origin Definition): https://www.w3.org/TR/2011/WD-html5-20110525/origin-0.html
//
// See (Simple String Comparison Definition): https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.1
//
// See (Apple Associated Domains): https://developer.apple.com/documentation/xcode/supporting-associated-domains
//
// See (IANA Well Known URI Assignments): https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml
//
// See (Well-Known Uniform Resource Identifiers): https://datatracker.ietf.org/doc/html/rfc8615
func IsOriginInHaystack(needle string, haystack []string) bool {
needleURI := parseOriginURI(needle)

if needleURI != nil {
for _, hay := range haystack {
if hayURI := parseOriginURI(hay); hayURI != nil {
if isOriginEqual(needleURI, hayURI) {
return true
}
}
}
} else {
for _, hay := range haystack {
if needle == hay {
return true
}
}
}

return false
}

func isOriginEqual(a *url.URL, b *url.URL) bool {
if !strings.EqualFold(a.Scheme, b.Scheme) {
return false
}

if !strings.EqualFold(a.Host, b.Host) {
return false
}

return true
}

func parseOriginURI(raw string) *url.URL {
if !isPossibleFQDN(raw) {
return nil
}

// We can ignore the error here because it's effectively not a FQDN if this fails.
uri, _ := url.Parse(raw)

if uri == nil {
return nil
}

// Normalize the port if necessary.
switch uri.Scheme {
case "http":
if uri.Port() == "80" {
uri.Host = uri.Hostname()
}
case "https":
if uri.Port() == "443" {
uri.Host = uri.Hostname()
}
}

return uri
}

func isPossibleFQDN(raw string) bool {
normalized := strings.ToLower(raw)

return strings.HasPrefix(normalized, "http://") || strings.HasPrefix(normalized, "https://")
}
Loading
Loading