├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ └── dotnetcore.yml ├── .gitignore ├── LICENSE ├── README.md ├── U2F.Core.sln ├── src ├── U2F.Core │ ├── Crypto │ │ ├── CryptoService.cs │ │ ├── ICryptoService.cs │ │ └── U2F.cs │ ├── Exceptions │ │ ├── U2fException.cs │ │ └── UnsupportedOperationException.cs │ ├── Models │ │ ├── AuthenticateResponse.cs │ │ ├── BaseModel.cs │ │ ├── ClientData.cs │ │ ├── DeviceRegistration.cs │ │ ├── RawAuthenticateResponse.cs │ │ ├── RawRegisterResponse.cs │ │ ├── RegisterResponse.cs │ │ ├── StartedAuthentication.cs │ │ └── StartedRegistration.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── U2F.Core.csproj │ ├── U2F.nuspec │ └── Utils │ │ ├── Asn1Helper.cs │ │ └── Utils.cs └── U2F.Demo │ └── U2F.Demo │ ├── Controllers │ ├── ProfileController.cs │ └── U2FController.cs │ ├── DataStore │ └── U2FContext.cs │ ├── Models │ ├── AuthenticationRequest.cs │ ├── Device.cs │ ├── ServerChallenge.cs │ ├── ServerRegisterResponse.cs │ └── User.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Services │ ├── IMembershipService.cs │ └── MembershipService.cs │ ├── Startup.cs │ ├── U2F.Demo.csproj │ ├── ViewModel │ ├── CompleteLoginViewModel.cs │ ├── CompleteRegisterViewModel.cs │ ├── RegisterViewModel.cs │ ├── StartLoginViewModel.cs │ └── StartRegisterViewModel.cs │ ├── Views │ ├── Profile │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ └── _Layout.cshtml │ ├── U2F │ │ ├── FinishLogin.cshtml │ │ ├── FinishRegister.cshtml │ │ ├── Index.cshtml │ │ ├── Login.cshtml │ │ ├── Register.cshtml │ │ └── SucessfulRegister.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── appsettings.json │ ├── libman.json │ └── wwwroot │ ├── css │ ├── site.css │ └── site.min.css │ ├── favicon.ico │ ├── images │ ├── banner1.svg │ ├── banner2.svg │ ├── banner3.svg │ └── banner4.svg │ ├── js │ ├── site.js │ ├── site.min.js │ ├── u2f-api-1.1.js │ ├── u2f-api.js │ └── u2f-api.min.js │ └── lib │ ├── bootstrap │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── jquery-validation-unobtrusive │ ├── LICENSE.txt │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map └── tests └── UnitTests ├── CryptoServiceTests.cs ├── Properties └── AssemblyInfo.cs ├── TestConstants.cs ├── U2FTests.cs └── UnitTests.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '36 14 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'csharp', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_h.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *_wpftmp.csproj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush personal settings 296 | .cr/personal 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | # Local History for Visual Studio 334 | .localhistory/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bryce Foster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=brucedog_U2F_Core&metric=alert_status)](https://sonarcloud.io/dashboard?id=brucedog_U2F_Core) 2 | ![.NET Core](https://github.com/brucedog/U2F_Core/workflows/.NET%20Core/badge.svg) 3 | [![NuGet](https://img.shields.io/nuget/dt/U2F.Core.svg)](https://www.nuget.org/packages/U2F.Core) 4 | 5 | ## .NET Universal 2nd Factor (U2F) 6 | 7 | U2F is being depratcated in favor of FIDO2. A dotnet version can be found here https://github.com/passwordless-lib/fido2-net-lib 8 | This repository provides functionality for working with the server side aspects of the U2F protocol on .NET core framework. 9 | 10 | ## Working Demo Site 11 | https://bfoster.me/U2FDemo/DemoHome 12 | 13 | ## NuGet page 14 | https://www.nuget.org/packages/U2F.Core 15 | 16 | ## Original source JAVA library is located below 17 | https://github.com/Yubico/java-u2flib-server 18 | 19 | ## Useful links 20 | - https://www.yubico.com/products/yubikey-hardware/fido-u2f-security-key/ 21 | - https://developers.yubico.com/U2F/ 22 | - https://fidoalliance.org/ 23 | 24 | ## License 25 | 26 | MIT License 27 | 28 | Copyright (c) 2021 Bryce Foster 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | -------------------------------------------------------------------------------- /U2F.Core.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30309.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C4989C64-8065-47EF-BD1D-D5BAE0EC9286}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A5DF56E0-1ED7-4885-8E52-0108E65C2CC1}" 9 | ProjectSection(SolutionItems) = preProject 10 | .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml 11 | README.md = README.md 12 | src\U2F.Core\U2F.nuspec = src\U2F.Core\U2F.nuspec 13 | U2FCore.pfx = U2FCore.pfx 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C22ABDC2-97E5-4C12-802C-0877F09DEE03}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DemoWebSite", "DemoWebSite", "{FD9C495E-9ED2-4B8A-A171-F0BFCBD2C7F6}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "U2F.Core", "src\U2F.Core\U2F.Core.csproj", "{78B21560-5F9E-4494-83D1-239BDF7D57F6}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{6D8A572D-9F40-4423-B87A-0243DFEE57E0}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "U2F.Demo", "src\U2F.Demo\U2F.Demo\U2F.Demo.csproj", "{BD4FAEA5-3290-4126-83A5-79D8D8871342}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {78B21560-5F9E-4494-83D1-239BDF7D57F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {78B21560-5F9E-4494-83D1-239BDF7D57F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {78B21560-5F9E-4494-83D1-239BDF7D57F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {78B21560-5F9E-4494-83D1-239BDF7D57F6}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {6D8A572D-9F40-4423-B87A-0243DFEE57E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {6D8A572D-9F40-4423-B87A-0243DFEE57E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {6D8A572D-9F40-4423-B87A-0243DFEE57E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {6D8A572D-9F40-4423-B87A-0243DFEE57E0}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {BD4FAEA5-3290-4126-83A5-79D8D8871342}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {BD4FAEA5-3290-4126-83A5-79D8D8871342}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {BD4FAEA5-3290-4126-83A5-79D8D8871342}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {BD4FAEA5-3290-4126-83A5-79D8D8871342}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {FD9C495E-9ED2-4B8A-A171-F0BFCBD2C7F6} = {C4989C64-8065-47EF-BD1D-D5BAE0EC9286} 50 | {78B21560-5F9E-4494-83D1-239BDF7D57F6} = {C4989C64-8065-47EF-BD1D-D5BAE0EC9286} 51 | {6D8A572D-9F40-4423-B87A-0243DFEE57E0} = {C22ABDC2-97E5-4C12-802C-0877F09DEE03} 52 | {BD4FAEA5-3290-4126-83A5-79D8D8871342} = {FD9C495E-9ED2-4B8A-A171-F0BFCBD2C7F6} 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {828FBD32-2481-43BF-A06A-D3AA8F24F986} 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /src/U2F.Core/Crypto/CryptoService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Cryptography; 4 | using System.Security.Cryptography.X509Certificates; 5 | using U2F.Core.Exceptions; 6 | using U2F.Core.Utils; 7 | 8 | namespace U2F.Core.Crypto 9 | { 10 | public sealed class CryptoService : IDisposable, ICryptoService 11 | { 12 | private SHA256 _sha256 = SHA256.Create(); 13 | private RandomNumberGenerator _randomNumberGenerator; 14 | 15 | public CryptoService() 16 | { 17 | _sha256.Initialize(); 18 | _randomNumberGenerator = RandomNumberGenerator.Create(); 19 | } 20 | 21 | /// 22 | /// Validates signed bytes against a signature using the raw bytes of the EC public key. 23 | /// 24 | /// Raw bytes of the EC public key 25 | /// Bytes signed with private key 26 | /// signature to compare against 27 | /// true if valid else throws an exception that the signature does not match the signed bytes. 28 | public bool CheckSignature(byte[] publicKey, byte[] signedBytes, byte[] signature) 29 | { 30 | try 31 | { 32 | var cngPubKey = ConvertPublicKey(publicKey); 33 | 34 | if (cngPubKey == null 35 | || signedBytes == null || signedBytes.Length == 0 36 | || signature == null || signature.Length == 0) 37 | throw new U2fException(U2fException.InvalidArguments); 38 | 39 | bool result = VerifySignedBytesAgainstSignature(cngPubKey, signedBytes, signature); 40 | 41 | if (!result) 42 | throw new U2fException(U2fException.SignatureError); 43 | 44 | return true; 45 | } 46 | catch (Exception exception) 47 | { 48 | throw new U2fException(U2fException.SignatureError, exception); 49 | } 50 | } 51 | 52 | /// 53 | /// Verifies the signed bytes against the signature using the EC public key in the Cert provided. 54 | /// 55 | /// Cert containing EC public key 56 | /// Bytes signed with private key 57 | /// signature to compare against 58 | /// true if valid else throws an exception that the signature does not match the signed bytes. 59 | public bool CheckSignature(X509Certificate2 certificate, byte[] signedBytes, byte[] signature) 60 | { 61 | try 62 | { 63 | if (certificate == null 64 | || signedBytes == null || signedBytes.Length == 0 65 | || signature == null || signature.Length == 0) 66 | throw new U2fException(U2fException.InvalidArguments); 67 | 68 | bool result = VerifySignedBytesAgainstSignature(certificate.GetECDsaPublicKey(), signedBytes, signature); 69 | 70 | if (!result) 71 | throw new U2fException(U2fException.SignatureError); 72 | 73 | return true; 74 | } 75 | catch (Exception exception) 76 | { 77 | throw new U2fException(U2fException.SignatureError, exception); 78 | } 79 | } 80 | 81 | public byte[] GenerateChallenge() 82 | { 83 | byte[] randomBytes = new byte[32]; 84 | _randomNumberGenerator.GetBytes(randomBytes); 85 | 86 | return randomBytes; 87 | } 88 | 89 | public byte[] Hash(string stringToHash) 90 | { 91 | return Hash(stringToHash.GetBytes()); 92 | } 93 | 94 | public byte[] Hash(byte[] bytes) 95 | { 96 | try 97 | { 98 | byte[] hash = _sha256.ComputeHash(bytes); 99 | 100 | return hash; 101 | } 102 | catch (Exception exception) 103 | { 104 | throw new UnsupportedOperationException(UnsupportedOperationException.Sha256Exception, exception); 105 | } 106 | } 107 | 108 | /// 109 | /// Simplified method that validates signed bytes against the signature using a EC public key. 110 | /// 111 | private bool VerifySignedBytesAgainstSignature(ECDsa publicKey, byte[] signedBytes, byte[] signature) 112 | { 113 | bool result = publicKey.VerifyData(signedBytes, signature.FromAsn1Signature(), HashAlgorithmName.SHA256); 114 | return result; 115 | } 116 | 117 | /// 118 | /// Coverts byte array into a P256 EC Public key 119 | /// 120 | /// 121 | /// NIST P256 Public key 122 | private ECDsa ConvertPublicKey(byte[] rawData) 123 | { 124 | if (rawData == null || rawData.Length != 65) 125 | throw new U2fException(U2fException.InvalidArguments); 126 | 127 | var pubKeyX = rawData.Skip(1).Take(32).ToArray(); 128 | var pubKeyY = rawData.Skip(33).ToArray(); 129 | 130 | return ECDsa.Create(new ECParameters 131 | { 132 | Curve = ECCurve.NamedCurves.nistP256, 133 | Q = new ECPoint 134 | { 135 | X = pubKeyX, 136 | Y = pubKeyY 137 | } 138 | }); 139 | } 140 | 141 | public void Dispose() 142 | { 143 | _sha256.Dispose(); 144 | _sha256 = null; 145 | _randomNumberGenerator.Dispose(); 146 | _randomNumberGenerator = null; 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/U2F.Core/Crypto/ICryptoService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Security.Cryptography.X509Certificates; 3 | 4 | namespace U2F.Core.Crypto 5 | { 6 | public interface ICryptoService 7 | { 8 | /// 9 | /// Generated a securely generated byte array 10 | /// 11 | /// Securely generated random bytes 12 | byte[] GenerateChallenge(); 13 | 14 | /// 15 | /// Checks the signature. 16 | /// 17 | /// The certificate with a public key. 18 | /// The signed data. 19 | /// The signature. 20 | /// 21 | bool CheckSignature(X509Certificate2 certificate, byte[] signedBytes, byte[] signature); 22 | 23 | 24 | /// 25 | /// Checks the signature. 26 | /// 27 | /// The raw public key. 28 | /// The signed data. 29 | /// The signature. 30 | /// 31 | bool CheckSignature(byte[] publicKey, byte[] signedBytes, byte[] signature); 32 | 33 | /// 34 | /// Hashes the specified bytes. 35 | /// 36 | /// The bytes. 37 | /// byte array of hashed byte array 38 | byte[] Hash(byte[] bytes); 39 | 40 | /// 41 | /// Hashes the specified string with sha256. 42 | /// 43 | /// The string to be hased. 44 | /// byte array of hashed string 45 | byte[] Hash(string stringToHash); 46 | } 47 | } -------------------------------------------------------------------------------- /src/U2F.Core/Crypto/U2F.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using U2F.Core.Models; 3 | using U2F.Core.Utils; 4 | 5 | namespace U2F.Core.Crypto 6 | { 7 | public static class U2F 8 | { 9 | public static ICryptoService Crypto { get; set; } = new CryptoService(); 10 | public const string U2FVersion = "U2F_V2"; 11 | private const string AuthenticateTyp = "navigator.id.getAssertion"; 12 | private const string RegisterType = "navigator.id.finishEnrollment"; 13 | 14 | /// 15 | /// Initiates the registration of a device. 16 | /// 17 | /// ppId the U2F AppID. Set this to the Web Origin of the login page, unless you need to support logging in from multiple Web Origins. 18 | /// a StartedRegistration, which should be sent to the client and temporary saved by the server. 19 | public static StartedRegistration StartRegistration(string appId) 20 | { 21 | byte[] challenge = Crypto.GenerateChallenge(); 22 | string challengeBase64 = challenge.ByteArrayToBase64String(); 23 | 24 | return new StartedRegistration(challengeBase64, appId); 25 | } 26 | 27 | /// 28 | /// Finishes a previously started registration. 29 | /// 30 | /// started registration response. 31 | /// tokenResponse the response from the token/client. 32 | /// A list of valid facets to verify against. (note: optional) 33 | /// a DeviceRegistration object, holding information about the registered device. Servers should persist this. 34 | public static DeviceRegistration FinishRegistration(StartedRegistration startedRegistration, 35 | RegisterResponse tokenResponse, HashSet facets = null) 36 | { 37 | ClientData clientData = tokenResponse.GetClientData(); 38 | clientData.CheckContent(RegisterType, startedRegistration.Challenge, facets); 39 | 40 | RawRegisterResponse rawRegisterResponse = RawRegisterResponse.FromBase64(tokenResponse.RegistrationData); 41 | rawRegisterResponse.CheckSignature(startedRegistration.AppId, clientData.AsJson()); 42 | 43 | return rawRegisterResponse.CreateDevice(); 44 | } 45 | 46 | /// 47 | /// Initiates the authentication process. 48 | /// 49 | /// appId the U2F AppID. Set this to the Web Origin of the login page, unless you need to support logging in from multiple Web Origins. 50 | /// the DeviceRegistration for which to initiate authentication. 51 | /// a StartedAuthentication which should be sent to the client and temporary saved by the server. 52 | public static StartedAuthentication StartAuthentication(string appId, DeviceRegistration deviceRegistration) 53 | { 54 | byte[] challenge = Crypto.GenerateChallenge(); 55 | return StartAuthentication(appId, deviceRegistration, challenge); 56 | } 57 | 58 | /// 59 | /// Initiates the authentication process. 60 | /// 61 | /// appId the U2F AppID. Set this to the Web Origin of the login page, unless you need to support logging in from multiple Web Origins. 62 | /// the DeviceRegistration for which to initiate authentication. 63 | /// random generated byte[] from ICryptoService. 64 | /// a StartedAuthentication which should be sent to the client and temporary saved by the server. 65 | public static StartedAuthentication StartAuthentication(string appId, DeviceRegistration deviceRegistration, byte[] challenge) 66 | { 67 | return new StartedAuthentication( 68 | challenge.ByteArrayToBase64String(), 69 | appId, 70 | deviceRegistration.KeyHandle.ByteArrayToBase64String()); 71 | } 72 | 73 | /// 74 | /// Finishes a previously started authentication. 75 | /// 76 | /// The authentication the device started 77 | /// response the response from the token/client. 78 | /// 79 | /// A list of valid facets to verify against. (note: optional) 80 | /// the new value of the DeviceRegistration's counter 81 | public static uint FinishAuthentication(StartedAuthentication startedAuthentication, 82 | AuthenticateResponse response, 83 | DeviceRegistration deviceRegistration, 84 | HashSet facets = null) 85 | { 86 | ClientData clientData = response.GetClientData(); 87 | clientData.CheckContent(AuthenticateTyp, startedAuthentication.Challenge, facets); 88 | 89 | RawAuthenticateResponse authenticateResponse = RawAuthenticateResponse.FromBase64(response.SignatureData); 90 | authenticateResponse.CheckSignature(startedAuthentication.AppId, clientData.AsJson(), deviceRegistration.PublicKey); 91 | authenticateResponse.CheckUserPresence(); 92 | 93 | return deviceRegistration.CheckAndUpdateCounter(authenticateResponse.Counter); 94 | } 95 | 96 | /// 97 | /// Generates a base 64 encode string 98 | /// 99 | /// base 64 encode string 100 | public static string GenerateChallenge() 101 | { 102 | byte[] challenge = Crypto.GenerateChallenge(); 103 | string challengeBase64 = challenge.ByteArrayToBase64String(); 104 | 105 | return challengeBase64; 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/U2F.Core/Exceptions/U2fException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace U2F.Core.Exceptions 4 | { 5 | public class U2fException : Exception 6 | { 7 | public const string SignatureError = "Error when verifying signature"; 8 | public const string ErrorDecodingPublicKey = "Error when decoding public key"; 9 | public const string InvalidArguments = "The arguments passed the were not valid"; 10 | 11 | public U2fException(string message, Exception innerException = null) : base(message, innerException) 12 | { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/U2F.Core/Exceptions/UnsupportedOperationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace U2F.Core.Exceptions 4 | { 5 | public class UnsupportedOperationException : Exception 6 | { 7 | public const string Sha256Exception = "Error when computing SHA-256"; 8 | 9 | public UnsupportedOperationException(string message, Exception innerException) : base(message, innerException) 10 | { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/U2F.Core/Models/AuthenticateResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace U2F.Core.Models 5 | { 6 | public class AuthenticateResponse : BaseModel 7 | { 8 | private readonly ClientData _clientDataRef; 9 | 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The client data. 14 | /// The signature data. 15 | /// The key handle. 16 | public AuthenticateResponse(string clientData, string signatureData, string keyHandle) 17 | { 18 | if(string.IsNullOrWhiteSpace(clientData) 19 | || string.IsNullOrWhiteSpace(signatureData) 20 | || string.IsNullOrWhiteSpace(keyHandle)) 21 | throw new ArgumentException("Invalid argument(s) were being passed."); 22 | 23 | ClientData = clientData; 24 | SignatureData = signatureData; 25 | KeyHandle = keyHandle; 26 | _clientDataRef = new ClientData(ClientData); 27 | } 28 | 29 | public ClientData GetClientData() 30 | { 31 | return _clientDataRef; 32 | } 33 | 34 | public string GetRequestId() 35 | { 36 | return GetClientData().Challenge; 37 | } 38 | 39 | /// 40 | /// Gets the signature data. 41 | /// websafe-base64(raw response from U2F device) 42 | /// 43 | /// 44 | /// The signature data. 45 | /// 46 | public string SignatureData { get; private set; } 47 | 48 | /// 49 | /// Gets the Client data. 50 | /// 51 | /// 52 | /// The Client data. 53 | /// 54 | public string ClientData { get; private set; } 55 | 56 | /// 57 | /// keyHandle originally passed 58 | /// 59 | /// 60 | /// The key handle. 61 | /// 62 | public string KeyHandle { get; private set; } 63 | 64 | public override int GetHashCode() 65 | { 66 | int hash = ClientData.Sum(c => c + 31); 67 | hash += SignatureData.Sum(c => c + 31); 68 | hash += KeyHandle.Sum(c => c + 31); 69 | 70 | return hash; 71 | } 72 | 73 | public override bool Equals(object obj) 74 | { 75 | if (!(obj is AuthenticateResponse)) 76 | return false; 77 | if (this == obj) 78 | return true; 79 | if (GetType() != obj.GetType()) 80 | return false; 81 | 82 | AuthenticateResponse other = (AuthenticateResponse)obj; 83 | 84 | if (ClientData == null) 85 | { 86 | if (other.ClientData != null) 87 | return false; 88 | } 89 | else if (!ClientData.Equals(other.ClientData)) 90 | return false; 91 | 92 | if (KeyHandle == null) 93 | { 94 | if (other.KeyHandle != null) 95 | return false; 96 | } 97 | else if (!KeyHandle.Equals(other.KeyHandle)) 98 | return false; 99 | 100 | if (SignatureData == null) 101 | { 102 | if (other.SignatureData != null) 103 | return false; 104 | } 105 | else if (!SignatureData.Equals(other.SignatureData)) 106 | return false; 107 | 108 | return true; 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/U2F.Core/Models/BaseModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace U2F.Core.Models 5 | { 6 | public abstract class BaseModel 7 | { 8 | public String ToJson() 9 | { 10 | return JsonConvert.SerializeObject(this); 11 | } 12 | 13 | public static T FromJson(String json) 14 | { 15 | return JsonConvert.DeserializeObject(json); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/U2F.Core/Models/ClientData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json.Linq; 4 | using U2F.Core.Exceptions; 5 | using U2F.Core.Utils; 6 | 7 | namespace U2F.Core.Models 8 | { 9 | public class ClientData 10 | { 11 | private const string TypeParam = "typ"; 12 | private const string ChallengeParam = "challenge"; 13 | private const string OriginParam = "origin"; 14 | 15 | public string Type { get; private set; } 16 | public string Challenge { get; private set; } 17 | public string Origin { get; private set; } 18 | public string RawClientData { get; private set; } 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The client data. 24 | /// ClientData has wrong format 25 | public ClientData(string clientData) 26 | { 27 | try 28 | { 29 | if(string.IsNullOrWhiteSpace(clientData)) 30 | throw new U2fException(U2fException.InvalidArguments); 31 | 32 | RawClientData = clientData.Base64StringToByteArray().GetString(); 33 | 34 | JObject element = JObject.Parse(RawClientData); 35 | if (element == null) 36 | throw new U2fException("ClientData has wrong format"); 37 | 38 | JToken theType, theChallenge, theOrgin; 39 | if (!element.TryGetValue(TypeParam, out theType)) 40 | throw new U2fException("Bad clientData: missing 'typ' param"); 41 | if (!element.TryGetValue(ChallengeParam, out theChallenge)) 42 | throw new U2fException("Bad clientData: missing 'challenge' param"); 43 | 44 | Type = theType.ToString(); 45 | Challenge = theChallenge.ToString(); 46 | if (element.TryGetValue(OriginParam, out theOrgin)) 47 | Origin = theOrgin.ToString(); 48 | } 49 | catch (Exception exception) 50 | { 51 | throw new U2fException("Invalid clientData format.: " + exception.Message); 52 | } 53 | } 54 | 55 | public void CheckContent(string type, string challenge, HashSet facets) 56 | { 57 | if (!type.Equals(Type) || string.IsNullOrWhiteSpace(type)) 58 | { 59 | throw new U2fException("Bad clientData: bad type " + type); 60 | } 61 | if (!challenge.Equals(Challenge) || string.IsNullOrWhiteSpace(challenge)) 62 | { 63 | throw new U2fException("Wrong challenge signed in clientData"); 64 | } 65 | if (facets != null) 66 | { 67 | VerifyOrigin(Origin, CanonicalizeOrigins(facets)); 68 | } 69 | } 70 | 71 | public string AsJson() 72 | { 73 | return RawClientData; 74 | } 75 | 76 | private void VerifyOrigin(string origin, HashSet allowedOrigins) 77 | { 78 | if (!allowedOrigins.Contains(CanonicalizeOrigin(origin))) 79 | { 80 | throw new UriFormatException(origin + " is not a recognized home origin for this backend"); 81 | } 82 | } 83 | 84 | private HashSet CanonicalizeOrigins(HashSet origins) 85 | { 86 | HashSet result = new HashSet(); 87 | foreach (string orgin in origins) 88 | { 89 | result.Add(CanonicalizeOrigin(orgin)); 90 | } 91 | return result; 92 | } 93 | 94 | private string CanonicalizeOrigin(string url) 95 | { 96 | if (string.IsNullOrWhiteSpace(url)) 97 | throw new U2fException(U2fException.InvalidArguments); 98 | try 99 | { 100 | Uri uri = new Uri(url); 101 | if (string.IsNullOrWhiteSpace(uri.Authority)) 102 | return url; 103 | 104 | return uri.Scheme + "://" + uri.Authority; 105 | } 106 | catch (UriFormatException e) 107 | { 108 | throw new UriFormatException("specified bad origin", e); 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/U2F.Core/Models/DeviceRegistration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography.X509Certificates; 3 | using Newtonsoft.Json; 4 | using System.Linq; 5 | using U2F.Core.Exceptions; 6 | 7 | namespace U2F.Core.Models 8 | { 9 | public class DeviceRegistration : BaseModel 10 | { 11 | public const uint InitialCounterValue = 0; 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The key handle. 17 | /// The public key. 18 | /// The attestation cert. 19 | /// The counter. 20 | /// 21 | /// Invalid attestation certificate 22 | public DeviceRegistration(byte[] keyHandle, byte[] publicKey, byte[] attestationCert, uint counter, bool isCompromised = false) 23 | { 24 | KeyHandle = keyHandle; 25 | PublicKey = publicKey; 26 | Counter = counter; 27 | IsCompromised = isCompromised; 28 | 29 | try 30 | { 31 | AttestationCert = attestationCert; 32 | } 33 | catch (Exception exception) 34 | { 35 | throw new U2fException("Malformed attestation certificate", exception); 36 | } 37 | } 38 | 39 | /// 40 | /// Gets if the device has been compromised. 41 | /// 42 | /// 43 | /// If the device has been compromised. 44 | /// 45 | public bool IsCompromised { get; private set; } 46 | 47 | /// 48 | /// Gets the key handle. 49 | /// 50 | /// 51 | /// The key handle. 52 | /// 53 | public byte[] KeyHandle { get; private set; } 54 | 55 | /// 56 | /// Gets the public key. 57 | /// 58 | /// 59 | /// The public key. 60 | /// 61 | public byte[] PublicKey { get; private set; } 62 | 63 | /// 64 | /// Gets the attestation cert. 65 | /// 66 | /// 67 | /// The attestation cert. 68 | /// 69 | public byte[] AttestationCert { get; private set; } 70 | 71 | /// 72 | /// Usage counter from the device, this should be incremented by 1 every use. 73 | /// 74 | /// 75 | /// Number of device uses. 76 | /// 77 | public uint Counter { get; private set; } 78 | 79 | public X509Certificate GetAttestationCertificate() 80 | { 81 | if (AttestationCert == null) 82 | throw new U2fException("Missing Attestation Certificate."); 83 | 84 | return new X509Certificate(AttestationCert); 85 | } 86 | 87 | /// 88 | /// To the json with out attestation cert. 89 | /// 90 | /// 91 | public string ToJsonWithOutAttestionCert() 92 | { 93 | return JsonConvert.SerializeObject(new DeviceWithoutCertificate(KeyHandle, PublicKey, Counter, IsCompromised)); 94 | } 95 | 96 | /// 97 | /// Checks the and increment counter. 98 | /// 99 | /// The client counter. 100 | /// Counter value smaller than expected! 101 | /// device counter 102 | public uint CheckAndUpdateCounter(uint clientCounter) 103 | { 104 | if (clientCounter <= Counter) 105 | { 106 | IsCompromised = true; 107 | throw new U2fException("Counter value smaller than expected!"); 108 | } 109 | Counter = clientCounter; 110 | return Counter; 111 | } 112 | 113 | public override int GetHashCode() 114 | { 115 | int hash = PublicKey.Sum(b => b + 31); 116 | hash += AttestationCert.Sum(b => b + 31); 117 | hash += KeyHandle.Sum(b => b + 31); 118 | 119 | return hash; 120 | } 121 | 122 | public override bool Equals(object obj) 123 | { 124 | if (!(obj is DeviceRegistration)) 125 | return false; 126 | 127 | DeviceRegistration other = (DeviceRegistration)obj; 128 | 129 | return KeyHandle.SequenceEqual(other.KeyHandle) 130 | && PublicKey.SequenceEqual(other.PublicKey) 131 | && AttestationCert.SequenceEqual(other.AttestationCert) 132 | && (IsCompromised == other.IsCompromised); 133 | } 134 | } 135 | 136 | internal class DeviceWithoutCertificate 137 | { 138 | internal DeviceWithoutCertificate(byte[] keyHandle, byte[] publicKey, uint counter, bool isCompromised) 139 | { 140 | KeyHandle = keyHandle; 141 | PublicKey = publicKey; 142 | Counter = counter; 143 | IsCompromised = isCompromised; 144 | } 145 | 146 | public bool IsCompromised { get; set; } 147 | 148 | public byte[] PublicKey { get; private set; } 149 | 150 | public byte[] KeyHandle { get; private set; } 151 | 152 | public uint Counter { get; private set; } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/U2F.Core/Models/RawAuthenticateResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography.X509Certificates; 6 | using U2F.Core.Exceptions; 7 | using U2F.Core.Utils; 8 | 9 | namespace U2F.Core.Models 10 | { 11 | public class RawAuthenticateResponse 12 | { 13 | private const byte UserPresentFlag = 0x01; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The user presence. 19 | /// The counter. 20 | /// The signature. 21 | public RawAuthenticateResponse(byte userPresence, uint counter, byte[] signature) 22 | { 23 | UserPresence = userPresence; 24 | Counter = counter; 25 | Signature = signature; 26 | } 27 | 28 | /// 29 | /// Gets the user presence. 30 | /// Bit 0 is set to 1, which means that user presence was verified. (This 31 | /// version of the protocol doesn't specify a way to request authentication 32 | /// responses without requiring user presence.) A different value of bit 0, as 33 | /// well as bits 1 through 7, are reserved for future use. The values of bit 1 34 | /// through 7 SHOULD be 0 35 | /// 36 | /// 37 | /// The user presence. 38 | /// 39 | public byte UserPresence { get; private set; } 40 | 41 | /// 42 | /// Gets the counter. 43 | /// This is the big-endian representation of a counter value that the U2F device 44 | /// increments every time it performs an authentication operation. 45 | /// 46 | /// 47 | /// The counter. 48 | /// 49 | public uint Counter { get; private set; } 50 | 51 | /// 52 | /// Gets the signature. 53 | /// This is a ECDSA signature (on P-256) 54 | /// 55 | /// 56 | /// The signature. 57 | /// 58 | public byte[] Signature { get; private set; } 59 | 60 | /// 61 | /// From the base64. 62 | /// 63 | /// The raw data base64. 64 | /// the raw auth response 65 | public static RawAuthenticateResponse FromBase64(string rawDataBase64) 66 | { 67 | byte[] bytes = rawDataBase64.Base64StringToByteArray(); 68 | 69 | Stream stream = new MemoryStream(bytes); 70 | BinaryReader binaryReader = new BinaryReader(stream); 71 | 72 | byte userPresence = binaryReader.ReadByte(); 73 | byte[] counterBytes = binaryReader.ReadBytes(4); 74 | 75 | //counter has to be reversed if its little endian encoded 76 | if (BitConverter.IsLittleEndian) 77 | Array.Reverse(counterBytes); 78 | 79 | uint counter = BitConverter.ToUInt32(counterBytes, 0); 80 | 81 | long size = binaryReader.BaseStream.Length - binaryReader.BaseStream.Position; 82 | byte[] signature = binaryReader.ReadBytes((int)size); 83 | 84 | try 85 | { 86 | return new RawAuthenticateResponse( 87 | userPresence, 88 | counter, 89 | signature); 90 | } 91 | finally 92 | { 93 | stream.Dispose(); 94 | binaryReader.Dispose(); 95 | } 96 | } 97 | 98 | /// 99 | /// Checks the signature. 100 | /// 101 | /// The application identifier. 102 | /// The client data. 103 | /// The public key. 104 | public void CheckSignature(string appId, string clientData, byte[] publicKey) 105 | { 106 | byte[] signedBytes = PackBytesToSign( 107 | Crypto.U2F.Crypto.Hash(appId), 108 | UserPresence, 109 | Counter, 110 | Crypto.U2F.Crypto.Hash(clientData)); 111 | 112 | Crypto.U2F.Crypto.CheckSignature( 113 | publicKey, 114 | signedBytes, 115 | Signature); 116 | } 117 | 118 | /// 119 | /// Packs the bytes to sign. 120 | /// 121 | /// The application identifier hash. 122 | /// The user presence. 123 | /// The counter. 124 | /// The challenge hash. 125 | /// 126 | public byte[] PackBytesToSign(byte[] appIdHash, byte userPresence, uint counter, byte[] challengeHash) 127 | { 128 | // covert the counter to a byte array in case the int is to big for a single byte 129 | byte[] counterBytes = BitConverter.GetBytes(counter); 130 | 131 | //counter has to be reversed if its little endian encoded 132 | if (BitConverter.IsLittleEndian) 133 | Array.Reverse(counterBytes); 134 | 135 | List someBytes = new List(); 136 | someBytes.AddRange(appIdHash); 137 | someBytes.Add(userPresence); 138 | someBytes.AddRange(counterBytes); 139 | someBytes.AddRange(challengeHash); 140 | 141 | return someBytes.ToArray(); 142 | } 143 | 144 | public void CheckUserPresence() 145 | { 146 | if (UserPresence != UserPresentFlag) 147 | { 148 | throw new U2fException("User presence invalid during authentication"); 149 | } 150 | } 151 | 152 | public override int GetHashCode() 153 | { 154 | return 23 + Signature.Sum(b => b + 31 + (int)Counter + UserPresence); 155 | } 156 | 157 | public override bool Equals(object obj) 158 | { 159 | if (!(obj is RawAuthenticateResponse)) 160 | return false; 161 | if (this == obj) 162 | return true; 163 | if (GetType() != obj.GetType()) 164 | return false; 165 | RawAuthenticateResponse other = (RawAuthenticateResponse)obj; 166 | if (Counter != other.Counter) 167 | return false; 168 | 169 | if (!Signature.SequenceEqual(other.Signature)) 170 | return false; 171 | return UserPresence == other.UserPresence; 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /src/U2F.Core/Models/RawRegisterResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography.X509Certificates; 6 | using U2F.Core.Exceptions; 7 | using U2F.Core.Utils; 8 | 9 | namespace U2F.Core.Models 10 | { 11 | public class RawRegisterResponse 12 | { 13 | private const byte RegistrationReservedByteValue = 0x05; 14 | private const byte RegistrationSignedReservedByteValue = 0x00; 15 | 16 | // The (uncompressed) x,y-representation of a curve point on the P-256 NIST elliptic curve. 17 | private readonly byte[] _userPublicKey; 18 | 19 | // A handle that allows the U2F token to identify the generated key pair. 20 | private readonly byte[] _keyHandle; 21 | 22 | private readonly X509Certificate2 _attestationCertificate; 23 | 24 | // A ECDSA signature (on P-256) 25 | private readonly byte[] _signature; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// The user public key. 31 | /// The key handle. 32 | /// The attestation certificate. 33 | /// The signature. 34 | public RawRegisterResponse(byte[] userPublicKey, byte[] keyHandle, 35 | X509Certificate2 attestationCertificate, byte[] signature) 36 | { 37 | _userPublicKey = userPublicKey; 38 | _keyHandle = keyHandle; 39 | _attestationCertificate = attestationCertificate; 40 | _signature = signature; 41 | } 42 | 43 | /// 44 | /// Converts string response into RawRegisterResponse object 45 | /// 46 | /// raw string from client 47 | /// RawRegisterResponse object 48 | public static RawRegisterResponse FromBase64(string rawDataBase64) 49 | { 50 | if(string.IsNullOrWhiteSpace(rawDataBase64)) 51 | throw new ArgumentException("Invalid argument were being passed."); 52 | 53 | byte[] bytes = rawDataBase64.Base64StringToByteArray(); 54 | 55 | Stream stream = new MemoryStream(bytes); 56 | BinaryReader binaryReader = new BinaryReader(stream); 57 | 58 | try 59 | { 60 | byte reservedByte = binaryReader.ReadByte(); 61 | if (reservedByte != RegistrationReservedByteValue) 62 | { 63 | throw new U2fException($"Incorrect value of reserved byte. Expected: {RegistrationReservedByteValue}. Was: {reservedByte}"); 64 | } 65 | 66 | byte[] publicKey = binaryReader.ReadBytes(65); 67 | byte keyHandleLength = binaryReader.ReadByte(); 68 | byte[] keyHandle = binaryReader.ReadBytes(keyHandleLength); 69 | 70 | List rawCertBytes = new List(); 71 | while (binaryReader.BaseStream.Length - binaryReader.BaseStream.Position > 0) 72 | { 73 | byte end = binaryReader.ReadByte(); 74 | 75 | rawCertBytes.Add(end); 76 | } 77 | 78 | X509Certificate2 attestationCertificate = new X509Certificate2(rawCertBytes.ToArray()); 79 | 80 | // reserved byte + public key length + key handle length + cert data length 81 | int size = 1 + 65 + 1 + keyHandle.Length + attestationCertificate.RawData.Length; 82 | 83 | byte[] signature = bytes.Skip(size).Take(bytes.Length - size).ToArray(); 84 | 85 | RawRegisterResponse rawRegisterResponse = new RawRegisterResponse( 86 | publicKey, 87 | keyHandle, 88 | attestationCertificate, 89 | signature); 90 | 91 | return rawRegisterResponse; 92 | } 93 | catch (Exception exception) 94 | { 95 | throw new U2fException("Error when parsing attestation certificate", exception); 96 | } 97 | finally 98 | { 99 | stream.Dispose(); 100 | binaryReader.Dispose(); 101 | } 102 | } 103 | 104 | public void CheckSignature(string appId, string clientData) 105 | { 106 | if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(clientData)) 107 | throw new ArgumentException("Invalid argument(s) were being passed."); 108 | 109 | byte[] signedBytes = PackBytesToSign( 110 | Crypto.U2F.Crypto.Hash(appId), 111 | Crypto.U2F.Crypto.Hash(clientData), 112 | _keyHandle, 113 | _userPublicKey); 114 | 115 | Crypto.U2F.Crypto.CheckSignature(_attestationCertificate, signedBytes, _signature); 116 | } 117 | 118 | public DeviceRegistration CreateDevice() 119 | { 120 | return new DeviceRegistration( 121 | _keyHandle, 122 | _userPublicKey, 123 | _attestationCertificate.RawData, 124 | DeviceRegistration.InitialCounterValue); 125 | } 126 | 127 | public byte[] PackBytesToSign(byte[] appIdHash, 128 | byte[] clientDataHash, byte[] keyHandle, byte[] userPublicKey) 129 | { 130 | List someBytes = new List(); 131 | someBytes.Add(RegistrationSignedReservedByteValue); 132 | someBytes.AddRange(appIdHash); 133 | someBytes.AddRange(clientDataHash); 134 | someBytes.AddRange(keyHandle); 135 | someBytes.AddRange(userPublicKey); 136 | 137 | return someBytes.ToArray(); 138 | } 139 | 140 | public override int GetHashCode() 141 | { 142 | int hash = 23 + _userPublicKey.Sum(b => b + 31); 143 | hash += _keyHandle.Sum(b => b + 31); 144 | hash += _signature.Sum(b => b + 31); 145 | 146 | return hash; 147 | } 148 | 149 | public override bool Equals(object obj) 150 | { 151 | if (!(obj is RawRegisterResponse)) 152 | return false; 153 | if (this == obj) 154 | return true; 155 | if (GetType() != obj.GetType()) 156 | return false; 157 | RawRegisterResponse other = (RawRegisterResponse)obj; 158 | if (!_attestationCertificate.Equals(other._attestationCertificate)) 159 | return false; 160 | if (!_keyHandle.SequenceEqual(other._keyHandle)) 161 | return false; 162 | if (!_signature.SequenceEqual(other._signature)) 163 | return false; 164 | return _userPublicKey.SequenceEqual(other._userPublicKey); 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /src/U2F.Core/Models/RegisterResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace U2F.Core.Models 5 | { 6 | public class RegisterResponse : BaseModel 7 | { 8 | private readonly ClientData _clientDataRef; 9 | 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The registration data. 14 | /// The client data. 15 | public RegisterResponse(string registrationData, string clientData) 16 | { 17 | if (string.IsNullOrWhiteSpace(registrationData) || string.IsNullOrWhiteSpace(clientData)) 18 | throw new ArgumentException("Invalid argument(s) were being passed."); 19 | 20 | RegistrationData = registrationData; 21 | ClientData = clientData; 22 | _clientDataRef = new ClientData(ClientData); 23 | } 24 | 25 | /// 26 | /// Gets the registration data. 27 | /// 28 | /// 29 | /// The registration data. 30 | /// 31 | public string RegistrationData { get; private set; } 32 | 33 | /// 34 | /// Gets the Client data. 35 | /// 36 | /// 37 | /// The Client data. 38 | /// 39 | public string ClientData { get; private set; } 40 | 41 | public ClientData GetClientData() 42 | { 43 | return _clientDataRef; 44 | } 45 | 46 | public string GetRequestId() 47 | { 48 | return GetClientData().Challenge; 49 | } 50 | 51 | public override int GetHashCode() 52 | { 53 | int hash = RegistrationData.Sum(c => c + 31); 54 | hash += ClientData.Sum(c => c + 31); 55 | 56 | return hash; 57 | } 58 | 59 | public override bool Equals(object obj) 60 | { 61 | if (!(obj is RegisterResponse)) 62 | return false; 63 | if (this == obj) 64 | return true; 65 | if (GetType() != obj.GetType()) 66 | return false; 67 | RegisterResponse other = (RegisterResponse)obj; 68 | if (ClientData == null) 69 | { 70 | if (other.ClientData != null) 71 | return false; 72 | } 73 | else if (!ClientData.Equals(other.ClientData)) 74 | return false; 75 | if (RegistrationData == null) 76 | { 77 | if (other.RegistrationData != null) 78 | return false; 79 | } 80 | else if (!RegistrationData.Equals(other.RegistrationData)) 81 | return false; 82 | return true; 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/U2F.Core/Models/StartedAuthentication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace U2F.Core.Models 5 | { 6 | public class StartedAuthentication : BaseModel 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The challenge. 12 | /// The application identifier. 13 | /// The key handle. 14 | public StartedAuthentication(string challenge, string appId, string keyHandle) 15 | { 16 | if (string.IsNullOrWhiteSpace(challenge) || string.IsNullOrWhiteSpace(keyHandle) || string.IsNullOrWhiteSpace(appId)) 17 | throw new ArgumentException("Invalid argument(s) were being passed."); 18 | 19 | Version = Crypto.U2F.U2FVersion; 20 | Challenge = challenge; 21 | AppId = appId; 22 | KeyHandle = keyHandle; 23 | } 24 | 25 | /// 26 | /// Gets the Version 27 | /// Version of the protocol that the to-be-registered U2F token must speak. For 28 | /// the version of the protocol described herein, must be "U2F_V2" 29 | /// 30 | /// 31 | /// The key handle. 32 | /// 33 | public string Version { get; private set; } 34 | 35 | /// 36 | /// Gets the key handle. 37 | /// websafe-base64 encoding of the key handle obtained from the U2F token 38 | /// during registration. 39 | /// 40 | /// 41 | /// The key handle. 42 | /// 43 | public string KeyHandle { get; private set; } 44 | 45 | /// 46 | /// Gets the challenge. 47 | /// The websafe-base64-encoded challenge. 48 | /// 49 | /// 50 | /// The challenge. 51 | /// 52 | public string Challenge { get; private set; } 53 | 54 | /// 55 | /// Gets the application identifier. 56 | /// The application id that the RP would like to assert. The U2F token will 57 | /// enforce that the key handle provided above is associated with this 58 | /// application id. The browser enforces that the calling origin belongs to the 59 | /// application identified by the application id. 60 | /// 61 | /// 62 | /// The application identifier. 63 | /// 64 | public string AppId { get; private set; } 65 | 66 | public override int GetHashCode() 67 | { 68 | int hash = 23 + Version.Sum(c => c + 31); 69 | hash += Challenge.Sum(c => c + 31); 70 | hash += AppId.Sum(c => c + 31); 71 | hash += KeyHandle.Sum(c => c + 31); 72 | 73 | return hash; 74 | } 75 | 76 | public override bool Equals(object obj) 77 | { 78 | if (!(obj is StartedAuthentication)) 79 | return false; 80 | if (this == obj) 81 | return true; 82 | if (GetType() != obj.GetType()) 83 | return false; 84 | StartedAuthentication other = (StartedAuthentication)obj; 85 | if (AppId == null) 86 | { 87 | if (other.AppId != null) 88 | return false; 89 | } 90 | else if (!AppId.Equals(other.AppId)) 91 | return false; 92 | if (Challenge == null) 93 | { 94 | if (other.Challenge != null) 95 | return false; 96 | } 97 | else if (!Challenge.Equals(other.Challenge)) 98 | return false; 99 | if (KeyHandle == null) 100 | { 101 | if (other.KeyHandle != null) 102 | return false; 103 | } 104 | else if (!KeyHandle.Equals(other.KeyHandle)) 105 | return false; 106 | if (Version == null) 107 | { 108 | if (other.Version != null) 109 | return false; 110 | } 111 | else if (!Version.Equals(other.Version)) 112 | return false; 113 | return true; 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/U2F.Core/Models/StartedRegistration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace U2F.Core.Models 5 | { 6 | public class StartedRegistration : BaseModel 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The challenge. 12 | /// The application identifier. 13 | public StartedRegistration(string challenge, string appId) 14 | { 15 | if(string.IsNullOrWhiteSpace(challenge) || string.IsNullOrWhiteSpace(appId)) 16 | throw new ArgumentException("Invalid argument(s) were being passed."); 17 | 18 | Version = Crypto.U2F.U2FVersion; 19 | Challenge = challenge; 20 | AppId = appId; 21 | } 22 | 23 | /// 24 | /// Gets or sets the version. 25 | /// Version of the protocol that the to-be-registered U2F token must speak. For 26 | /// the version of the protocol described herein, must be "U2F_V2" 27 | /// 28 | /// 29 | /// The version. 30 | /// 31 | public string Version { get; private set; } 32 | 33 | /// 34 | /// Gets the challenge. 35 | /// The websafe-base64-encoded challenge. 36 | /// 37 | /// 38 | /// The challenge. 39 | /// 40 | public string Challenge { get; private set; } 41 | 42 | /// 43 | /// Gets the application identifier. 44 | /// The application id that the RP would like to assert. The U2F token will 45 | /// enforce that the key handle provided above is associated with this 46 | /// application id. The browser enforces that the calling origin belongs to the 47 | /// application identified by the application id. 48 | /// 49 | /// 50 | /// The application identifier. 51 | /// 52 | public string AppId { get; private set; } 53 | 54 | public override int GetHashCode() 55 | { 56 | int hash = Version.Sum(c => c + 31); 57 | hash += Challenge.Sum(c => c + 31); 58 | hash += AppId.Sum(c => c + 31); 59 | 60 | return hash; 61 | } 62 | 63 | public override bool Equals(object obj) 64 | { 65 | if (!(obj is StartedRegistration)) 66 | return false; 67 | if (this == obj) 68 | return true; 69 | if (GetType() != obj.GetType()) 70 | return false; 71 | StartedRegistration other = (StartedRegistration)obj; 72 | if (AppId == null) 73 | { 74 | if (other.AppId != null) 75 | return false; 76 | } 77 | else if (!AppId.Equals(other.AppId)) 78 | return false; 79 | if (Challenge == null) 80 | { 81 | if (other.Challenge != null) 82 | return false; 83 | } 84 | else if (!Challenge.Equals(other.Challenge)) 85 | return false; 86 | if (Version == null) 87 | { 88 | if (other.Version != null) 89 | return false; 90 | } 91 | else if (!Version.Equals(other.Version)) 92 | return false; 93 | return true; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/U2F.Core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("U2F.Core")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("78b21560-5f9e-4494-83d1-239bdf7d57f6")] 20 | -------------------------------------------------------------------------------- /src/U2F.Core/U2F.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Provides FIDO U2F for .NET Core 5 | Bryce Foster (brucedog@bfoster.net) 6 | net5.0 7 | U2F.Core 8 | U2F.Core 9 | false 10 | false 11 | false 12 | 2.0.1 13 | https://github.com/brucedog/U2F_Core 14 | git 15 | U2F .NET Core FIDO 16 | https://github.com/brucedog/U2F_Core/blob/master/LICENSE 17 | https://github.com/brucedog/U2F_Core 18 | 2016 19 | true 20 | false 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/U2F.Core/U2F.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | U2F.Core 5 | 2.0.0 6 | Bryce Foster (brucedog@bfoster.net) 7 | Bryce Foster (brucedog@bfoster.net) 8 | false 9 | 2016 10 | U2F .NET Core FIDO 11 | 2016 12 | Cross platform library that provides FIDO U2F for .NET Core 13 | Cross platform library that provides FIDO U2F for .NET Core 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/U2F.Core/Utils/Asn1Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace U2F.Core.Utils 5 | { 6 | /// 7 | /// ASN1 Integer helper class. 8 | /// 9 | internal class Asn1Helper 10 | { 11 | /// 12 | /// The ASN1 integer tag 13 | /// 14 | private const byte IntegerTag = 0x02; 15 | 16 | /// 17 | /// Converts the DER encoded ASN1 integer from the binary binaryReader. 18 | /// Note: the actual data should be either 33 or 32 bytes long. 19 | /// 20 | /// The binary reader. 21 | /// A ANS1 integer. 22 | internal static byte[] ConvertDerToAsn1(BinaryReader binaryReader) 23 | { 24 | if (binaryReader == null) 25 | { 26 | throw new ArgumentNullException(nameof(binaryReader)); 27 | } 28 | 29 | byte tag = binaryReader.ReadByte(); 30 | if (tag != IntegerTag) 31 | { 32 | throw new ArgumentOutOfRangeException(nameof(binaryReader), "byte does not match the integer tag."); 33 | } 34 | 35 | byte dataLength = binaryReader.ReadByte(); 36 | if (dataLength < 32 || dataLength > 33) 37 | { 38 | throw new ArgumentOutOfRangeException(nameof(binaryReader), "The data is not 32 or 33 bytes long."); 39 | } 40 | 41 | byte[] data; 42 | if (dataLength == 33) 43 | { 44 | byte zero = binaryReader.ReadByte(); 45 | if (zero != 0x00) 46 | { 47 | throw new ArgumentOutOfRangeException(nameof(binaryReader), "The first byte should be zero."); 48 | } 49 | data = binaryReader.ReadBytes(32); 50 | } 51 | else 52 | { 53 | data = binaryReader.ReadBytes(32); 54 | } 55 | 56 | return data; 57 | } 58 | } 59 | 60 | /// 61 | /// Extension method for converting a DER encoded ASN1 Signature. 62 | /// The DER encoded ASN1 signature should be formatted as the following: 63 | /// 0x30: The DER sequence tag. 64 | /// xx : The length of the rest of the data: 68, 69 or 70 [2*(32{+1}+2)] 65 | /// 0x02: The ASN1 integer tag. 66 | /// yy : The length of the first object, either 32 or 33 67 | /// The length should be 32 unless the first byte is greater than 0x80 when a leading 0x00 will be added. 68 | /// 0x02: Again the ASN1 integer tag. 69 | /// zz : The length of the second bit of data should be either 32 or 33. 70 | /// The length should be 32 unless the first byte is greater than 0x80 when a leading 0x00 will be added. 71 | /// 72 | public static class Asn1Extensions 73 | { 74 | private const byte SequenceTag = 0x30; 75 | 76 | /// 77 | /// Convert from a DER encoded ASN1 signature, which is composed of two ASN1 integers. 78 | /// 79 | /// The DER encoded ASN1 signature. 80 | /// The converted signature. 81 | public static byte[] FromAsn1Signature(this byte[] signature) 82 | { 83 | if (signature.Length < 70 || signature.Length > 72) 84 | { 85 | throw new ArgumentOutOfRangeException(nameof(signature), "The signature should be 70 to 70 bytes long."); 86 | } 87 | 88 | using (MemoryStream stream = new MemoryStream(signature)) 89 | using (BinaryReader reader = new BinaryReader(stream)) 90 | { 91 | byte tag = reader.ReadByte(); 92 | if (tag != SequenceTag) 93 | { 94 | throw new ArgumentOutOfRangeException(nameof(signature), "Invalid sequence tag."); 95 | } 96 | 97 | reader.ReadByte(); 98 | 99 | byte[] first = Asn1Helper.ConvertDerToAsn1(reader); 100 | byte[] second = Asn1Helper.ConvertDerToAsn1(reader); 101 | 102 | byte[] convertedSignature = new byte[64]; 103 | Array.Copy(first, 0, convertedSignature, 0, 32); 104 | Array.Copy(second, 0, convertedSignature, 32, 32); 105 | 106 | return convertedSignature; 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/U2F.Core/Utils/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Security.Cryptography.X509Certificates; 4 | using System.Text; 5 | 6 | namespace U2F.Core.Utils 7 | { 8 | public static class Utils 9 | { 10 | /// 11 | /// Converts byte array to a properly formatted base64 string 12 | /// 13 | /// The argument. 14 | public static string ByteArrayToBase64String(this byte[] input) 15 | { 16 | string result = Convert.ToBase64String(input); 17 | result = result.TrimEnd('='); 18 | result = result.Replace('+', '-'); 19 | result = result.Replace('/', '_'); 20 | 21 | return result; 22 | } 23 | 24 | /// 25 | /// Formats string to proper base64 string and returns it as a byte array. 26 | /// 27 | /// The input. 28 | public static byte[] Base64StringToByteArray(this string input) 29 | { 30 | input = input.Replace('-', '+'); 31 | input = input.Replace('_', '/'); 32 | 33 | int mod4 = input.Length % 4; 34 | if (mod4 > 0) 35 | { 36 | input += new string('=', 4 - mod4); 37 | } 38 | 39 | return Convert.FromBase64String(input); 40 | } 41 | 42 | /// 43 | /// Convert string into UTF8 byte[] 44 | /// 45 | /// The string to convert. 46 | /// UTF8 encoded byte[] 47 | public static byte[] GetBytes(this string stringToConvert) 48 | { 49 | return Encoding.UTF8.GetBytes(stringToConvert); 50 | } 51 | 52 | /// 53 | /// Converts byte[] to UTF8 encoded string 54 | /// 55 | /// Byte array to be converted to UTF8 string. 56 | /// UTF8 encoded string 57 | public static string GetString(this byte[] bytes) 58 | { 59 | return Encoding.UTF8.GetString(bytes); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Controllers/ProfileController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | using U2F.Demo.Models; 10 | using U2F.Demo.Services; 11 | using U2F.Core.Utils; 12 | using U2F.Demo.ViewModel; 13 | 14 | namespace U2F.Demo.Controllers 15 | { 16 | [Authorize] 17 | public class ProfileController : Controller 18 | { 19 | private readonly IMembershipService _membershipService; 20 | private readonly ILogger _logger; 21 | 22 | public ProfileController(IMembershipService membershipService, 23 | ILogger logger) 24 | { 25 | _membershipService = membershipService; 26 | _logger = logger; 27 | } 28 | 29 | public async Task Index() 30 | { 31 | if (!HttpContext.User.Identity.IsAuthenticated) 32 | { 33 | ModelState.AddModelError("", "User has timed out."); 34 | RedirectToAction("Login", "U2F"); 35 | } 36 | 37 | var user = await _membershipService.FindUserByUsername(HttpContext.User.Identity.Name); 38 | return View("Index", user); 39 | } 40 | 41 | public async Task AddDevice(string deviceResponse) 42 | { 43 | try 44 | { 45 | if (!HttpContext.User.Identity.IsAuthenticated) 46 | { 47 | ModelState.AddModelError("", "User has timed out."); 48 | RedirectToAction("Login", "U2F"); 49 | } 50 | bool result = await _membershipService.CompleteRegistration(HttpContext.User.Identity.Name, deviceResponse); 51 | if(result) 52 | return Ok(); 53 | } 54 | catch (Exception exception) 55 | { 56 | _logger.LogError(exception.Message); 57 | return StatusCode(500); 58 | } 59 | return BadRequest(); 60 | } 61 | 62 | public async Task GetChallenge() 63 | { 64 | try 65 | { 66 | List serverRegisterResponse = await _membershipService.GenerateDeviceChallenges(HttpContext.User.Identity.Name); 67 | CompleteRegisterViewModel registerModel = new CompleteRegisterViewModel 68 | { 69 | UserName = HttpContext.User.Identity.Name, 70 | AppId = serverRegisterResponse[0].appId, 71 | Challenge = serverRegisterResponse[0].challenge, 72 | Version = serverRegisterResponse[0].version 73 | }; 74 | 75 | return new JsonResult(JsonConvert.SerializeObject(registerModel)); 76 | } 77 | catch (Exception exception) 78 | { 79 | _logger.LogError(exception.Message); 80 | } 81 | return NoContent(); 82 | } 83 | 84 | public async Task DeviceInfo(int deviceId) 85 | { 86 | try 87 | { 88 | if (!HttpContext.User.Identity.IsAuthenticated) 89 | { 90 | ModelState.AddModelError("", "User has timed out."); 91 | RedirectToAction("Login", "U2F"); 92 | } 93 | 94 | User user = await _membershipService.FindUserByUsername(HttpContext.User.Identity.Name); 95 | Device device = user.DeviceRegistrations.FirstOrDefault(f => f.Id == deviceId); 96 | dynamic formattedResult = new 97 | { 98 | Id = device.Id, 99 | KeyHandle = device.KeyHandle.ByteArrayToBase64String(), 100 | PublicKey = device.PublicKey.ByteArrayToBase64String(), 101 | Counter = device.Counter, 102 | UpdatedOn = device.UpdatedOn 103 | }; 104 | return new JsonResult(JsonConvert.SerializeObject(formattedResult)); 105 | } 106 | catch (Exception exception) 107 | { 108 | _logger.LogError(exception.Message); 109 | } 110 | return NoContent(); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Controllers/U2FController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | using U2F.Demo.Models; 10 | using U2F.Demo.Services; 11 | using U2F.Demo.ViewModel; 12 | 13 | namespace U2F.Demo.Controllers 14 | { 15 | public class U2FController : Controller 16 | { 17 | private readonly ILogger _logger; 18 | private readonly IMembershipService _membershipService; 19 | 20 | public U2FController(IMembershipService membershipService, ILogger logger) 21 | { 22 | _membershipService = membershipService; 23 | _logger = logger; 24 | } 25 | 26 | [AllowAnonymous] 27 | public IActionResult Index() 28 | { 29 | return View(); 30 | } 31 | 32 | 33 | public IActionResult Error() 34 | { 35 | return View(); 36 | } 37 | 38 | [AllowAnonymous] 39 | public IActionResult Login() 40 | { 41 | return View(); 42 | } 43 | 44 | [HttpPost] 45 | [AllowAnonymous] 46 | [ValidateAntiForgeryToken] 47 | public async Task BeginLogin(StartLoginViewModel model) 48 | { 49 | bool isUserRegistered = await _membershipService.IsUserRegistered(model.UserName); 50 | bool areCredsValid = await _membershipService.IsValidUserNameAndPassword(model.UserName, model.Password); 51 | 52 | if (string.IsNullOrWhiteSpace(model.Password) || !isUserRegistered) 53 | { 54 | _logger.LogInformation($"invalid username {model.UserName} or password {model.Password}"); 55 | // If we got this far, something failed, redisplay form 56 | ModelState.AddModelError("CustomError", "User has not been registered."); 57 | return View("Login", model); 58 | } 59 | 60 | if (!areCredsValid) 61 | { 62 | _logger.LogInformation($"invalid username {model.UserName} or password {model.Password}"); 63 | ModelState.AddModelError("CustomError", "User/Password is not invalid."); 64 | return View("Login", model); 65 | } 66 | 67 | try 68 | { 69 | List deviceChallenges = await _membershipService.GenerateDeviceChallenges(model.UserName); 70 | 71 | if (deviceChallenges == null || deviceChallenges.Count == 0) 72 | throw new Exception("No server challenges were generated."); 73 | 74 | var challenges = JsonConvert.SerializeObject(deviceChallenges); 75 | CompleteLoginViewModel loginModel = new CompleteLoginViewModel 76 | { 77 | AppId = deviceChallenges[0].appId, 78 | Version = deviceChallenges[0].version, 79 | Challenge = deviceChallenges[0].challenge, 80 | Challenges = challenges, 81 | UserName = model.UserName.Trim() 82 | }; 83 | return View("FinishLogin", loginModel); 84 | } 85 | catch (Exception e) 86 | { 87 | _logger.LogError(e.Message); 88 | ModelState.AddModelError("CustomError", e.Message); 89 | return View("Login", model); 90 | } 91 | } 92 | 93 | [HttpPost] 94 | [AllowAnonymous] 95 | [ValidateAntiForgeryToken] 96 | public async Task CompletedLogin([FromForm]CompleteLoginViewModel model) 97 | { 98 | bool isUserRegistered = await _membershipService.IsUserRegistered(model.UserName); 99 | if (!isUserRegistered) 100 | { 101 | // If we got this far, something failed, redisplay form 102 | ModelState.AddModelError("", "User has not been registered."); 103 | return View("FinishLogin", model); 104 | } 105 | 106 | try 107 | { 108 | if (!await _membershipService.AuthenticateUser(model.UserName, model.DeviceResponse)) 109 | throw new Exception("Device response did not work with user."); 110 | 111 | return RedirectToAction("Index", "Profile"); 112 | } 113 | catch (Exception e) 114 | { 115 | _logger.LogError(e.Message); 116 | 117 | ModelState.AddModelError("", "Error authenticating"); 118 | return View("FinishLogin", model); 119 | } 120 | } 121 | 122 | [AllowAnonymous] 123 | public IActionResult Register() 124 | { 125 | return View(); 126 | } 127 | 128 | [HttpPost] 129 | [AllowAnonymous] 130 | [ValidateAntiForgeryToken] 131 | public async Task BeginRegister(StartRegisterViewModel viewModel) 132 | { 133 | bool isUserRegistered = await _membershipService.IsUserRegistered(viewModel.UserName); 134 | if (isUserRegistered) 135 | { 136 | ModelState.AddModelError("CustomError", "User is already registered."); 137 | return View("Register", viewModel); 138 | } 139 | 140 | if (!string.IsNullOrWhiteSpace(viewModel.Password) 141 | && !string.IsNullOrWhiteSpace(viewModel.UserName) 142 | && viewModel.Password.Equals(viewModel.ConfirmPassword)) 143 | { 144 | try 145 | { 146 | bool result = await _membershipService.SaveNewUser(viewModel.UserName, viewModel.Password, viewModel.Email); 147 | if (!result) 148 | throw new Exception("Failed to create user"); 149 | 150 | ServerRegisterResponse serverRegisterResponse = await _membershipService.GenerateServerChallenge(viewModel.UserName); 151 | 152 | CompleteRegisterViewModel registerModel = new CompleteRegisterViewModel 153 | { 154 | UserName = viewModel.UserName, 155 | AppId = serverRegisterResponse.AppId, 156 | Challenge = serverRegisterResponse.Challenge, 157 | Version = serverRegisterResponse.Version 158 | }; 159 | 160 | return View("FinishRegister", registerModel); 161 | } 162 | catch (Exception e) 163 | { 164 | _logger.LogError(e.Message); 165 | ModelState.AddModelError("CustomError", e.Message); 166 | 167 | return View("Register", viewModel); 168 | } 169 | } 170 | 171 | ModelState.AddModelError("CustomError", "invalid input"); 172 | return View("Register", viewModel); 173 | } 174 | 175 | [HttpPost] 176 | [AllowAnonymous] 177 | [ValidateAntiForgeryToken] 178 | public async Task CompleteRegister(CompleteRegisterViewModel value) 179 | { 180 | if (!string.IsNullOrWhiteSpace(value.DeviceResponse) 181 | && !string.IsNullOrWhiteSpace(value.UserName)) 182 | { 183 | try 184 | { 185 | value.DeviceResponse = await _membershipService.CompleteRegistration(value.UserName, value.DeviceResponse) 186 | ? "Registration was successful." 187 | : "Registration failed."; 188 | 189 | return RedirectToAction("Index", "Profile"); 190 | } 191 | catch (Exception e) 192 | { 193 | _logger.LogError(e.Message); 194 | ModelState.AddModelError("CustomError", e.Message); 195 | 196 | return View("FinishRegister", value); 197 | } 198 | } 199 | 200 | ModelState.AddModelError("CustomError", "bad username/device response"); 201 | return View("FinishRegister", value); 202 | } 203 | 204 | [HttpPost] 205 | [ValidateAntiForgeryToken] 206 | public async Task LogOff() 207 | { 208 | await _membershipService.SignOut(); 209 | _logger.LogInformation(4, "User logged out."); 210 | return RedirectToAction("Index", "U2F"); 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/DataStore/U2FContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Logging; 5 | using U2F.Demo.Models; 6 | 7 | namespace U2F.Demo.DataStore 8 | { 9 | public sealed class U2FContext : IdentityDbContext 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public U2FContext(DbContextOptions options, ILogger logger) 14 | : base(options) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public override int SaveChanges() 20 | { 21 | try 22 | { 23 | return base.SaveChanges(); 24 | } 25 | catch (Exception e) 26 | { 27 | _logger.LogError(e.Message); 28 | // zero means no items were saved to the DB 29 | return 0; 30 | } 31 | } 32 | 33 | public DbSet Users { get; set; } 34 | 35 | public DbSet Devices { get; set; } 36 | 37 | public DbSet AuthenticationRequests { get; set; } 38 | } 39 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Models/AuthenticationRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace U2F.Demo.Models 5 | { 6 | public class AuthenticationRequest 7 | { 8 | [Key] 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | public int Id { get; set; } 11 | 12 | public string KeyHandle { get; set; } 13 | 14 | [Required] 15 | public string Challenge { get; set; } 16 | 17 | [Required] 18 | [StringLength(200)] 19 | public string AppId { get; set; } 20 | 21 | [Required] 22 | [StringLength(50)] 23 | public string Version { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Models/Device.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace U2F.Demo.Models 6 | { 7 | public class Device 8 | { 9 | [Key] 10 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 11 | public int Id { get; set; } 12 | 13 | [Required] 14 | public DateTime CreatedOn { get; set; } 15 | 16 | public DateTime UpdatedOn { get; set; } 17 | 18 | [Required] 19 | public byte[] KeyHandle { get; set; } 20 | 21 | [Required] 22 | public byte[] PublicKey { get; set; } 23 | 24 | [Required] 25 | public byte[] AttestationCert { get; set; } 26 | 27 | [Required] 28 | public int Counter { get; set; } 29 | 30 | public bool IsCompromised { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Models/ServerChallenge.cs: -------------------------------------------------------------------------------- 1 | namespace U2F.Demo.Models 2 | { 3 | public class ServerChallenge 4 | { 5 | public string challenge { get; set; } 6 | public string version { get; set; } 7 | public string appId { get; set; } 8 | public string keyHandle { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Models/ServerRegisterResponse.cs: -------------------------------------------------------------------------------- 1 | namespace U2F.Demo.Models 2 | { 3 | public class ServerRegisterResponse 4 | { 5 | public string AppId { get; set; } 6 | public string Challenge { get; set; } 7 | public string Version { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using Microsoft.AspNetCore.Identity; 5 | 6 | namespace U2F.Demo.Models 7 | { 8 | public class User : IdentityUser 9 | { 10 | [StringLength(100)] 11 | public string Name { get; set; } 12 | 13 | [Required] 14 | public DateTime CreatedOn { get; set; } 15 | 16 | public DateTime UpdatedOn { get; set; } 17 | 18 | public virtual ICollection DeviceRegistrations { get; set; } 19 | 20 | public virtual ICollection AuthenticationRequest { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace U2F.Demo 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .ConfigureWebHostDefaults(webBuilder => 18 | { 19 | webBuilder.UseStartup(); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "https://localhost:44340/", 7 | "sslPort": 44340 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "U2F.Demo": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Services/IMembershipService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using U2F.Demo.Models; 4 | 5 | namespace U2F.Demo.Services 6 | { 7 | public interface IMembershipService 8 | { 9 | /// 10 | /// Checks to see if the provided username is registered 11 | /// 12 | /// user name to check. 13 | /// true if user is registered 14 | Task IsUserRegistered(string username); 15 | 16 | /// 17 | /// Checks to see if the username and password are correct 18 | /// 19 | /// username 20 | /// hashed password 21 | /// true if the provided username and password is valid 22 | Task IsValidUserNameAndPassword(string username, string password); 23 | 24 | /// 25 | /// Generates a list of challenges for each device the user has registered 26 | /// 27 | /// username to generate challenges for. 28 | /// list of challenges for each device the user has registered. 29 | Task> GenerateDeviceChallenges(string username); 30 | 31 | Task GenerateServerChallenge(string username); 32 | 33 | Task AuthenticateUser(string userName, string deviceResponse); 34 | 35 | Task SaveNewUser(string username, string password, string emailAddress); 36 | 37 | Task CompleteRegistration(string userName, string deviceResponse); 38 | 39 | /// 40 | /// Signs user out 41 | /// 42 | Task SignOut(); 43 | 44 | /// 45 | /// Returns a complete user object via its userName 46 | /// 47 | Task FindUserByUsername(string username); 48 | } 49 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Services/MembershipService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Net.Http.Headers; 10 | using U2F.Core.Models; 11 | using U2F.Demo.DataStore; 12 | using U2F.Demo.Models; 13 | using U2F.Core.Utils; 14 | 15 | namespace U2F.Demo.Services 16 | { 17 | public class MembershipService : IMembershipService 18 | { 19 | // NOTE: THIS HAS TO BE UPDATED TO MATCH YOUR SITE/EXAMPLE and sites must be https 20 | private const string DemoAppId = "https://localhost:44340"; 21 | private readonly U2FContext _dataContext; 22 | private readonly UserManager _userManager; 23 | private readonly SignInManager _signInManager; 24 | private readonly ILogger _logger; 25 | 26 | public MembershipService(U2FContext userRepository, 27 | UserManager userManager, 28 | SignInManager signInManager, 29 | ILogger logger) 30 | { 31 | _dataContext = userRepository; 32 | _userManager = userManager; 33 | _signInManager = signInManager; 34 | _logger = logger; 35 | } 36 | 37 | #region IMemeberShipService methods 38 | 39 | public async Task SaveNewUser(string userName, string password, string email) 40 | { 41 | bool result = await IsUserRegistered(userName); 42 | if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(email)|| result) 43 | return false; 44 | 45 | User newUser = new User {Name = userName.Trim(), Email = email.Trim(), CreatedOn = DateTime.Now, UserName = userName.Trim()}; 46 | IdentityResult userCreatedResult = await _userManager.CreateAsync(newUser, password.Trim()); 47 | 48 | if (userCreatedResult.Succeeded) 49 | { 50 | _logger.LogInformation($"user {newUser.Name} was created email {newUser.Email}"); 51 | return true; 52 | } 53 | else 54 | { 55 | _logger.LogInformation($"failed to create user {userName}"); 56 | foreach (IdentityError error in userCreatedResult.Errors) 57 | { 58 | _logger.LogError(error.Description); 59 | } 60 | return false; 61 | } 62 | } 63 | 64 | public async Task GenerateServerChallenge(string username) 65 | { 66 | User user = await FindUserByUsername(username); 67 | if (user == null) 68 | return null; 69 | 70 | StartedRegistration startedRegistration = Core.Crypto.U2F.StartRegistration(DemoAppId); 71 | 72 | if (user.AuthenticationRequest == null) 73 | user.AuthenticationRequest = new List(); 74 | if(user.DeviceRegistrations == null) 75 | user.DeviceRegistrations = new List(); 76 | 77 | user.AuthenticationRequest.Add( 78 | new AuthenticationRequest 79 | { 80 | AppId = startedRegistration.AppId, 81 | Challenge = startedRegistration.Challenge, 82 | Version = Core.Crypto.U2F.U2FVersion 83 | }); 84 | 85 | user.UpdatedOn = DateTime.Now; 86 | 87 | await _dataContext.SaveChangesAsync(); 88 | 89 | return new ServerRegisterResponse 90 | { 91 | AppId = startedRegistration.AppId, 92 | Challenge = startedRegistration.Challenge, 93 | Version = startedRegistration.Version 94 | }; 95 | } 96 | 97 | public async Task CompleteRegistration(string userName, string deviceResponse) 98 | { 99 | if (string.IsNullOrWhiteSpace(deviceResponse)) 100 | return false; 101 | 102 | User user = await FindUserByUsername(userName); 103 | 104 | if (user?.AuthenticationRequest == null || user.AuthenticationRequest.Count == 0) 105 | return false; 106 | 107 | 108 | RegisterResponse registerResponse = RegisterResponse.FromJson(deviceResponse); 109 | 110 | // TODO When the user is registration they should only ever have one auth request.???? 111 | AuthenticationRequest authenticationRequest = user.AuthenticationRequest.First(); 112 | 113 | StartedRegistration startedRegistration = new StartedRegistration(authenticationRequest.Challenge, authenticationRequest.AppId); 114 | DeviceRegistration registration = Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse); 115 | 116 | user.AuthenticationRequest.Clear(); 117 | user.UpdatedOn = DateTime.Now; 118 | user.DeviceRegistrations.Add(new Device 119 | { 120 | AttestationCert = registration.AttestationCert, 121 | Counter = Convert.ToInt32(registration.Counter), 122 | CreatedOn = DateTime.Now, 123 | UpdatedOn = DateTime.Now, 124 | KeyHandle = registration.KeyHandle, 125 | PublicKey = registration.PublicKey 126 | }); 127 | int result = await _dataContext.SaveChangesAsync(); 128 | 129 | if(result > 0) 130 | await _signInManager.SignInAsync(user, new AuthenticationProperties(), "U2F"); 131 | 132 | return result > 0; 133 | } 134 | 135 | public async Task AuthenticateUser(string userName, string deviceResponse) 136 | { 137 | if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(deviceResponse)) 138 | return false; 139 | 140 | User user = await FindUserByUsername(userName); 141 | if (user == null) 142 | return false; 143 | 144 | AuthenticateResponse authenticateResponse = AuthenticateResponse.FromJson(deviceResponse); 145 | 146 | Device device = user.DeviceRegistrations.FirstOrDefault(f => f.KeyHandle.SequenceEqual(authenticateResponse.KeyHandle.Base64StringToByteArray())); 147 | 148 | if (device == null || user.AuthenticationRequest == null) 149 | return false; 150 | 151 | // User will have a authentication request for each device they have registered so get the one that matches the device key handle 152 | AuthenticationRequest authenticationRequest = user.AuthenticationRequest.First(f => f.KeyHandle.Equals(authenticateResponse.KeyHandle)); 153 | DeviceRegistration registration = new DeviceRegistration(device.KeyHandle, device.PublicKey, device.AttestationCert, Convert.ToUInt32(device.Counter)); 154 | 155 | StartedAuthentication authentication = new StartedAuthentication(authenticationRequest.Challenge, authenticationRequest.AppId, authenticationRequest.KeyHandle); 156 | 157 | Core.Crypto.U2F.FinishAuthentication(authentication, authenticateResponse, registration); 158 | await _signInManager.SignInAsync(user, new AuthenticationProperties(), "U2F"); 159 | 160 | user.AuthenticationRequest.Clear(); 161 | user.UpdatedOn = DateTime.Now; 162 | 163 | device.Counter = Convert.ToInt32(registration.Counter); 164 | device.UpdatedOn = DateTime.Now; 165 | 166 | int result = await _dataContext.SaveChangesAsync(); 167 | 168 | return result > 0; 169 | } 170 | 171 | public async Task IsUserRegistered(string userName) 172 | { 173 | if (string.IsNullOrWhiteSpace(userName)) 174 | return false; 175 | 176 | User user = await FindUserByUsername(userName); 177 | 178 | return user != null; 179 | } 180 | 181 | public async Task> GenerateDeviceChallenges(string userName) 182 | { 183 | User user = await FindUserByUsername(userName); 184 | 185 | if (user == null) 186 | return null; 187 | 188 | // We only want to generate challenges for un-compromised devices 189 | List devices = user.DeviceRegistrations.Where(w => w.IsCompromised == false).ToList(); 190 | 191 | if (devices.Count == 0) 192 | return null; 193 | 194 | user.AuthenticationRequest.Clear(); 195 | string challenge = Core.Crypto.U2F.GenerateChallenge(); 196 | 197 | List serverChallenges = new List(); 198 | foreach (Device registeredDevice in devices) 199 | { 200 | serverChallenges.Add(new ServerChallenge 201 | { 202 | appId = DemoAppId, 203 | challenge = challenge, 204 | keyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(), 205 | version = Core.Crypto.U2F.U2FVersion 206 | }); 207 | user.AuthenticationRequest.Add( 208 | new AuthenticationRequest 209 | { 210 | AppId = DemoAppId, 211 | Challenge = challenge, 212 | KeyHandle = registeredDevice.KeyHandle.ByteArrayToBase64String(), 213 | Version = Core.Crypto.U2F.U2FVersion 214 | }); 215 | } 216 | user.UpdatedOn = DateTime.Now; 217 | await _dataContext.SaveChangesAsync(); 218 | return serverChallenges; 219 | } 220 | 221 | public async Task IsValidUserNameAndPassword(string userName, string password) 222 | { 223 | if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password)) 224 | return false; 225 | 226 | User user = await FindUserByUsername(userName.Trim()); 227 | SignInResult result = await _signInManager.CheckPasswordSignInAsync(user, password.Trim(), false); 228 | 229 | return result.Succeeded; 230 | } 231 | 232 | public async Task SignOut() 233 | { 234 | await _signInManager.SignOutAsync(); 235 | } 236 | 237 | public async Task FindUserByUsername(string username) 238 | { 239 | User user = null; 240 | 241 | if (!string.IsNullOrWhiteSpace(username)) 242 | { 243 | if (await _dataContext.Users.AnyAsync(person => person.Name == username.Trim())) 244 | { 245 | user = await _dataContext.Users 246 | .Include(i => i.AuthenticationRequest) 247 | .Include(i => i.DeviceRegistrations) 248 | .FirstAsync(person => person.Name == username.Trim()); 249 | } 250 | } 251 | 252 | return user; 253 | } 254 | 255 | #endregion 256 | } 257 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using U2F.Demo.DataStore; 11 | using U2F.Demo.Models; 12 | using U2F.Demo.Services; 13 | 14 | namespace U2F.Demo 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddDbContext(options => options.UseInMemoryDatabase("DemoDb")); 29 | 30 | services.AddIdentity() 31 | .AddEntityFrameworkStores( 32 | 33 | ) 34 | .AddDefaultTokenProviders(); 35 | 36 | services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => 37 | { 38 | // Cookie settings 39 | options.ExpireTimeSpan = TimeSpan.FromDays(150); 40 | options.LoginPath = "/U2F/Login"; 41 | options.LogoutPath = "/U2F/LogOff"; 42 | options.AccessDeniedPath = "/U2F"; 43 | //options.Cookies.ApplicationCookie.AutomaticAuthenticate = true; 44 | options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter; 45 | }); 46 | 47 | services.Configure(options => 48 | { 49 | // Password settings 50 | options.Password.RequireDigit = false; 51 | options.Password.RequiredLength = 6; 52 | options.Password.RequireNonAlphanumeric = false; 53 | options.Password.RequireUppercase = false; 54 | options.Password.RequireLowercase = false; 55 | 56 | // Lockout settings 57 | options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); 58 | options.Lockout.MaxFailedAccessAttempts = 10; 59 | 60 | // User settings 61 | options.User.RequireUniqueEmail = false; 62 | }); 63 | 64 | //services.AddMvc(); 65 | services.AddControllersWithViews(); 66 | services.AddRazorPages(); 67 | var connectionString = Configuration["connectionStrings:DBConnectionString"]; 68 | services.AddDbContext(o => o.UseInMemoryDatabase(Guid.NewGuid().ToString())); 69 | services.AddScoped(); 70 | } 71 | 72 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 73 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 74 | { 75 | if (env.IsDevelopment()) 76 | { 77 | app.UseDeveloperExceptionPage(); 78 | app.UseDatabaseErrorPage(); 79 | } 80 | else 81 | { 82 | app.UseExceptionHandler("/Home/Error"); 83 | } 84 | 85 | app.UseHttpsRedirection(); 86 | app.UseStaticFiles(); 87 | 88 | app.UseRouting(); 89 | 90 | app.UseAuthentication(); 91 | app.UseAuthorization(); 92 | 93 | app.UseEndpoints(endpoints => 94 | { 95 | endpoints.MapControllerRoute( 96 | name: "default", 97 | pattern: "{controller=U2F}/{action=Index}/{id?}"); 98 | endpoints.MapRazorPages(); 99 | }); 100 | 101 | try 102 | { 103 | using (var serviceScope = app.ApplicationServices.GetRequiredService() 104 | .CreateScope()) 105 | { 106 | 107 | serviceScope.ServiceProvider.GetService().Database.Migrate(); 108 | } 109 | } 110 | catch (Exception e) 111 | { 112 | Console.WriteLine(e.Message); 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/U2F.Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | true 6 | U2F.Demo 7 | Exe 8 | U2F.Demo 9 | win10-x64;win81-x64;win8-x64;win7-x64 10 | $(AssetTargetFallback);dotnet5.6;portable-net45+win8 11 | u2f-demo 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/ViewModel/CompleteLoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace U2F.Demo.ViewModel 4 | { 5 | public class CompleteLoginViewModel 6 | { 7 | [Required] 8 | [Display(Name = "User name")] 9 | public string UserName { get; set; } 10 | 11 | [Required] 12 | [Display(Name = "App id")] 13 | public string AppId { get; set; } 14 | 15 | [Required] 16 | [Display(Name = "Version")] 17 | public string Version { get; set; } 18 | 19 | [Required] 20 | [Display(Name = "Device Response")] 21 | public string DeviceResponse { get; set; } 22 | 23 | [Display(Name = "Challenges")] 24 | public string Challenges { get; set; } 25 | 26 | [Display(Name = "Challenge")] 27 | public string Challenge { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/ViewModel/CompleteRegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace U2F.Demo.ViewModel 4 | { 5 | public class CompleteRegisterViewModel 6 | { 7 | [Required] 8 | [Display(Name = "User name")] 9 | public string UserName { get; set; } 10 | 11 | [Display(Name = "Challenge")] 12 | public string Challenge { get; set; } 13 | 14 | [Display(Name = "Version")] 15 | public string Version { get; set; } 16 | 17 | [Display(Name = "App ID")] 18 | public string AppId { get; set; } 19 | 20 | [Display(Name = "Device Response")] 21 | public string DeviceResponse { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/ViewModel/RegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace U2F.Demo.ViewModel 4 | { 5 | public class RegisterViewModel 6 | { 7 | [Required] 8 | [Display(Name = "User name")] 9 | public string UserName { get; set; } 10 | 11 | [Required] 12 | [EmailAddress] 13 | public string Email { get; set; } 14 | 15 | [Required] 16 | [Display(Name = "Password")] 17 | public string Password { get; set; } 18 | 19 | [Required] 20 | [Display(Name = "Confirm Password")] 21 | public string ConfirmPassword { get; set; } 22 | 23 | [Display(Name = "Challenge")] 24 | public string Challenge { get; set; } 25 | 26 | [Display(Name = "Version")] 27 | public string Version { get; set; } 28 | 29 | [Display(Name = "App ID")] 30 | public string AppId { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/ViewModel/StartLoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace U2F.Demo.ViewModel 4 | { 5 | public class StartLoginViewModel 6 | { 7 | [Required] 8 | [Display(Name = "User name")] 9 | public string UserName { get; set; } 10 | 11 | [Required] 12 | [Display(Name = "Password")] 13 | public string Password { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/ViewModel/StartRegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace U2F.Demo.ViewModel 4 | { 5 | public class StartRegisterViewModel 6 | { 7 | [Required] 8 | [Display(Name = "User name")] 9 | public string UserName { get; set; } 10 | 11 | [Required] 12 | [EmailAddress] 13 | [Display(Name = "Email Address")] 14 | public string Email { get; set; } 15 | 16 | [Required] 17 | [Display(Name = "Password")] 18 | public string Password { get; set; } 19 | 20 | [Required] 21 | [Display(Name = "Confirm Password")] 22 | public string ConfirmPassword { get; set; } 23 | 24 | [Display(Name = "Challenge")] 25 | public string Challenge { get; set; } 26 | 27 | [Display(Name = "Version")] 28 | public string Version { get; set; } 29 | 30 | [Display(Name = "App ID")] 31 | public string AppId { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/Profile/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model U2F.Demo.Models.User 2 | 3 | @{ 4 | ViewBag.Title = Model.Name + " - Profile"; 5 | Layout = "~/Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 | @section featured 9 | { 10 |
11 |
@Model.Name Profile
12 |
13 | 14 |
15 |
16 | @Html.Label("User name: " + Model.Name) 17 |
18 |
19 |
20 |
21 | @Html.Label("email: " + Model.Email) 22 |
23 |
24 | 25 |
26 |
27 | @Html.Label("Registered: " + Model.CreatedOn) 28 |
29 |
30 | 31 |
32 |
33 | @Html.Label("Last Updated: " + Model.UpdatedOn) 34 |
35 |
36 | 37 | 40 | 41 |
42 |
43 | 44 | @*device panel*@ 45 |
46 |
Devices
47 | @*device table*@ 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | @foreach (var device in Model.DeviceRegistrations) 61 | { 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 74 | } 75 | 76 |
#CounterRegistered OnIs CompromisedLast UsedRemove
@device.Id@device.Counter@device.CreatedOn@device.IsCompromised@device.UpdatedOn 69 | 72 |
77 |
78 |
79 | @*device panel*@ 80 |
81 | 82 | @**begin add modal*@ 83 | 99 | @**end device modal*@ 100 | 101 | @*begin info modal**@ 102 | 139 | @**end info modal*@ 140 | } 141 | 142 | @section Scripts { 143 | 144 | 190 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 |

Development Mode

9 |

10 | Swapping to Development environment will display more detailed information about the error that occurred. 11 |

12 |

13 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 14 |

15 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http 2 | 3 | 4 | 5 | 6 | 7 | 8 | ASP.NET Core Application U2F.Core Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | @RenderSection("specialJS", required: false) 21 | 22 | 23 | 24 | 25 | 26 | 57 | 58 |
59 |
60 |
61 |
62 | 65 |
66 | 67 | @RenderSection("featured", required: false) 68 | @RenderBody() 69 |
70 | 71 |
72 | 73 |
74 |
75 |

© @DateTime.Now.Year - ASP.NET Core Application U2F.Core Demo

76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 89 | 93 | 94 | 95 | @RenderSection("scripts", required: false) 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/U2F/FinishLogin.cshtml: -------------------------------------------------------------------------------- 1 | @model U2F.Demo.ViewModel.CompleteLoginViewModel 2 | @{ 3 | ViewBag.Title = "Finish Authentication"; 4 | Layout = "~/Views/Shared/_Layout.cshtml"; 5 | } 6 | 7 |

Finish Authentication

8 | @using (Html.BeginForm("CompletedLogin", "U2F", FormMethod.Post, new { id = "loginForm" })) 9 | { 10 | @Html.AntiForgeryToken() 11 | @Html.ValidationSummary() 12 | 13 |
14 | Registration Form 15 |

Please plug in your device and touch the button.

16 | 17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 | 43 | 59 | } 60 | 61 | @section Scripts { 62 | 91 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/U2F/FinishRegister.cshtml: -------------------------------------------------------------------------------- 1 | @model U2F.Demo.ViewModel.CompleteRegisterViewModel 2 | @{ 3 | ViewBag.Title = "Complete Register"; 4 | Layout = "~/Views/Shared/_Layout.cshtml"; 5 | } 6 | 7 | @section featured 8 | { 9 | 10 | @using (Html.BeginForm("CompleteRegister", "U2F", FormMethod.Post, new { id = "registerForm" })) 11 | { 12 | @Html.AntiForgeryToken() 13 | @Html.ValidationSummary(true) 14 |
15 | Complete Registration Form 16 |

Please plug in your device and touch the button.

17 | 18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | @Html.TextBoxFor(m => m.AppId, new { @readonly = "readonly", @class = "form-control" }) 26 |
27 |
28 | 29 | @Html.TextBoxFor(m => m.Version, new { @readonly = "readonly", @class = "form-control" }) 30 |
31 |
32 | 33 | @Html.TextBoxFor(m => m.Challenge, new { @readonly = "readonly", @class = "form-control" }) 34 |
35 |
36 | 37 | @Html.TextBoxFor(m => m.DeviceResponse, new { @class = "form-control" }) 38 |
39 |
40 | 41 |
42 | } 43 | 44 | 45 | 61 | } 62 | 63 | @section Scripts { 64 | 65 | 96 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/U2F/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 | @section featured 6 | { 7 | 8 |
9 |
10 |

ASP.NET Core demo site for the U2F.Core library

11 |

This demo will let you create a user and enroll a U2F device, then authenticate yourself using the enrolled device. This requires a U2F device, as well as a browser with U2F support. Start by registering a user, then try logging in.

12 |

13 | Learn more » 14 |

15 |

16 | @Html.ActionLink("Login", "Login", "U2F", new { id = "login" }, new { @class = "btn btn-lg btn-success" }) 17 | @Html.ActionLink("Register", "Register", "U2F", new { id = "register" }, new { @class = "btn btn-lg btn-success" }) 18 |

19 |
20 |
21 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/U2F/Login.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Threading.Tasks 2 | @using U2F.Demo.ViewModel 3 | @model StartLoginViewModel 4 | 5 | @{ 6 | ViewBag.Title = "Login Page"; 7 | } 8 | 9 | @section featured { 10 | 11 | @using (Html.BeginForm("BeginLogin", "U2F")) 12 | { 13 | @Html.AntiForgeryToken() 14 | @Html.ValidationSummary(true) 15 |
16 | Login 17 |

Enter a username and password.

18 | 19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 | 31 |
32 | } 33 | } 34 | 35 | @section Scripts { 36 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/U2F/Register.cshtml: -------------------------------------------------------------------------------- 1 | @using U2F.Demo.ViewModel 2 | @model StartRegisterViewModel 3 | @{ 4 | ViewBag.Title = "Register"; 5 | Layout = "~/Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 | @section featured { 9 | 10 | @using (Html.BeginForm("BeginRegister", "U2F", FormMethod.Post, new { id = "registerForm" })) 11 | { 12 | @Html.AntiForgeryToken() 13 | @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 14 |
15 | Register Form 16 |

Enter a username, email and password.

17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 |
34 | 35 | 36 | 37 |
38 | 39 | @Html.HiddenFor(m => m.AppId) 40 | @Html.HiddenFor(m => m.Challenge) 41 | @Html.HiddenFor(m => m.Version) 42 |
43 | 44 |
45 | } 46 | } 47 | 48 | @section Scripts { 49 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/U2F/SucessfulRegister.cshtml: -------------------------------------------------------------------------------- 1 | @model U2F.Demo.ViewModel.CompleteRegisterViewModel 2 | @{ 3 | ViewBag.Title = "Successful Registeration"; 4 | Layout = "~/Views/Shared/_Layout.cshtml"; 5 | } 6 | 7 |

User: "@Model.UserName" you have successfully registered

8 | 9 |

10 | @Html.ActionLink("Log in", "Login", "U2F") now you can log in with Yubico's U2F 11 |

-------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using U2F.Demo 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "connectionStrings": { 11 | "DBConnectionString": "Server=.\\sqlexpress;Database=U2F;Integrated Security=true" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "cdnjs", 4 | "libraries": [] 5 | } -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Set widths on the form inputs since otherwise they're 100% wide */ 14 | input, 15 | select, 16 | textarea { 17 | max-width: 280px; 18 | } 19 | 20 | /* Carousel */ 21 | .carousel-caption p { 22 | font-size: 20px; 23 | line-height: 1.4; 24 | } 25 | 26 | /* Make .svg files in the carousel display properly in older browsers */ 27 | .carousel-inner .item img[src$=".svg"] 28 | { 29 | width: 100%; 30 | } 31 | 32 | /* Hide/rearrange for smaller screens */ 33 | @media screen and (max-width: 767px) { 34 | /* Hide captions */ 35 | .carousel-caption { 36 | display: none 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}input,select,textarea{max-width:280px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucedog/U2F_Core/eef8bd1bcc7cd3de5e621cd5f3f58425882fbf72/src/U2F.Demo/U2F.Demo/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/images/banner1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/images/banner3.svg: -------------------------------------------------------------------------------- 1 | banner3b -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your Javascript code. 2 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucedog/U2F_Core/eef8bd1bcc7cd3de5e621cd5f3f58425882fbf72/src/U2F.Demo/U2F.Demo/wwwroot/js/site.min.js -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/js/u2f-api.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucedog/U2F_Core/eef8bd1bcc7cd3de5e621cd5f3f58425882fbf72/src/U2F.Demo/U2F.Demo/wwwroot/js/u2f-api.min.js -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2018 Twitter, Inc. 4 | Copyright (c) 2011-2018 The Bootstrap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([tabindex]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | a:not([href]):not([tabindex]):focus { 147 | outline: 0; 148 | } 149 | 150 | pre, 151 | code, 152 | kbd, 153 | samp { 154 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 155 | font-size: 1em; 156 | } 157 | 158 | pre { 159 | margin-top: 0; 160 | margin-bottom: 1rem; 161 | overflow: auto; 162 | } 163 | 164 | figure { 165 | margin: 0 0 1rem; 166 | } 167 | 168 | img { 169 | vertical-align: middle; 170 | border-style: none; 171 | } 172 | 173 | svg { 174 | overflow: hidden; 175 | vertical-align: middle; 176 | } 177 | 178 | table { 179 | border-collapse: collapse; 180 | } 181 | 182 | caption { 183 | padding-top: 0.75rem; 184 | padding-bottom: 0.75rem; 185 | color: #6c757d; 186 | text-align: left; 187 | caption-side: bottom; 188 | } 189 | 190 | th { 191 | text-align: inherit; 192 | } 193 | 194 | label { 195 | display: inline-block; 196 | margin-bottom: 0.5rem; 197 | } 198 | 199 | button { 200 | border-radius: 0; 201 | } 202 | 203 | button:focus { 204 | outline: 1px dotted; 205 | outline: 5px auto -webkit-focus-ring-color; 206 | } 207 | 208 | input, 209 | button, 210 | select, 211 | optgroup, 212 | textarea { 213 | margin: 0; 214 | font-family: inherit; 215 | font-size: inherit; 216 | line-height: inherit; 217 | } 218 | 219 | button, 220 | input { 221 | overflow: visible; 222 | } 223 | 224 | button, 225 | select { 226 | text-transform: none; 227 | } 228 | 229 | select { 230 | word-wrap: normal; 231 | } 232 | 233 | button, 234 | [type="button"], 235 | [type="reset"], 236 | [type="submit"] { 237 | -webkit-appearance: button; 238 | } 239 | 240 | button:not(:disabled), 241 | [type="button"]:not(:disabled), 242 | [type="reset"]:not(:disabled), 243 | [type="submit"]:not(:disabled) { 244 | cursor: pointer; 245 | } 246 | 247 | button::-moz-focus-inner, 248 | [type="button"]::-moz-focus-inner, 249 | [type="reset"]::-moz-focus-inner, 250 | [type="submit"]::-moz-focus-inner { 251 | padding: 0; 252 | border-style: none; 253 | } 254 | 255 | input[type="radio"], 256 | input[type="checkbox"] { 257 | box-sizing: border-box; 258 | padding: 0; 259 | } 260 | 261 | input[type="date"], 262 | input[type="time"], 263 | input[type="datetime-local"], 264 | input[type="month"] { 265 | -webkit-appearance: listbox; 266 | } 267 | 268 | textarea { 269 | overflow: auto; 270 | resize: vertical; 271 | } 272 | 273 | fieldset { 274 | min-width: 0; 275 | padding: 0; 276 | margin: 0; 277 | border: 0; 278 | } 279 | 280 | legend { 281 | display: block; 282 | width: 100%; 283 | max-width: 100%; 284 | padding: 0; 285 | margin-bottom: .5rem; 286 | font-size: 1.5rem; 287 | line-height: inherit; 288 | color: inherit; 289 | white-space: normal; 290 | } 291 | 292 | progress { 293 | vertical-align: baseline; 294 | } 295 | 296 | [type="number"]::-webkit-inner-spin-button, 297 | [type="number"]::-webkit-outer-spin-button { 298 | height: auto; 299 | } 300 | 301 | [type="search"] { 302 | outline-offset: -2px; 303 | -webkit-appearance: none; 304 | } 305 | 306 | [type="search"]::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | ::-webkit-file-upload-button { 311 | font: inherit; 312 | -webkit-appearance: button; 313 | } 314 | 315 | output { 316 | display: inline-block; 317 | } 318 | 319 | summary { 320 | display: list-item; 321 | cursor: pointer; 322 | } 323 | 324 | template { 325 | display: none; 326 | } 327 | 328 | [hidden] { 329 | display: none !important; 330 | } 331 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | // Unobtrusive validation support library for jQuery and jQuery Validate 2 | // Copyright (c) .NET Foundation. All rights reserved. 3 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 4 | // @version v3.2.11 5 | !function(a){"function"==typeof define&&define.amd?define("jquery.validate.unobtrusive",["jquery-validation"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery-validation")):jQuery.validator.unobtrusive=a(jQuery)}(function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function u(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=f.unobtrusive.options||{},u=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),u("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),u("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),u("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var m,f=a.validator,v="unobtrusiveValidation";return f.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=u(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){f.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=u(this);a&&a.attachValidation()})}},m=f.unobtrusive.adapters,m.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},m.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},m.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},m.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},f.addMethod("__dummy__",function(a,e,n){return!0}),f.addMethod("regex",function(a,e,n){var t;return!!this.optional(e)||(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),f.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),f.methods.extension?(m.addSingleVal("accept","mimtype"),m.addSingleVal("extension","extension")):m.addSingleVal("extension","extension","accept"),m.addSingleVal("regex","pattern"),m.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),m.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),m.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),m.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),m.add("required",function(a){"INPUT"===a.element.tagName.toUpperCase()&&"CHECKBOX"===a.element.type.toUpperCase()||e(a,"required",!0)}),m.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),m.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),m.add("fileextensions",["extensions"],function(a){e(a,"extension",a.params.extensions)}),a(function(){f.unobtrusive.parse(document)}),f.unobtrusive}); -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/U2F.Demo/U2F.Demo/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /tests/UnitTests/CryptoServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Moq; 4 | using U2F.Core.Crypto; 5 | using U2F.Core.Exceptions; 6 | using Xunit; 7 | 8 | namespace U2F.Core.UnitTests 9 | { 10 | public class CryptoServiceTests 11 | { 12 | [Theory, ClassData(typeof(CryptoServices))] 13 | public void CryptoService2ChallengesShouldBeDifferent(ICryptoService generator) 14 | { 15 | byte[] firstChallenge = generator.GenerateChallenge(); 16 | byte[] secondChallenge = generator.GenerateChallenge(); 17 | 18 | Assert.True(firstChallenge.Length == secondChallenge.Length); 19 | Assert.False(firstChallenge.SequenceEqual(secondChallenge)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/UnitTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("UnitTEsts")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("6d8a572d-9f40-4423-b87a-0243dfee57e0")] 20 | -------------------------------------------------------------------------------- /tests/UnitTests/TestConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using U2F.Core.Crypto; 3 | using U2F.Core.Utils; 4 | using Xunit; 5 | 6 | namespace U2F.Core.UnitTests 7 | { 8 | internal static class TestConstants 9 | { 10 | //Test vectors from FIDO U2F: Raw Message Formats - Draft 4 11 | public static HashSet TRUSTED_DOMAINS = new HashSet() { "http://example.com" }; 12 | public static string APP_ID_ENROLL = "http://example.com"; 13 | public static string APP_SIGN_ID = "https://gstatic.com/securitykey/a/example.com"; 14 | public static string ORIGIN = "http://example.com"; 15 | 16 | public static string SERVER_CHALLENGE_REGISTER_BASE64 = "vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo"; 17 | public static string SERVER_CHALLENGE_SIGN_BASE64 = "opsXqUifDriAAmWclinfbS0e-USY0CgyJHe_Otd7z8o"; 18 | 19 | public static string CHANNEL_ID_STRING = 20 | "{\"kty\":\"EC\"," 21 | + "\"crv\":\"P-256\"," 22 | + "\"x\":\"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8\"," 23 | + "\"y\":\"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4\"}"; 24 | 25 | public static string CLIENT_DATA_REGISTER = 26 | "{\"typ\":\"navigator.id.finishEnrollment\",\"challenge\":\"vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo\",\"origin\":\"http://example.com\",\"cid_pubkey\":\"BNNo8bZlut48M6IPHkKcd1DVAzZgwBkRnSmqS6erwEqnyApGu-EcqMtWdNdPMfipA_a60QX7ardK7-9NuLACXh0\"}"; 27 | 28 | 29 | public static string CLIENT_DATA_REGISTER_BASE64 = "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6InZxclM2V1hEZTFKVXM1X2MzaTQtTGtLSUhSci0zWFZiM2F6dUE1VGlmSG8iLCJjaWRfcHVia2V5Ijp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiSHpRd2xmWFg3UTRTNU10Q0NuWlVOQnczUk16UE85dE95V2pCcVJsNHRKOCIsInkiOiJYVmd1R0ZMSVp4MWZYZzN3TnFmZGJuNzVoaTQtXzctQnhoTWxqdzQySHQ0In0sIm9yaWdpbiI6Imh0dHA6Ly9leGFtcGxlLmNvbSJ9"; 30 | 31 | public static string CLIENT_DATA_AUTHENTICATE = "{\"typ\":\"navigator.id.getAssertion\"," + "\"challenge\":\"" + SERVER_CHALLENGE_SIGN_BASE64 32 | + "\"," + "\"cid_pubkey\":" + CHANNEL_ID_STRING + "," + "\"origin\":\"" + ORIGIN + "\"}"; 33 | 34 | public static string CLIENT_DATA_AUTHENTICATE_BASE64 = "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoib3BzWHFVaWZEcmlBQW1XY2xpbmZiUzBlLVVTWTBDZ3lKSGVfT3RkN3o4byIsImNpZF9wdWJrZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJIelF3bGZYWDdRNFM1TXRDQ25aVU5CdzNSTXpQTzl0T3lXakJxUmw0dEo4IiwieSI6IlhWZ3VHRkxJWngxZlhnM3dOcWZkYm43NWhpNC1fNy1CeGhNbGp3NDJIdDQifSwib3JpZ2luIjoiaHR0cDovL2V4YW1wbGUuY29tIn0"; 35 | 36 | 37 | public static string REGISTRATION_RESPONSE_DATA_BASE64 = "BQSxdLxJx8olS3DS5cIHzunPF0gg69d+o8ZVCMJtpRtlfBzGuVL4YhaXk2SC2gptPTgmpZCV2vbNfAPi5gOF0vbZQCpVLf23R37WX9hBM/hhlgELIhW1faddMVt7no/i45JaYBlVG6th0WWRZZy68AtJUPer/mZg4uAG92hot3LXDCUwggE8MIHkoAMCAQICCkeQEoAAEVWVc1IwCgYIKoZIzj0EAwIwFzEVMBMGA1UEAxMMR251YmJ5IFBpbG90MB4XDTEyMDgxNDE4MjkzMloXDTEzMDgxNDE4MjkzMlowMTEvMC0GA1UEAxMmUGlsb3RHbnViYnktMC40LjEtNDc5MDEyODAwMDExNTU5NTczNTIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASNYX5lyVCOZLzFZzrIKmeZ2jwURmgsJYxGP//fWN/S+j5sN4tT15XEpN/7QZnt14YvI6uvAgO0uJEboFaZlOEBMAoGCCqGSM49BAMCA0cAMEQCIGDNtgYenCImLRqsHZbYxwgpsjZlMd2iaIMsuDa80w36AiBjGxRZ8J5jMAVXIsjYm39IiDuQibiNYNHZeVkCswQQ3zBFAiAUcYmbzDmH5i6CAsmznDPBkDP3NANS26gPyrAX25Iw5AIhAIJnfWc9iRkzreb2F+Xb3i4kfnBCP9WteASm09OWHvhx"; 38 | 39 | public static string KEY_HANDLE_BASE64 = "KlUt_bdHftZf2EEz-GGWAQsiFbV9p10xW3uej-LjklpgGVUbq2HRZZFlnLrwC0lQ96v-ZmDi4Ab3aGi3ctcMJQ"; 40 | 41 | public static byte[] KEY_HANDLE_BASE64_BYTE = KEY_HANDLE_BASE64.Base64StringToByteArray(); 42 | 43 | 44 | public static byte[] USER_PUBLIC_KEY_AUTHENTICATE_HEX = "BNNo8bZlut48M6IPHkKcd1DVAzZgwBkRnSmqS6erwEqnyApGu-EcqMtWdNdPMfipA_a60QX7ardK7-9NuLACXh0".Base64StringToByteArray(); 45 | 46 | 47 | public static string SIGN_RESPONSE_DATA_BASE64 = "AQAAAAEwRAIgS18M0XU0zt2MNO4JVw71QqNT30Q2AwzkPUBt6HC4R3gCICZ7uZj6ybcmbrYOfLC16r39W6lhT1PHsiJy7BAEepI_"; 48 | 49 | public static string ATTESTATION_CERTIFICATE = "MIIBPDCB5KADAgECAgpHkBKAABFVlXNSMAoGCCqGSM49BAMCMBcxFTATBgNVBAMTDEdudWJieSBQaWxvdDAeFw0xMjA4MTQxODI5MzJaFw0xMzA4MTQxODI5MzJaMDExLzAtBgNVBAMTJlBpbG90R251YmJ5LTAuNC4xLTQ3OTAxMjgwMDAxMTU1OTU3MzUyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjWF-ZclQjmS8xWc6yCpnmdo8FEZoLCWMRj__31jf0vo-bDeLU9eVxKTf-0GZ7deGLyOrrwIDtLiRG6BWmZThATAKBggqhkjOPQQDAgNHADBEAiBgzbYGHpwiJi0arB2W2McIKbI2ZTHdomiDLLg2vNMN-gIgYxsUWfCeYzAFVyLI2Jt_SIg7kIm4jWDR2XlZArMEEN8"; 50 | } 51 | 52 | internal class CryptoServices : TheoryData 53 | { 54 | public CryptoServices() 55 | { 56 | Add(new CryptoService()); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /tests/UnitTests/U2FTests.cs: -------------------------------------------------------------------------------- 1 | using U2F.Core.Crypto; 2 | using U2F.Core.Models; 3 | using U2F.Core.Utils; 4 | using Xunit; 5 | 6 | namespace U2F.Core.UnitTests 7 | { 8 | public class U2FTests 9 | { 10 | [Theory, ClassData(typeof(CryptoServices))] 11 | public void U2F_FinishRegistration(ICryptoService crypto) 12 | { 13 | Crypto.U2F.Crypto = crypto; 14 | 15 | StartedRegistration startedRegistration = 16 | new StartedRegistration(TestConstants.SERVER_CHALLENGE_REGISTER_BASE64, TestConstants.APP_ID_ENROLL); 17 | RegisterResponse registerResponse = new RegisterResponse(TestConstants.REGISTRATION_RESPONSE_DATA_BASE64, 18 | TestConstants.CLIENT_DATA_REGISTER_BASE64); 19 | 20 | DeviceRegistration results = 21 | U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse, 22 | TestConstants.TRUSTED_DOMAINS); 23 | 24 | Assert.NotNull(results); 25 | Assert.NotNull(results.KeyHandle); 26 | Assert.NotNull(results.PublicKey); 27 | Assert.NotNull(results.GetAttestationCertificate()); 28 | } 29 | 30 | [Theory, ClassData(typeof(CryptoServices))] 31 | public void U2F_FinishRegistrationNoFacets(ICryptoService crypto) 32 | { 33 | Crypto.U2F.Crypto = crypto; 34 | 35 | StartedRegistration startedRegistration = 36 | new StartedRegistration(TestConstants.SERVER_CHALLENGE_REGISTER_BASE64, TestConstants.APP_ID_ENROLL); 37 | RegisterResponse registerResponse = new RegisterResponse(TestConstants.REGISTRATION_RESPONSE_DATA_BASE64, 38 | TestConstants.CLIENT_DATA_REGISTER_BASE64); 39 | 40 | var results = U2F.Core.Crypto.U2F.FinishRegistration(startedRegistration, registerResponse); 41 | 42 | Assert.NotNull(results); 43 | Assert.NotNull(results.KeyHandle); 44 | Assert.NotNull(results.PublicKey); 45 | Assert.NotNull(results.GetAttestationCertificate()); 46 | } 47 | 48 | [Theory, ClassData(typeof(CryptoServices))] 49 | public void U2F_StartRegistration(ICryptoService crypto) 50 | { 51 | Crypto.U2F.Crypto = crypto; 52 | 53 | var results = U2F.Core.Crypto.U2F.StartRegistration(TestConstants.APP_ID_ENROLL); 54 | 55 | Assert.NotNull(results); 56 | Assert.NotNull(results.Challenge); 57 | Assert.NotNull(results.Version); 58 | Assert.Equal(results.AppId, TestConstants.APP_ID_ENROLL); 59 | } 60 | 61 | [Theory, ClassData(typeof(CryptoServices))] 62 | public void U2F_StartAuthentication(ICryptoService crypto) 63 | { 64 | Crypto.U2F.Crypto = crypto; 65 | 66 | RegisterResponse registerResponse = new RegisterResponse(TestConstants.REGISTRATION_RESPONSE_DATA_BASE64, 67 | TestConstants.CLIENT_DATA_REGISTER_BASE64); 68 | RawRegisterResponse rawAuthenticateResponse = 69 | RawRegisterResponse.FromBase64(registerResponse.RegistrationData); 70 | DeviceRegistration deviceRegistration = rawAuthenticateResponse.CreateDevice(); 71 | 72 | var results = U2F.Core.Crypto.U2F.StartAuthentication(TestConstants.APP_ID_ENROLL, deviceRegistration); 73 | 74 | Assert.NotNull(results); 75 | Assert.NotNull(results.AppId); 76 | Assert.NotNull(results.Challenge); 77 | Assert.NotNull(results.KeyHandle); 78 | Assert.NotNull(results.Version); 79 | } 80 | 81 | [Theory, ClassData(typeof(CryptoServices))] 82 | public void U2F_FinishAuthentication(ICryptoService crypto) 83 | { 84 | Crypto.U2F.Crypto = crypto; 85 | 86 | StartedAuthentication startedAuthentication = new StartedAuthentication( 87 | TestConstants.SERVER_CHALLENGE_SIGN_BASE64, 88 | TestConstants.APP_SIGN_ID, 89 | TestConstants.KEY_HANDLE_BASE64); 90 | 91 | AuthenticateResponse authenticateResponse = new AuthenticateResponse( 92 | TestConstants.CLIENT_DATA_AUTHENTICATE_BASE64, 93 | TestConstants.SIGN_RESPONSE_DATA_BASE64, 94 | TestConstants.KEY_HANDLE_BASE64); 95 | 96 | 97 | DeviceRegistration deviceRegistration = new DeviceRegistration(TestConstants.KEY_HANDLE_BASE64_BYTE, 98 | TestConstants.USER_PUBLIC_KEY_AUTHENTICATE_HEX, 99 | TestConstants.ATTESTATION_CERTIFICATE.Base64StringToByteArray(), 100 | 0); 101 | 102 | uint orginalValue = deviceRegistration.Counter; 103 | 104 | U2F.Core.Crypto.U2F.FinishAuthentication(startedAuthentication, authenticateResponse, deviceRegistration); 105 | 106 | Assert.True(deviceRegistration.Counter != 0); 107 | Assert.NotEqual(orginalValue, deviceRegistration.Counter); 108 | Assert.Equal(orginalValue + 1, deviceRegistration.Counter); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /tests/UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Unit tests for the U2F.Core library 5 | U2F Core Unit Tests 6 | net5.0 7 | UnitTests 8 | UnitTests 9 | true 10 | false 11 | false 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------