diff --git a/src/Core/Utilities/Fido2/Fido2DomainUtils.cs b/src/Core/Utilities/Fido2/Fido2DomainUtils.cs new file mode 100644 index 000000000..5f9eb8faf --- /dev/null +++ b/src/Core/Utilities/Fido2/Fido2DomainUtils.cs @@ -0,0 +1,37 @@ +using System.Text.RegularExpressions; + +namespace Bit.Core.Utilities.Fido2 +{ + public class Fido2DomainUtils + { + // TODO: This is a basic implementation of the domain validation logic, and is probably not correct. + // It doesn't support IP-adresses, and it doesn't follow the algorithm in the spec: + // https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to + public static bool IsValidRpId(string rpId, string origin) + { + if (rpId == null || origin == null) + { + return false; + } + + // TODO: DomainName doesn't like it when we give it a URL with a protocol or port + // So we remove the protocol and port here, while still supporting ipv6 shortform + // https is enforced in the client, so we don't need to worry about that here + var originWithoutProtocolOrPort = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?", "$2$4"); + + if (rpId == originWithoutProtocolOrPort) + { + return true; + } + + if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolOrPort, out var parsedOrgin)) + { + return false; + } + + return parsedOrgin.Tld == parsedRpId.Tld && + parsedOrgin.Domain == parsedRpId.Domain && + (parsedOrgin.SubDomain == parsedRpId.SubDomain || parsedOrgin.SubDomain.EndsWith(parsedRpId.SubDomain)); + } + } +} diff --git a/test/Core.Test/Utilities/Fido2/Fido2DomainUtilsTests.cs b/test/Core.Test/Utilities/Fido2/Fido2DomainUtilsTests.cs new file mode 100644 index 000000000..457c2a076 --- /dev/null +++ b/test/Core.Test/Utilities/Fido2/Fido2DomainUtilsTests.cs @@ -0,0 +1,40 @@ +using Bit.Core.Utilities.Fido2; +using Xunit; + +namespace Bit.Core.Test.Utilities.Fido2 +{ + public class Fido2DomainUtilsTests + { + [Theory] + // From https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to + // [InlineData("0.0.0.0", "0.0.0.0", true)] // IP-addresses not allowed by WebAuthn spec + // [InlineData("0x10203", "0.1.2.3", true)] + // [InlineData("[0::1]", "::1", true)] + [InlineData("example.com", "example.com", true)] + [InlineData("example.com", "example.com.", false)] + [InlineData("example.com.", "example.com", false)] + [InlineData("example.com", "www.example.com", true)] + [InlineData("com", "example.com", false)] + [InlineData("example", "example", true)] + [InlineData("compute.amazonaws.com", "example.compute.amazonaws.com", false)] + [InlineData("example.compute.amazonaws.com", "www.example.compute.amazonaws.com", false)] + [InlineData("amazonaws.com", "www.example.compute.amazonaws.com", false)] + [InlineData("amazonaws.com", "test.amazonaws.com", true)] + // Custom tests + [InlineData("sub.login.bitwarden.com", "https://login.bitwarden.com:1337", false)] + [InlineData("passwordless.dev", "https://login.bitwarden.com:1337", false)] + [InlineData("login.passwordless.dev", "https://login.bitwarden.com:1337", false)] + [InlineData("bitwarden", "localhost", false)] + [InlineData("bitwarden", "bitwarden", true)] + [InlineData("127.0.0.1", "127.0.0.1", false)] + [InlineData("localhost", "https://localhost:8080", true)] + [InlineData("bitwarden.com", "https://bitwarden.com", true)] + [InlineData("bitwarden.com", "https://login.bitwarden.com:1337", true)] + [InlineData("login.bitwarden.com", "https://login.bitwarden.com:1337", true)] + [InlineData("login.bitwarden.com", "https://sub.login.bitwarden.com:1337", true)] + public void ValidateRpId(string rpId, string origin, bool isValid) + { + Assert.Equal(isValid, Fido2DomainUtils.IsValidRpId(rpId, origin)); + } + } +}