451 lines
14 KiB
Go
Vendored
451 lines
14 KiB
Go
Vendored
package protocol
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestVerifyCollectedClientData(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
origin string
|
|
topOrigin string
|
|
crossOrigin bool
|
|
rpOrigins []string
|
|
rpTopOrigins []string
|
|
topOriginMode TopOriginVerificationMode
|
|
allowCrossOrign bool
|
|
ceremony CeremonyType
|
|
err string
|
|
errType string
|
|
errDetails string
|
|
errInfo string
|
|
}{
|
|
{
|
|
name: "ShouldSucceed",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
allowCrossOrign: true,
|
|
},
|
|
{
|
|
name: "ShouldSucceedNoTopOrigin",
|
|
origin: "http://example.com",
|
|
crossOrigin: true,
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
allowCrossOrign: true,
|
|
},
|
|
{
|
|
name: "ShouldSucceedTopOriginDifferentFromOrigin",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example2.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
},
|
|
{
|
|
name: "ShouldFailTopOriginMismatch",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example2.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
rpTopOrigins: []string{"https://example3.com"},
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
err: "Error validating top origin",
|
|
},
|
|
{
|
|
name: "ShouldSucceedTopOriginImplicit",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
topOriginMode: TopOriginImplicitVerificationMode,
|
|
},
|
|
{
|
|
name: "ShouldSucceedTopOriginAuto",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
rpTopOrigins: []string{"https://example.com"},
|
|
topOriginMode: TopOriginAutoVerificationMode,
|
|
},
|
|
{
|
|
name: "ShouldSucceedMultipleExpectedOrigins",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
rpOrigins: []string{"https://exmaple.com", "9C:B4:AE:EF:05:53:6E:73:0E:C4:B8:02:E7:67:F6:7D:A4:E7:BC:26:D7:42:B5:27:FF:01:7D:68:2A:EB:FA:1D", "http://example.com"},
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
},
|
|
{
|
|
name: "ShouldFailTopOriginInvalidMode",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
rpTopOrigins: []string{"https://example.com"},
|
|
topOriginMode: -1,
|
|
errType: "not_implemented",
|
|
errDetails: "Error handling unknown Top Origin verification mode",
|
|
},
|
|
{
|
|
name: "ShouldFailCrossOriginNotAllowed",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: false,
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
errType: "verification_error",
|
|
errDetails: "Error validating cross origin flag",
|
|
errInfo: "The cross origin flag is invalid due to the configuration.",
|
|
},
|
|
{
|
|
name: "ShouldFailUnexpectedOrigin",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
rpOrigins: []string{"http://different.com"},
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
errType: "verification_error",
|
|
errDetails: "Error validating origin",
|
|
errInfo: "Expected Values: [http://different.com], Received: http://example.com",
|
|
},
|
|
{
|
|
name: "ShouldFailTopOriginWithoutCrossOrigin",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example2.com",
|
|
crossOrigin: false,
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
errType: "verification_error",
|
|
errDetails: "Error validating topOrigin",
|
|
errInfo: "The topOrigin can't have values unless crossOrigin is true.",
|
|
},
|
|
{
|
|
name: "ShouldFailUnexpectedTopOrigin",
|
|
origin: "http://example.com",
|
|
topOrigin: "http://example.com",
|
|
crossOrigin: true,
|
|
allowCrossOrign: true,
|
|
rpOrigins: []string{"http://example.com"},
|
|
rpTopOrigins: []string{"http://different.com"},
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
err: "Error validating top origin",
|
|
},
|
|
{
|
|
name: "ShouldFailCeremonyMismatch",
|
|
origin: "http://example.com",
|
|
crossOrigin: false,
|
|
topOriginMode: TopOriginExplicitVerificationMode,
|
|
ceremony: AssertCeremony,
|
|
errType: "verification_error",
|
|
errDetails: "Error validating ceremony type",
|
|
errInfo: fmt.Sprintf("Expected Value: %s, Received: %s", AssertCeremony, CreateCeremony),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
challenge, err := CreateChallenge()
|
|
require.NoError(t, err)
|
|
|
|
ccd := setupCollectedClientData(challenge, tc.origin, tc.topOrigin, tc.crossOrigin)
|
|
|
|
rpOrigins := tc.rpOrigins
|
|
if rpOrigins == nil {
|
|
rpOrigins = []string{ccd.Origin}
|
|
}
|
|
|
|
rpTopOrigins := tc.rpTopOrigins
|
|
if rpTopOrigins == nil {
|
|
rpTopOrigins = []string{ccd.TopOrigin}
|
|
}
|
|
|
|
ceremony := tc.ceremony
|
|
if ceremony == "" {
|
|
ceremony = ccd.Type
|
|
}
|
|
|
|
err = ccd.Verify(challenge.String(), ceremony, rpOrigins, rpTopOrigins, tc.topOriginMode, tc.allowCrossOrign)
|
|
|
|
switch {
|
|
case tc.err != "":
|
|
assert.EqualError(t, err, tc.err)
|
|
case tc.errType != "":
|
|
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
|
|
default:
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifyCollectedClientData_IncorrectChallenge(t *testing.T) {
|
|
challenge, err := CreateChallenge()
|
|
require.NoError(t, err)
|
|
|
|
ccd := setupCollectedClientData(challenge, "http://example.com", "http://example.com", true)
|
|
|
|
bogusChallenge, err := CreateChallenge()
|
|
require.NoError(t, err)
|
|
|
|
AssertIsProtocolError(t, ccd.Verify(bogusChallenge.String(), ccd.Type, []string{ccd.Origin}, []string{ccd.TopOrigin}, TopOriginExplicitVerificationMode, true), "verification_error", "Error validating challenge", fmt.Sprintf("Expected b Value: \"%s\"\nReceived b: \"%s\"\n", bogusChallenge.String(), challenge.String()))
|
|
}
|
|
|
|
func TestVerifyCollectedClientData_TokenBinding(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
tokenBinding *TokenBinding
|
|
err string
|
|
}{
|
|
{
|
|
name: "ShouldSucceedWithNilTokenBinding",
|
|
tokenBinding: nil,
|
|
},
|
|
{
|
|
name: "ShouldSucceedWithPresentStatus",
|
|
tokenBinding: &TokenBinding{Status: Present, ID: "abc"},
|
|
},
|
|
{
|
|
name: "ShouldSucceedWithSupportedStatus",
|
|
tokenBinding: &TokenBinding{Status: Supported},
|
|
},
|
|
{
|
|
name: "ShouldSucceedWithNotSupportedStatus",
|
|
tokenBinding: &TokenBinding{Status: NotSupported},
|
|
},
|
|
{
|
|
name: "ShouldFailWithEmptyStatus",
|
|
tokenBinding: &TokenBinding{},
|
|
err: "Error decoding clientData, token binding present without status",
|
|
},
|
|
{
|
|
name: "ShouldFailWithInvalidStatus",
|
|
tokenBinding: &TokenBinding{Status: "invalid-status"},
|
|
err: "Error decoding clientData, token binding present with invalid status",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
newChallenge, err := CreateChallenge()
|
|
require.NoError(t, err)
|
|
|
|
ccd := setupCollectedClientData(newChallenge, "http://example.com", "", false)
|
|
ccd.TokenBinding = tc.tokenBinding
|
|
|
|
err = ccd.Verify(newChallenge.String(), CreateCeremony, []string{ccd.Origin}, nil, TopOriginExplicitVerificationMode, false)
|
|
if tc.err != "" {
|
|
assert.EqualError(t, err, tc.err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFullyQualifiedOrigin(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
have string
|
|
expected, expectedErr string
|
|
}{
|
|
{"ShouldParse", "https://app.example.com", "https://app.example.com", ``},
|
|
{"ShouldParseWithPath", "https://app.example.com/apath", "https://app.example.com", ``},
|
|
{"ShouldParseWithPort", "https://app.example.com:8443/apath", "https://app.example.com:8443", ``},
|
|
{"ShouldParseWithCredentials", "https://user:password@app.example.com/", "https://app.example.com", ``},
|
|
{"ShouldParseWithQuery", "https://app.example.com/?abc=123", "https://app.example.com", ``},
|
|
{"ShouldParseWithFragment", "https://app.example.com/#abc", "https://app.example.com", ``},
|
|
{"ShouldSkipParsingAndroidNative", "android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0", "android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0", ""},
|
|
{"ShouldFailToParseMissingScheme", "app.example.com/apath", "", `parse "app.example.com/apath": invalid URI for request`},
|
|
{"ShouldFailToParseBlankScheme", "://app.example.com/apath", "", `parse "://app.example.com/apath": missing protocol scheme`},
|
|
{"ShouldFailToParseMissingHost", "https:///apath", "", `url 'https:///apath' does not have a host`},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
actual, actualErr := FullyQualifiedOrigin(tc.have)
|
|
|
|
assert.Equal(t, tc.expected, actual)
|
|
|
|
if tc.expectedErr == "" {
|
|
assert.NoError(t, actualErr)
|
|
} else {
|
|
assert.EqualError(t, actualErr, tc.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsOriginInHaystack(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
origin string
|
|
haystack []string
|
|
expected bool
|
|
}{
|
|
{
|
|
"ShouldHandleFullyQualifiedOrigin",
|
|
"https://app.example.com",
|
|
[]string{"https://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginCaseInsensitiveScheme",
|
|
"https://app.example.com",
|
|
[]string{"HTTPS://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginCaseInsensitiveHost",
|
|
"https://app.EXAMPLE.com",
|
|
[]string{"https://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginWithPort",
|
|
"https://app.example.com:443",
|
|
[]string{"https://app.example.com:443"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentScheme",
|
|
"http://app.example.com",
|
|
[]string{"https://app.example.com"},
|
|
false,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentPort",
|
|
"https://app.example.com:443",
|
|
[]string{"https://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentPortNotMatchingScheme",
|
|
"https://app.example.com:80",
|
|
[]string{"https://app.example.com"},
|
|
false,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentPath",
|
|
"https://app.example.com/abc",
|
|
[]string{"https://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentQuery",
|
|
"https://app.example.com/?abc=123",
|
|
[]string{"https://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentQueryCount",
|
|
"https://app.example.com/?abc=123",
|
|
[]string{"https://app.example.com/?zyz=123&abc=123"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentQueryOrder",
|
|
"https://app.example.com/?abc=123&xyz=123",
|
|
[]string{"https://app.example.com/?xyz=123&abc=123"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDifferentQueryValue",
|
|
"https://app.example.com/?abc=123&xyz=123",
|
|
[]string{"https://app.example.com/?xyz=1234&abc=123"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginFragment",
|
|
"https://app.example.com/#abc",
|
|
[]string{"https://app.example.com/#abc"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginFragmentDifferent",
|
|
"https://app.example.com/#abc",
|
|
[]string{"https://app.example.com/#abc2"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginWithoutAllowed",
|
|
"https://app.example.com",
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginWithTrailingSlashes",
|
|
"https://app.example.com/",
|
|
[]string{"https://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleNativeAppAndroid",
|
|
"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0",
|
|
[]string{"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleNativeAppAndroidCaseSensitive",
|
|
"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69F0",
|
|
[]string{"android:apk-key-hash:7d1043473d55bfa90e8530d35801d4e381bc69f0"},
|
|
false,
|
|
},
|
|
{
|
|
"ShouldHandleNonFQDNOrigin",
|
|
"https://user:password@app.example.com/",
|
|
[]string{"https://app.example.com/"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleNonFQDNOriginExactStringMatch",
|
|
"https://user:password@app.example.com/",
|
|
[]string{"https://user:password@app.example.com/"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDefaultPortEquivalentHTTPS",
|
|
"https://app.example.com:443",
|
|
[]string{"https://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleFullyQualifiedOriginDefaultPortEquivalentHTTP",
|
|
"http://app.example.com:80",
|
|
[]string{"http://app.example.com"},
|
|
true,
|
|
},
|
|
{
|
|
"ShouldHandleInvalidURLAsSimpleStringMatch",
|
|
"http://app.example.%%%&123?1",
|
|
[]string{"http://app.example.%%%&123?1"},
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.expected, IsOriginInHaystack(tc.origin, tc.haystack))
|
|
})
|
|
}
|
|
}
|
|
|
|
func setupCollectedClientData(challenge URLEncodedBase64, origin, topOrigin string, crossOrigin bool) *CollectedClientData {
|
|
ccd := &CollectedClientData{
|
|
Type: CreateCeremony,
|
|
Origin: origin,
|
|
TopOrigin: topOrigin,
|
|
CrossOrigin: crossOrigin,
|
|
Challenge: challenge.String(),
|
|
}
|
|
|
|
return ccd
|
|
}
|