Skip to content

Commit bdad59e

Browse files
authored
Fix fully qualified origin, add unit tests. (#217)
1 parent 83b8214 commit bdad59e

File tree

2 files changed

+227
-4
lines changed

2 files changed

+227
-4
lines changed

Src/Fido2/AuthenticatorResponse.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ protected void BaseVerify(string expectedOrigin, byte[] originalChallenge, byte[
6767
if (!Challenge.SequenceEqual(originalChallenge))
6868
throw new Fido2VerificationException("Challenge not equal to original challenge");
6969

70+
var fullyQualifiedOrigin = FullyQualifiedOrigin(Origin);
71+
var fullyQualifiedExpectedOrigin = FullyQualifiedOrigin(expectedOrigin);
72+
7073
// 5. Verify that the value of C.origin matches the Relying Party's origin.
71-
if (!string.Equals(FullyQualifiedOrigin(this.Origin), expectedOrigin, StringComparison.OrdinalIgnoreCase))
72-
throw new Fido2VerificationException($"Origin {Origin} not equal to original origin {expectedOrigin}");
74+
if (!string.Equals(fullyQualifiedOrigin, fullyQualifiedExpectedOrigin, StringComparison.OrdinalIgnoreCase))
75+
throw new Fido2VerificationException($"Fully qualified origin {fullyQualifiedOrigin} of {Origin} not equal to fully qualified original origin {fullyQualifiedExpectedOrigin} of {expectedOrigin}");
7376

7477
// 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained.
7578
// If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
@@ -83,9 +86,10 @@ private string FullyQualifiedOrigin(string origin)
8386
{
8487
var uri = new Uri(origin);
8588

86-
var fullyQualifiedOrigin = uri.IsDefaultPort ? $"{uri.Scheme}://{uri.Host}" : $"{uri.Scheme}://{uri.Host}:{uri.Port}";
89+
if (UriHostNameType.Unknown != uri.HostNameType)
90+
return uri.IsDefaultPort ? $"{uri.Scheme}://{uri.Host}" : $"{uri.Scheme}://{uri.Host}:{uri.Port}";
8791

88-
return fullyQualifiedOrigin;
92+
return origin;
8993
}
9094
}
9195
}

Test/AuthenticatorResponse.cs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,225 @@ namespace Test
1515
{
1616
public class AuthenticatorResponse
1717
{
18+
19+
[Theory]
20+
[InlineData("https://www.passwordless.dev", "https://www.passwordless.dev")]
21+
[InlineData("https://www.passwordless.dev:443", "https://www.passwordless.dev:443")]
22+
[InlineData("https://www.passwordless.dev", "https://www.passwordless.dev:443")]
23+
[InlineData("https://www.passwordless.dev:443", "https://www.passwordless.dev")]
24+
[InlineData("https://www.passwordless.dev:443/foo/bar.html","https://www.passwordless.dev:443/foo/bar.html")]
25+
[InlineData("https://www.passwordless.dev:443/foo/bar.html", "https://www.passwordless.dev:443/bar/foo.html")]
26+
[InlineData("https://www.passwordless.dev:443/foo/bar.html", "https://www.passwordless.dev/bar/foo.html")]
27+
[InlineData("https://www.passwordless.dev:443/foo/bar.html", "https://www.passwordless.dev")]
28+
[InlineData("ftp://www.passwordless.dev", "ftp://www.passwordless.dev")]
29+
[InlineData("ftp://www.passwordless.dev:8080", "ftp://www.passwordless.dev:8080")]
30+
[InlineData("http://127.0.0.1", "http://127.0.0.1")]
31+
[InlineData("http://localhost", "http://localhost")]
32+
[InlineData("https://127.0.0.1:80", "https://127.0.0.1:80")]
33+
[InlineData("http://localhost:80", "http://localhost:80")]
34+
[InlineData("http://127.0.0.1:443", "http://127.0.0.1:443")]
35+
[InlineData("http://localhost:443", "http://localhost:443")]
36+
[InlineData("android:apk-key-hash:Ea3dD4m7ccbwcw+a27/D547hfwYra2gKE4lIBbBjCTU", "android:apk-key-hash:Ea3dD4m7ccbwcw+a27/D547hfwYra2gKE4lIBbBjCTU")]
37+
[InlineData("lorem:ipsum:dolor", "lorem:ipsum:dolor")]
38+
[InlineData("lorem:/ipsum:4321", "lorem:/ipsum:4321")]
39+
[InlineData("lorem://ipsum:1234", "lorem://ipsum:1234")]
40+
[InlineData("lorem://ipsum:9876/sit", "lorem://ipsum:9876/sit")]
41+
[InlineData("foo://bar:321/path/", "foo://bar:321/path/")]
42+
[InlineData("foo://bar:321/path","foo://bar:321/path")]
43+
[InlineData("http://[0:0:0:0:0:0:0:1]", "http://[0:0:0:0:0:0:0:1]")]
44+
[InlineData("http://[0:0:0:0:0:0:0:1]", "http://[0:0:0:0:0:0:0:1]:80")]
45+
[InlineData("https://[0:0:0:0:0:0:0:1]", "https://[0:0:0:0:0:0:0:1]")]
46+
[InlineData("https://[0:0:0:0:0:0:0:1]", "https://[0:0:0:0:0:0:0:1]:443")]
47+
public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin)
48+
{
49+
var challenge = RandomGenerator.Default.GenerateBytes(128);
50+
var rp = origin;
51+
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
52+
var authData = new AuthenticatorData(
53+
SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(origin)),
54+
AuthenticatorFlags.UP | AuthenticatorFlags.AT,
55+
0,
56+
acd,
57+
null
58+
).ToByteArray();
59+
var clientDataJson = Encoding.UTF8.GetBytes(
60+
JsonConvert.SerializeObject
61+
(
62+
new
63+
{
64+
Type = "webauthn.create",
65+
Challenge = challenge,
66+
Origin = rp,
67+
}
68+
)
69+
);
70+
var rawResponse = new AuthenticatorAttestationRawResponse
71+
{
72+
Type = PublicKeyCredentialType.PublicKey,
73+
Id = new byte[] { 0xf1, 0xd0 },
74+
RawId = new byte[] { 0xf1, 0xd0 },
75+
Response = new AuthenticatorAttestationRawResponse.ResponseData()
76+
{
77+
AttestationObject = CBORObject.NewMap().Add("fmt", "none").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(),
78+
ClientDataJson = clientDataJson
79+
},
80+
};
81+
82+
var origChallenge = new CredentialCreateOptions
83+
{
84+
Attestation = AttestationConveyancePreference.Direct,
85+
AuthenticatorSelection = new AuthenticatorSelection
86+
{
87+
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
88+
RequireResidentKey = true,
89+
UserVerification = UserVerificationRequirement.Required,
90+
},
91+
Challenge = challenge,
92+
ErrorMessage = "",
93+
PubKeyCredParams = new List<PubKeyCredParam>()
94+
{
95+
new PubKeyCredParam
96+
{
97+
Alg = COSE.Algorithm.ES256,
98+
Type = PublicKeyCredentialType.PublicKey,
99+
}
100+
},
101+
Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
102+
Status = "ok",
103+
User = new Fido2User
104+
{
105+
Name = "testuser",
106+
Id = Encoding.UTF8.GetBytes("testuser"),
107+
DisplayName = "Test User",
108+
},
109+
Timeout = 60000,
110+
};
111+
112+
IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
113+
{
114+
return Task.FromResult(true);
115+
};
116+
117+
var lib = new Fido2(new Fido2Configuration()
118+
{
119+
ServerDomain = rp,
120+
ServerName = rp,
121+
Origin = expectedOrigin,
122+
});
123+
124+
var result = await lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback);
125+
}
126+
127+
128+
[Theory]
129+
[InlineData("https://www.passwordless.dev", "http://www.passwordless.dev")]
130+
[InlineData("https://www.passwordless.dev:443", "http://www.passwordless.dev:443")]
131+
[InlineData("https://www.passwordless.dev", "http://www.passwordless.dev:443")]
132+
[InlineData("https://www.passwordless.dev:443", "http://www.passwordless.dev")]
133+
[InlineData("https://www.passwordless.dev:443/foo/bar.html", "http://www.passwordless.dev:443/foo/bar.html")]
134+
[InlineData("https://www.passwordless.dev:443/foo/bar.html", "http://www.passwordless.dev:443/bar/foo.html")]
135+
[InlineData("https://www.passwordless.dev:443/foo/bar.html", "http://www.passwordless.dev/bar/foo.html")]
136+
[InlineData("https://www.passwordless.dev:443/foo/bar.html", "http://www.passwordless.dev")]
137+
[InlineData("ftp://www.passwordless.dev", "ftp://www.passwordless.dev:80")]
138+
[InlineData("ftp://www.passwordless.dev:8080", "ftp://www.passwordless.dev:8081")]
139+
[InlineData("https://127.0.0.1", "http://127.0.0.1")]
140+
[InlineData("https://localhost", "http://localhost")]
141+
[InlineData("https://127.0.0.1:80", "https://127.0.0.1:81")]
142+
[InlineData("http://localhost:80", "http://localhost:82")]
143+
[InlineData("http://127.0.0.1:443", "http://127.0.0.1:444")]
144+
[InlineData("http://localhost:443", "http://localhost:444")]
145+
[InlineData("android:apk-key-hash:Ea3dD4m7ccbwcw+a27/D547hfwYra2gKE4lIBbBjCTU", "android:apk-key-hash:Ae3dD4m7ccbwcw+a27/D547hfwYra2gKE4lIBbBjCTU")]
146+
[InlineData("lorem:ipsum:dolor", "lorem:dolor:ipsum")]
147+
[InlineData("lorem:/ipsum:4321", "lorem:/ipsum:4322")]
148+
[InlineData("lorem://ipsum:1234", "lorem://ipsum:1235")]
149+
[InlineData("lorem://ipsum:9876/sit", "lorem://ipsum:9877/sit")]
150+
[InlineData("foo://bar:321/path/", "foo://bar:322/path/")]
151+
[InlineData("foo://bar:321/path", "foo://bar:322/path")]
152+
[InlineData("https://[0:0:0:0:0:0:0:1]", "http://[0:0:0:0:0:0:0:1]")]
153+
[InlineData("https://[0:0:0:0:0:0:0:1]", "http://[0:0:0:0:0:0:0:1]:80")]
154+
[InlineData("http://[0:0:0:0:0:0:0:1]", "https://[0:0:0:0:0:0:0:1]")]
155+
[InlineData("http://[0:0:0:0:0:0:0:1]", "https://[0:0:0:0:0:0:0:1]:443")]
156+
public void TestAuthenticatorOriginsFail(string origin, string expectedOrigin)
157+
{
158+
var challenge = RandomGenerator.Default.GenerateBytes(128);
159+
var rp = origin;
160+
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
161+
var authData = new AuthenticatorData(
162+
SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(origin)),
163+
AuthenticatorFlags.UP | AuthenticatorFlags.AT,
164+
0,
165+
acd,
166+
null
167+
).ToByteArray();
168+
var clientDataJson = Encoding.UTF8.GetBytes(
169+
JsonConvert.SerializeObject
170+
(
171+
new
172+
{
173+
Type = "webauthn.create",
174+
Challenge = challenge,
175+
Origin = rp,
176+
}
177+
)
178+
);
179+
var rawResponse = new AuthenticatorAttestationRawResponse
180+
{
181+
Type = PublicKeyCredentialType.PublicKey,
182+
Id = new byte[] { 0xf1, 0xd0 },
183+
RawId = new byte[] { 0xf1, 0xd0 },
184+
Response = new AuthenticatorAttestationRawResponse.ResponseData()
185+
{
186+
AttestationObject = CBORObject.NewMap().Add("fmt", "none").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(),
187+
ClientDataJson = clientDataJson
188+
},
189+
};
190+
191+
var origChallenge = new CredentialCreateOptions
192+
{
193+
Attestation = AttestationConveyancePreference.Direct,
194+
AuthenticatorSelection = new AuthenticatorSelection
195+
{
196+
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
197+
RequireResidentKey = true,
198+
UserVerification = UserVerificationRequirement.Required,
199+
},
200+
Challenge = challenge,
201+
ErrorMessage = "",
202+
PubKeyCredParams = new List<PubKeyCredParam>()
203+
{
204+
new PubKeyCredParam
205+
{
206+
Alg = COSE.Algorithm.ES256,
207+
Type = PublicKeyCredentialType.PublicKey,
208+
}
209+
},
210+
Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
211+
Status = "ok",
212+
User = new Fido2User
213+
{
214+
Name = "testuser",
215+
Id = Encoding.UTF8.GetBytes("testuser"),
216+
DisplayName = "Test User",
217+
},
218+
Timeout = 60000,
219+
};
220+
221+
IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
222+
{
223+
return Task.FromResult(true);
224+
};
225+
226+
var lib = new Fido2(new Fido2Configuration()
227+
{
228+
ServerDomain = rp,
229+
ServerName = rp,
230+
Origin = expectedOrigin,
231+
});
232+
233+
var ex = Assert.ThrowsAsync<Fido2VerificationException>(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback));
234+
Assert.StartsWith("Fully qualified origin", ex.Result.Message);
235+
}
236+
18237
[Fact]
19238
public void TestAuthenticatorAttestationRawResponse()
20239
{

0 commit comments

Comments
 (0)