├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ └── dotnetcore.yml ├── .gitignore ├── LICENSE ├── README.md ├── TestAuthority.sln ├── _config.yml ├── source ├── TestAuthority.Application │ ├── CertificateBuilder2.cs │ ├── CertificateBuilder2Extensions.cs │ ├── CertificateBuilders │ │ ├── CertificateBuilderRequest.cs │ │ └── CertificateBuilderSteps │ │ │ ├── AuthorityKeyIdentifierExtensionBehavior.cs │ │ │ ├── BasicContstrainsExtensionBehaviour.cs │ │ │ ├── CertificateLoggingPostProcessor.cs │ │ │ ├── CertificateValidityBehaviour.cs │ │ │ ├── CommonNameBehaviour.cs │ │ │ ├── CrlDistributionPointExtensionBehaviour.cs │ │ │ ├── ExtendedKeyUsageExtensionBehaviour.cs │ │ │ ├── IssuerNameBehaviour.cs │ │ │ ├── KeyPairGenerationPreProcessor.cs │ │ │ ├── SerialNumberBehaviour.cs │ │ │ ├── SignCertificateBehaviour.cs │ │ │ └── SubjectAlternativeNameBehaviour.cs │ ├── CertificateConverters │ │ ├── CertificateConverterService.cs │ │ ├── ICertificateConverter.cs │ │ ├── ICrlConverter.cs │ │ ├── PemConverter.cs │ │ └── PfxConverter.cs │ ├── CrlBuilders │ │ ├── CrlBuilderRequest.cs │ │ └── CrlBuilderSteps │ │ │ └── GenerateCrlBehaviour.cs │ ├── CrlSettings.cs │ ├── Extensions │ │ └── CertificateSignerInfoExtensions.cs │ ├── Random │ │ └── RandomService.cs │ ├── RootWithIntermediateCertificateProvider.cs │ ├── SignatureFactoryProviders │ │ ├── ISignatureFactoryProvider.cs │ │ └── RsaSignatureFactoryProvider.cs │ ├── Store │ │ ├── PfxCertificateStore.cs │ │ └── PfxContainerOptions.cs │ ├── TestAuthority.Application.csproj │ └── TimeServer.cs ├── TestAuthority.Domain │ ├── CertificateConverters │ │ └── ICertificateConverterService.cs │ ├── ITimeServer.cs │ ├── Models │ │ ├── CertificateRequestModel.cs │ │ ├── CertificateSignerInfo.cs │ │ ├── CertificateWithKey.cs │ │ ├── CrlFileModel.cs │ │ └── OutputFormat.cs │ ├── Services │ │ ├── ICertificateBuilder.cs │ │ ├── IRandomService.cs │ │ └── ISignerProvider.cs │ ├── Store │ │ ├── ICertificateStore.cs │ │ └── IContainerOptions.cs │ └── TestAuthority.Domain.csproj ├── TestAuthority.Host │ ├── .dockerignore │ ├── Contracts │ │ ├── CertificateRequestModel.cs │ │ ├── ErrorModel.cs │ │ └── ErrorResponse.cs │ ├── Controllers │ │ ├── CertificateController.cs │ │ ├── CrlController.cs │ │ ├── DerToolsController.cs │ │ ├── PemToolsController.cs │ │ └── Pkcs12ToolsController.cs │ ├── Dockerfile │ ├── Extensions │ │ └── SwaggerExtensions.cs │ ├── Filters │ │ └── ValidationFilter.cs │ ├── Program.cs │ ├── Properties │ │ ├── PublishProfiles │ │ │ └── 10.0.15.211.pubxml │ │ └── launchSettings.json │ ├── Swagger │ │ └── FormFileSwaggerFilter.cs │ ├── TestAuthority.Host.csproj │ ├── Validators │ │ └── CertificateRequestValidator.cs │ ├── appsettings.Development.json │ └── appsettings.json └── TestAuthority.Infrastructure │ ├── Extensions │ └── CertificateAuthorityExtensions.cs │ └── TestAuthority.Infrastructure.csproj └── tests └── TestAuthority.UnitTests ├── CertificateHelper.cs ├── CertificateSignerInfoExtensions.cs ├── TestAuthority.UnitTests.csproj └── Usings.cs /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .env 3 | .git 4 | .gitignore 5 | .vs 6 | .vscode 7 | docker-compose.yml 8 | docker-compose.*.yml 9 | */bin 10 | */obj 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | root = true 5 | # All files 6 | [*] 7 | indent_style = space 8 | 9 | # XML project files 10 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 11 | indent_size = 2 12 | 13 | # XML config files 14 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 15 | indent_size = 2 16 | 17 | # Code files 18 | [*.{cs,csx,vb,vbx}] 19 | indent_size = 4 20 | insert_final_newline = true 21 | ############################### 22 | # .NET Coding Conventions # 23 | ############################### 24 | [*.{cs,vb}] 25 | # Organize using directives 26 | dotnet_sort_system_directives_first = true 27 | # this. preferences 28 | dotnet_style_qualification_for_field = false:silent 29 | dotnet_style_qualification_for_property = false:silent 30 | dotnet_style_qualification_for_method = false:silent 31 | dotnet_style_qualification_for_event = false:silent 32 | # Language keywords vs BCL types preferences 33 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 34 | dotnet_style_predefined_type_for_member_access = true:silent 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 37 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 38 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 39 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 40 | # Modifier preferences 41 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 42 | dotnet_style_readonly_field = true:suggestion 43 | # Expression-level preferences 44 | dotnet_style_object_initializer = true:suggestion 45 | dotnet_style_collection_initializer = true:suggestion 46 | dotnet_style_explicit_tuple_names = true:suggestion 47 | dotnet_style_null_propagation = true:suggestion 48 | dotnet_style_coalesce_expression = true:suggestion 49 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 50 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 51 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 52 | dotnet_style_prefer_auto_properties = true:silent 53 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 54 | dotnet_style_prefer_conditional_expression_over_return = true:silent 55 | ############################### 56 | # Naming Conventions # 57 | ############################### 58 | # Style Definitions 59 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 60 | # Use PascalCase for constant fields 61 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 62 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 63 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 64 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 65 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 66 | dotnet_naming_symbols.constant_fields.required_modifiers = const 67 | ############################### 68 | # C# Coding Conventions # 69 | ############################### 70 | [*.cs] 71 | # var preferences 72 | csharp_style_var_for_built_in_types = true:silent 73 | csharp_style_var_when_type_is_apparent = true:silent 74 | csharp_style_var_elsewhere = true:silent 75 | # Expression-bodied members 76 | csharp_style_expression_bodied_methods = false:silent 77 | csharp_style_expression_bodied_constructors = false:silent 78 | csharp_style_expression_bodied_operators = false:silent 79 | csharp_style_expression_bodied_properties = true:silent 80 | csharp_style_expression_bodied_indexers = true:silent 81 | csharp_style_expression_bodied_accessors = true:silent 82 | # Pattern matching preferences 83 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 84 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 85 | # Null-checking preferences 86 | csharp_style_throw_expression = true:suggestion 87 | csharp_style_conditional_delegate_call = true:suggestion 88 | # Modifier preferences 89 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 90 | # Expression-level preferences 91 | csharp_prefer_braces = true:silent 92 | csharp_style_deconstructed_variable_declaration = true:suggestion 93 | csharp_prefer_simple_default_expression = true:suggestion 94 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 95 | csharp_style_inlined_variable_declaration = true:suggestion 96 | ############################### 97 | # C# Formatting Rules # 98 | ############################### 99 | # New line preferences 100 | csharp_new_line_before_open_brace = all 101 | csharp_new_line_before_else = true 102 | csharp_new_line_before_catch = true 103 | csharp_new_line_before_finally = true 104 | csharp_new_line_before_members_in_object_initializers = true 105 | csharp_new_line_before_members_in_anonymous_types = true 106 | csharp_new_line_between_query_expression_clauses = true 107 | # Indentation preferences 108 | csharp_indent_case_contents = true 109 | csharp_indent_switch_labels = true 110 | csharp_indent_labels = flush_left 111 | # Space preferences 112 | csharp_space_after_cast = false 113 | csharp_space_after_keywords_in_control_flow_statements = true 114 | csharp_space_between_method_call_parameter_list_parentheses = false 115 | csharp_space_between_method_declaration_parameter_list_parentheses = false 116 | csharp_space_between_parentheses = false 117 | csharp_space_before_colon_in_inheritance_clause = true 118 | csharp_space_after_colon_in_inheritance_clause = true 119 | csharp_space_around_binary_operators = before_and_after 120 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 121 | csharp_space_between_method_call_name_and_opening_parenthesis = false 122 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 123 | # Wrapping preferences 124 | csharp_preserve_single_line_statements = true 125 | csharp_preserve_single_line_blocks = true -------------------------------------------------------------------------------- /.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: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '38 17 * * 3' 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' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Publish Docker Action 13 | uses: jerray/publish-docker-action@v1.0.3 14 | with: 15 | # Username used to login docker registry 16 | username: ${{ secrets.dockerHubUsername }} 17 | # Password used to login docker registry 18 | password: ${{ secrets.dockerHubSecret }} 19 | # Docker build context 20 | path: . 21 | file: source/TestAuthority.Host/Dockerfile 22 | repository: nomail/test-authority 23 | auto_tag: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | [Oo]ut/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | project.fragment.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | #*.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | # The packages folder can be ignored because of Package Restore 158 | **/packages/* 159 | # except build/, which is used as an MSBuild target. 160 | !**/packages/build/ 161 | # Uncomment if necessary however generally it will be regenerated when needed 162 | #!**/packages/repositories.config 163 | # NuGet v3's project.json files produces more ignoreable files 164 | *.nuget.props 165 | *.nuget.targets 166 | 167 | # Microsoft Azure Build Output 168 | csx/ 169 | *.build.csdef 170 | 171 | # Microsoft Azure Emulator 172 | ecf/ 173 | rcf/ 174 | 175 | # Windows Store app package directories and files 176 | AppPackages/ 177 | BundleArtifacts/ 178 | Package.StoreAssociation.xml 179 | _pkginfo.txt 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | ~$* 190 | *~ 191 | *.dbmdl 192 | *.dbproj.schemaview 193 | *.jfm 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # CodeRush 258 | .cr/ 259 | 260 | # Python Tools for Visual Studio (PTVS) 261 | __pycache__/ 262 | *.pyc 263 | Source/TestAuthority.Web/Properties/PublishProfiles/CustomProfile.pubxml 264 | Source/TestAuthority.Web/Properties/PublishProfiles/CustomProfile1.pubxml 265 | 266 | .vscode/** 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | # TestAuthority 2 | 3 | Provides an easy way to issue SSL certificate(PFX,PEM) for a specific host. 4 | Contains tools for conversion to/from PEM format from/to PFX (PKCS12) 5 | 6 | # Quickstart 7 | 8 | ## Requirements 9 | 10 | * .NET 6 https://www.microsoft.com/net/download 11 | 12 | To start Certificate Authority 13 | `dotnet TestAuthority.dll` 14 | 15 | To start project in docker container 16 | 17 | `docker run -p 5000:80 -d nomail/test-authority:latest` 18 | 19 | or 20 | 21 | ``` 22 | docker run \ 23 | -p 5000:80 \ 24 | -v /usr/share/test-authority:/usr/share/test-authority \ 25 | -e CrlSettings__CrlDistributionPoints__0=http://example.com/root.crl \ 26 | -e CrlSettings__CrlDistributionPoints__0=http://example.com/root1.crl \ 27 | -d \ 28 | nomail/test-authority:latest 29 | ``` 30 | 31 | # Usage 32 | 33 | Issue certificate for example.com 34 | 35 | `http://localhost:5000/api/certificate?commonName=test-certificate&hostname=example.com&ipaddress=10.10.1.10&format=pem` 36 | 37 | Get root certificate 38 | 39 | `http://localhost:5000/api/certificate/root` 40 | 41 | Get dummy CRL file 42 | 43 | `http://localhost:5000/api/crl` 44 | 45 | # Docker 46 | 47 | https://hub.docker.com/r/nomail/test-authority/ 48 | 49 | # Swagger enabled (WebUI) 50 | 51 | You can use swagger UI for simple and explicit certificate issue. 52 | 53 | `http://localhost:5000` 54 | -------------------------------------------------------------------------------- /TestAuthority.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestAuthority.Host", "source\TestAuthority.Host\TestAuthority.Host.csproj", "{B16E4C07-B615-4E92-8077-7A7510C06E7F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAuthority.Domain", "source\TestAuthority.Domain\TestAuthority.Domain.csproj", "{84A5DE1E-D523-4181-9581-C6E7C758588D}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAuthority.Application", "source\TestAuthority.Application\TestAuthority.Application.csproj", "{DF09E3D4-9DB3-495E-B7A0-389887480082}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAuthority.UnitTests", "tests\TestAuthority.UnitTests\TestAuthority.UnitTests.csproj", "{529EBD07-85D5-4A5D-8D4A-2240660D256D}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAuthority.Infrastructure", "source\TestAuthority.Infrastructure\TestAuthority.Infrastructure.csproj", "{CA16A683-5908-4455-9E2D-C329FD8DBBB5}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {B16E4C07-B615-4E92-8077-7A7510C06E7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {B16E4C07-B615-4E92-8077-7A7510C06E7F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {B16E4C07-B615-4E92-8077-7A7510C06E7F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {B16E4C07-B615-4E92-8077-7A7510C06E7F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {84A5DE1E-D523-4181-9581-C6E7C758588D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {84A5DE1E-D523-4181-9581-C6E7C758588D}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {84A5DE1E-D523-4181-9581-C6E7C758588D}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {84A5DE1E-D523-4181-9581-C6E7C758588D}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {DF09E3D4-9DB3-495E-B7A0-389887480082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {DF09E3D4-9DB3-495E-B7A0-389887480082}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {DF09E3D4-9DB3-495E-B7A0-389887480082}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {DF09E3D4-9DB3-495E-B7A0-389887480082}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {529EBD07-85D5-4A5D-8D4A-2240660D256D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {529EBD07-85D5-4A5D-8D4A-2240660D256D}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {529EBD07-85D5-4A5D-8D4A-2240660D256D}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {529EBD07-85D5-4A5D-8D4A-2240660D256D}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {CA16A683-5908-4455-9E2D-C329FD8DBBB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {CA16A683-5908-4455-9E2D-C329FD8DBBB5}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {CA16A683-5908-4455-9E2D-C329FD8DBBB5}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {CA16A683-5908-4455-9E2D-C329FD8DBBB5}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {28AAAB80-E9B0-48C6-B670-BD70A815080F} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilder2.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Asn1; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using Org.BouncyCastle.Crypto; 4 | using Org.BouncyCastle.Crypto.Generators; 5 | using Org.BouncyCastle.Crypto.Operators; 6 | using Org.BouncyCastle.Math; 7 | using Org.BouncyCastle.Security; 8 | using Org.BouncyCastle.Utilities; 9 | using Org.BouncyCastle.X509; 10 | using TestAuthority.Domain.Models; 11 | using TestAuthority.Domain.Services; 12 | 13 | namespace TestAuthority.Application; 14 | 15 | /// 16 | /// Certificate builder. 17 | /// 18 | public class CertificateBuilder2 : ICertificateBuilder 19 | { 20 | private const string SignatureAlgorithm = "SHA256WithRSA"; 21 | private readonly X509V3CertificateGenerator certificateGenerator = new(); 22 | private readonly SecureRandom random; 23 | 24 | private X509Name? issuerName; 25 | private AsymmetricCipherKeyPair? keyPair; 26 | 27 | private X509Name? subjectName; 28 | 29 | /// 30 | /// Ctor. 31 | /// 32 | /// Random value. 33 | public CertificateBuilder2(SecureRandom random) 34 | { 35 | this.random = random; 36 | var serialNumber = 37 | BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random); 38 | certificateGenerator.SetSerialNumber(serialNumber); 39 | } 40 | 41 | /// 42 | /// Get public key. 43 | /// 44 | /// 45 | public AsymmetricKeyParameter? GetPublicKeyInfo() 46 | { 47 | return keyPair?.Public; 48 | } 49 | 50 | /// 51 | public ICertificateBuilder AddExtension(string oid, Asn1Encodable value, bool isCritical = false) 52 | { 53 | certificateGenerator.AddExtension(oid, isCritical, value); 54 | return this; 55 | } 56 | 57 | /// 58 | public CertificateWithKey Generate(AsymmetricCipherKeyPair signerKeyPair) 59 | { 60 | Validate(); 61 | 62 | ISignatureFactory signatureFactory = new Asn1SignatureFactory(SignatureAlgorithm, signerKeyPair.Private, random); 63 | certificateGenerator.SetPublicKey(keyPair!.Public); 64 | 65 | var certificate = certificateGenerator.Generate(signatureFactory); 66 | certificate.Verify(signerKeyPair.Public); 67 | 68 | var result = new CertificateWithKey(certificate, keyPair); 69 | return result; 70 | } 71 | 72 | /// 73 | public ICertificateBuilder WithIssuerName(X509Name issuer) 74 | { 75 | ArgumentNullException.ThrowIfNull(issuer); 76 | issuerName = issuer; 77 | certificateGenerator.SetIssuerDN(issuer); 78 | return this; 79 | } 80 | 81 | /// 82 | public ICertificateBuilder WithNotAfter(DateTimeOffset notAfter) 83 | { 84 | certificateGenerator.SetNotAfter(notAfter.UtcDateTime); 85 | return this; 86 | } 87 | 88 | /// 89 | public ICertificateBuilder WithNotBefore(DateTimeOffset notBefore) 90 | { 91 | certificateGenerator.SetNotBefore(notBefore.UtcDateTime); 92 | return this; 93 | } 94 | 95 | /// 96 | public ICertificateBuilder WithSubject(X509Name subject) 97 | { 98 | ArgumentNullException.ThrowIfNull(subject); 99 | subjectName = subject; 100 | certificateGenerator.SetSubjectDN(subject); 101 | return this; 102 | } 103 | 104 | /// 105 | public ICertificateBuilder WithBasicConstraints(BasicConstrainsConstants constrains) 106 | { 107 | if (constrains == BasicConstrainsConstants.EndEntity) 108 | { 109 | certificateGenerator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(false)); 110 | return this; 111 | } 112 | 113 | certificateGenerator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(true)); 114 | return this; 115 | } 116 | 117 | /// 118 | public ICertificateBuilder WithKeyPair(AsymmetricCipherKeyPair value) 119 | { 120 | keyPair = value; 121 | return this; 122 | } 123 | 124 | /// 125 | /// Generate key pair. 126 | /// 127 | /// Key strength. 128 | /// . 129 | /// . 130 | public static AsymmetricCipherKeyPair GenerateKeyPair(int keyStrength, SecureRandom random) 131 | { 132 | var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength); 133 | var keyPairGenerator = new RsaKeyPairGenerator(); 134 | keyPairGenerator.Init(keyGenerationParameters); 135 | var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); 136 | return subjectKeyPair; 137 | } 138 | 139 | /// TODO: refactor 140 | private void Validate() 141 | { 142 | if (issuerName == null) throw new InvalidOperationException("Issuer is empty"); 143 | 144 | if (subjectName == null) throw new InvalidOperationException("Issuer is empty"); 145 | 146 | if (GetPublicKeyInfo() == null) throw new InvalidOperationException("PublicKeyInfo is empty"); 147 | 148 | if (keyPair == null) throw new InvalidOperationException("KeyPair must not be null"); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilder2Extensions.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Asn1; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using Org.BouncyCastle.Crypto; 4 | using Org.BouncyCastle.X509; 5 | using TestAuthority.Domain.Services; 6 | 7 | namespace TestAuthority.Application; 8 | 9 | /// 10 | /// Extensions for certificates. 11 | /// 12 | public static class CertificateBuilder2Extensions 13 | { 14 | /// 15 | /// Get from name components. 16 | /// 17 | /// Name components. 18 | /// Result. 19 | public static X509Name GetX509Name(Dictionary nameComponents) 20 | { 21 | var keys = nameComponents.Keys.ToArray(); 22 | var values = nameComponents.Values.ToArray(); 23 | 24 | return new X509Name(keys, values); 25 | } 26 | 27 | /// 28 | /// Set authority key identifier. 29 | /// 30 | /// . 31 | /// 32 | /// . 33 | public static ICertificateBuilder WithAuthorityKeyIdentifier(this ICertificateBuilder builder, 34 | AsymmetricCipherKeyPair authorityKeyPair) 35 | { 36 | var subjectPublicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(authorityKeyPair.Public); 37 | builder.AddExtension(X509Extensions.AuthorityKeyIdentifier.Id, new AuthorityKeyIdentifier(subjectPublicKeyInfo)); 38 | 39 | return builder; 40 | } 41 | 42 | public static ICertificateBuilder WithCrlDistributionPoint(this ICertificateBuilder builder, List crlAddresses) 43 | { 44 | if (crlAddresses.Any() == false) 45 | { 46 | return builder; 47 | } 48 | var distributionPoints = new List(); 49 | 50 | foreach (var distributionPointUri in crlAddresses) 51 | { 52 | var generalNames = new GeneralNames(new GeneralName(GeneralName.UniformResourceIdentifier, distributionPointUri)); 53 | var distributionPointName = new DistributionPointName(generalNames); 54 | var crlDistributionPoint = new DistributionPoint(distributionPointName, null, null); 55 | distributionPoints.Add(crlDistributionPoint); 56 | } 57 | 58 | 59 | var extension = new CrlDistPoint(distributionPoints.ToArray()); 60 | builder.AddExtension(X509Extensions.CrlDistributionPoints.Id, extension); 61 | 62 | return builder; 63 | } 64 | 65 | /// 66 | /// Set extended key usage(EKU) extension. 67 | /// 68 | /// . 69 | /// . 70 | public static ICertificateBuilder WithExtendedKeyUsage(this ICertificateBuilder builder) 71 | { 72 | var extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeID.IdKPClientAuth, KeyPurposeID.IdKPServerAuth); 73 | builder.AddExtension(X509Extensions.ExtendedKeyUsage.Id, extendedKeyUsage); 74 | 75 | return builder; 76 | } 77 | 78 | /// 79 | /// Set Subject Alternative Name extension. 80 | /// 81 | /// . 82 | /// Hostnames and domain names. 83 | /// IP addresses. 84 | /// . 85 | public static ICertificateBuilder WithSubjectAlternativeName(this ICertificateBuilder builder, 86 | List? hostnames = null, 87 | List? ipAddresses = null) 88 | { 89 | var result = new List(); 90 | 91 | hostnames?.Select(x => new GeneralName(GeneralName.DnsName, x)) 92 | .Select(x => x as Asn1Encodable) 93 | .ToList() 94 | .ForEach(result.Add); 95 | 96 | ipAddresses?.Select(x => new GeneralName(GeneralName.IPAddress, x)) 97 | .Select(x => x as Asn1Encodable) 98 | .ToList() 99 | .ForEach(result.Add); 100 | 101 | if (result.Any() == false) 102 | { 103 | return builder; 104 | } 105 | 106 | var extension = new DerSequence(result.ToArray()); 107 | builder.AddExtension(X509Extensions.SubjectAlternativeName.Id, extension); 108 | return builder; 109 | } 110 | 111 | /// 112 | /// Set subject common name. 113 | /// 114 | /// . 115 | /// Common name 116 | /// . 117 | public static ICertificateBuilder WithSubjectCommonName(this ICertificateBuilder builder, string? commonName) 118 | { 119 | ArgumentNullException.ThrowIfNull(commonName); 120 | 121 | var subjectComponents = new Dictionary { { X509Name.CN, commonName } }; 122 | 123 | var subject = GetX509Name(subjectComponents); 124 | 125 | builder.WithSubject(subject); 126 | 127 | return builder; 128 | } 129 | 130 | /// 131 | /// Set subject key identifier extension. 132 | /// 133 | /// . 134 | /// . 135 | public static ICertificateBuilder WithSubjectKeyIdentifier(this ICertificateBuilder builder) 136 | { 137 | var subjectPublicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(builder.GetPublicKeyInfo()); 138 | builder.AddExtension(X509Extensions.SubjectKeyIdentifier.Id, new SubjectKeyIdentifier(subjectPublicKeyInfo)); 139 | return builder; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderRequest.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Crypto; 3 | using Org.BouncyCastle.X509; 4 | using TestAuthority.Domain.Models; 5 | 6 | namespace TestAuthority.Application.CertificateBuilders; 7 | 8 | /// 9 | /// What an awful naming. Please do something about it. //TODO: Do it. 10 | /// 11 | public record CertificateBuilderRequest(CertificateRequestModel RequestModel, X509Certificate SignerCertificate, bool IsCaCertificate = false) : IRequest 12 | { 13 | public X509V3CertificateGenerator CertificateGenerator { get; } = new(); 14 | 15 | public AsymmetricCipherKeyPair? KeyPair { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/AuthorityKeyIdentifierExtensionBehavior.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using Org.BouncyCastle.X509; 4 | using TestAuthority.Domain.Models; 5 | 6 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 7 | 8 | public class AuthorityKeyIdentifierExtensionBehavior : IPipelineBehavior 9 | { 10 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 11 | { 12 | var subjectPublicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(request.SignerCertificate.GetPublicKey()); 13 | request.CertificateGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier.Id, false, new AuthorityKeyIdentifier(subjectPublicKeyInfo)); 14 | return await next(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/BasicContstrainsExtensionBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 6 | 7 | public class BasicConstraintsExtensionBehaviour : IPipelineBehavior 8 | { 9 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 10 | { 11 | if (request.IsCaCertificate) 12 | { 13 | request.CertificateGenerator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(true)); 14 | return await next(); 15 | } 16 | 17 | request.CertificateGenerator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(false)); 18 | return await next(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/CertificateLoggingPostProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Text; 3 | using MediatR.Pipeline; 4 | using Microsoft.Extensions.Logging; 5 | using TestAuthority.Domain.Models; 6 | 7 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 8 | 9 | public class CertificateLoggingPostProcessor: IRequestPostProcessor 10 | { 11 | private readonly ILogger logger; 12 | 13 | public CertificateLoggingPostProcessor(ILogger logger) 14 | { 15 | this.logger = logger; 16 | } 17 | 18 | public Task Process(CertificateBuilderRequest request, CertificateWithKey response, CancellationToken cancellationToken) 19 | { 20 | var sb = new StringBuilder(); 21 | var certificate = response.Certificate; 22 | 23 | sb.AppendLine("== Certificate Info =="); 24 | sb.AppendLine($"Subject: {certificate.SubjectDN.ToString()}"); 25 | sb.AppendLine($"Issuer: {certificate.IssuerDN.ToString()}"); 26 | sb.AppendLine($"Not Before: {certificate.NotBefore.ToString("g")}"); 27 | sb.AppendLine($"Not After: {certificate.NotAfter.ToString("g")}"); 28 | sb.AppendLine($"Serial Number {certificate.SerialNumber}"); 29 | sb.AppendLine("Extensions:"); 30 | sb.AppendLine(" Subject Alternative Name"); 31 | foreach (ArrayList subjectAlternativeName in certificate.GetSubjectAlternativeNames()) 32 | { 33 | sb.AppendLine($" - {subjectAlternativeName[1]}"); 34 | } 35 | logger.LogInformation("{CertificateInformation}",sb.ToString()); 36 | return Task.CompletedTask; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/CertificateValidityBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TestAuthority.Domain; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 6 | 7 | public class CertificateValidityBehaviour : IPipelineBehavior 8 | { 9 | private readonly ITimeServer timeServer; 10 | 11 | public CertificateValidityBehaviour(ITimeServer timeServer) 12 | { 13 | this.timeServer = timeServer; 14 | } 15 | 16 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 17 | { 18 | var notBefore = timeServer.Now.Subtract(TimeSpan.FromHours(5)).UtcDateTime; 19 | var notAfter = timeServer.Now.Add(TimeSpan.FromDays(request.RequestModel.ValidityInDays)).UtcDateTime; 20 | request.CertificateGenerator.SetNotBefore(notBefore); 21 | request.CertificateGenerator.SetNotAfter(notAfter); 22 | return await next(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/CommonNameBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 6 | 7 | public class CommonNameBehaviour : IPipelineBehavior 8 | { 9 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 10 | { 11 | request.CertificateGenerator.SetSubjectDN(new X509Name($"CN={request.RequestModel.CommonName}")); 12 | return await next(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/CrlDistributionPointExtensionBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.Options; 3 | using Org.BouncyCastle.Asn1.X509; 4 | using Org.BouncyCastle.Math; 5 | using TestAuthority.Domain.Models; 6 | 7 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 8 | 9 | public class CrlDistributionPointExtensionBehaviour : IPipelineBehavior 10 | { 11 | private readonly IOptions crlSettings; 12 | 13 | public CrlDistributionPointExtensionBehaviour(IOptions crlSettings) 14 | { 15 | this.crlSettings = crlSettings; 16 | } 17 | 18 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 19 | { 20 | var serialNumber = request.SignerCertificate.SerialNumber; 21 | var distributionPointUris = GetCrlAddresses(crlSettings.Value, serialNumber); 22 | if (distributionPointUris.Any() == false) 23 | { 24 | return await next(); 25 | } 26 | var distributionPoints = new List(); 27 | 28 | foreach (var distributionPointUri in distributionPointUris) 29 | { 30 | var generalNames = new GeneralNames(new GeneralName(GeneralName.UniformResourceIdentifier, distributionPointUri)); 31 | var distributionPointName = new DistributionPointName(generalNames); 32 | var crlDistributionPoint = new DistributionPoint(distributionPointName,null,null); 33 | distributionPoints.Add(crlDistributionPoint); 34 | } 35 | 36 | 37 | CrlDistPoint extension = new CrlDistPoint(distributionPoints.ToArray()); 38 | request.CertificateGenerator.AddExtension(X509Extensions.CrlDistributionPoints.Id, false, extension); 39 | return await next(); 40 | } 41 | 42 | private static List GetCrlAddresses(CrlSettings settings, BigInteger certificateSerialNumber) 43 | { 44 | return new List { $"{settings.CaAddress}/api/crl/{certificateSerialNumber.ToString(16)}" }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/ExtendedKeyUsageExtensionBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 6 | 7 | public class ExtendedKeyUsageExtensionBehaviour : IPipelineBehavior 8 | { 9 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 10 | { 11 | var extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeID.IdKPClientAuth, KeyPurposeID.IdKPServerAuth); 12 | request.CertificateGenerator.AddExtension(X509Extensions.ExtendedKeyUsage.Id, false, extendedKeyUsage); 13 | return await next(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/IssuerNameBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TestAuthority.Domain.Models; 3 | 4 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 5 | 6 | public class IssuerNameBehaviour : IPipelineBehavior 7 | { 8 | public Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 9 | { 10 | request.CertificateGenerator.SetIssuerDN(request.SignerCertificate.SubjectDN); 11 | return next(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/KeyPairGenerationPreProcessor.cs: -------------------------------------------------------------------------------- 1 | using MediatR.Pipeline; 2 | using TestAuthority.Domain.Services; 3 | 4 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 5 | 6 | public class KeyPairGenerationBehaviour : IRequestPreProcessor 7 | { 8 | private readonly IRandomService randomService; 9 | 10 | public KeyPairGenerationBehaviour(IRandomService randomService) 11 | { 12 | this.randomService = randomService; 13 | } 14 | 15 | public Task Process(CertificateBuilderRequest request, CancellationToken cancellationToken) 16 | { 17 | var keyPair = CertificateBuilder2.GenerateKeyPair(2048, randomService.GenerateRandom()); 18 | 19 | request.CertificateGenerator.SetPublicKey(keyPair.Public); 20 | request.KeyPair = keyPair; 21 | return Task.CompletedTask; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/SerialNumberBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Math; 3 | using Org.BouncyCastle.Utilities; 4 | using TestAuthority.Domain.Models; 5 | using TestAuthority.Domain.Services; 6 | 7 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 8 | 9 | public class SerialNumberBehaviour : IPipelineBehavior 10 | { 11 | private readonly IRandomService randomService; 12 | 13 | public SerialNumberBehaviour(IRandomService randomService) 14 | { 15 | this.randomService = randomService; 16 | } 17 | 18 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 19 | { 20 | var serialNumber = 21 | BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), randomService.GenerateRandom()); 22 | request.CertificateGenerator.SetSerialNumber(serialNumber); 23 | return await next(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/SignCertificateBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TestAuthority.Application.SignatureFactoryProviders; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 6 | 7 | public class SignCertificateBehaviour : IPipelineBehavior 8 | { 9 | private readonly ISignatureFactoryProvider signatureFactoryProvider; 10 | 11 | public SignCertificateBehaviour(ISignatureFactoryProvider signatureFactoryProvider) 12 | { 13 | this.signatureFactoryProvider = signatureFactoryProvider; 14 | } 15 | 16 | public Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 17 | { 18 | request.CertificateGenerator.SetPublicKey(request.KeyPair!.Public); 19 | 20 | var certificate = signatureFactoryProvider.Generate(request.CertificateGenerator); 21 | certificate.Verify(request.SignerCertificate.GetPublicKey()); 22 | 23 | var result = new CertificateWithKey(certificate, request.KeyPair); 24 | return Task.FromResult(result); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateBuilders/CertificateBuilderSteps/SubjectAlternativeNameBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Asn1; 3 | using Org.BouncyCastle.Asn1.X509; 4 | using TestAuthority.Domain.Models; 5 | 6 | namespace TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 7 | 8 | public class SubjectAlternativeNameBehaviour : IPipelineBehavior 9 | { 10 | public async Task Handle(CertificateBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 11 | { 12 | var result = new List(); 13 | 14 | request.RequestModel.Hostnames.Select(x => new GeneralName(GeneralName.DnsName, x)) 15 | .Select(x => x as Asn1Encodable) 16 | .ToList() 17 | .ForEach(result.Add); 18 | 19 | request.RequestModel.IpAddresses.Select(x => new GeneralName(GeneralName.IPAddress, x)) 20 | .Select(x => x as Asn1Encodable) 21 | .ToList() 22 | .ForEach(result.Add); 23 | 24 | var extension = new DerSequence(result.ToArray()); 25 | request.CertificateGenerator.AddExtension(X509Extensions.SubjectAlternativeName.Id, false, extension); 26 | return await next(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateConverters/CertificateConverterService.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TestAuthority.Domain.CertificateConverters; 3 | using TestAuthority.Domain.Models; 4 | using TestAuthority.Domain.Services; 5 | 6 | namespace TestAuthority.Application.CertificateConverters; 7 | 8 | /// 9 | /// Service that provides conversion of certificates to . 10 | /// 11 | public class CertificateConverterService : ICertificateConverterService 12 | { 13 | private readonly PemConverter pemConverter; 14 | private readonly PfxConverter pfxConverter; 15 | 16 | /// 17 | /// Ctor. 18 | /// 19 | /// . 20 | /// . 21 | /// . 22 | public CertificateConverterService(IRandomService randomService, ISignerProvider signerProvider, IMediator mediator) 23 | { 24 | pemConverter = new PemConverter(signerProvider, mediator); 25 | pfxConverter = new PfxConverter(signerProvider, randomService); 26 | } 27 | 28 | /// 29 | /// Convert certificate to zip archive with certificate and key in PEM format. 30 | /// 31 | /// . 32 | /// 33 | public async Task ConvertToPemArchiveAsync(CertificateWithKey certificate) 34 | { 35 | return await pemConverter.Convert(certificate); 36 | } 37 | 38 | /// 39 | /// Convert certificate to pfx file. 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// Password for pfx file. 45 | /// Pfx file as a byte array. 46 | public async Task ConvertToPfx(CertificateWithKey certificate, string password) 47 | { 48 | return await pfxConverter.Convert(certificate, new PfxConverter.PfxConverterOptions(password)); 49 | } 50 | 51 | /// 52 | public Task ConvertToPem(CrlFileModel crl) 53 | { 54 | return pemConverter.Convert(crl); 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateConverters/ICertificateConverter.cs: -------------------------------------------------------------------------------- 1 | using TestAuthority.Domain.Models; 2 | 3 | namespace TestAuthority.Application.CertificateConverters; 4 | 5 | public interface ICertificateConverter 6 | { 7 | Task Convert(CertificateWithKey input, ICertificateConverterOptions? options = null); 8 | } 9 | 10 | public interface ICertificateConverterOptions 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateConverters/ICrlConverter.cs: -------------------------------------------------------------------------------- 1 | using TestAuthority.Domain.Models; 2 | 3 | namespace TestAuthority.Application.CertificateConverters; 4 | 5 | public interface ICrlConverter 6 | { 7 | Task Convert(CrlFileModel input); 8 | } 9 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateConverters/PemConverter.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using System.Text; 3 | using MediatR; 4 | using Org.BouncyCastle.Crypto; 5 | using Org.BouncyCastle.OpenSsl; 6 | using Org.BouncyCastle.X509; 7 | using TestAuthority.Application.CrlBuilders; 8 | using TestAuthority.Domain.Models; 9 | using TestAuthority.Domain.Services; 10 | 11 | namespace TestAuthority.Application.CertificateConverters; 12 | 13 | public class PemConverter : ICertificateConverter, ICrlConverter 14 | { 15 | private readonly IMediator mediator; 16 | private readonly ISignerProvider signerProvider; 17 | public PemConverter(ISignerProvider signerProvider, IMediator mediator) 18 | { 19 | this.signerProvider = signerProvider; 20 | this.mediator = mediator; 21 | } 22 | 23 | public async Task Convert(CertificateWithKey input, ICertificateConverterOptions? options = null) 24 | { 25 | return await ConvertToPemArchiveCore(input.Certificate, input.KeyPair.Private); 26 | } 27 | 28 | public Task Convert(CrlFileModel input) 29 | { 30 | var pem = ConvertToPemFormat(input); 31 | return Task.FromResult(Encoding.ASCII.GetBytes(pem)); 32 | } 33 | 34 | private async Task ConvertToPemArchiveCore(X509Certificate certificate, AsymmetricKeyParameter keyPair) 35 | { 36 | var signerInfo = signerProvider.GetCertificateSignerInfo(); 37 | var rootCertificate = signerInfo.GetRootCertificate(); 38 | var intermediateCertificates = signerInfo.GetIntermediateCertificates(); 39 | 40 | 41 | var crls = await GetCrls(signerInfo); 42 | 43 | 44 | using var stream = new MemoryStream(); 45 | using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true)) 46 | { 47 | WriteEntry("root.crt", rootCertificate, archive); 48 | WriteEntry("private.key", keyPair, archive); 49 | WriteEntry("certificate.crt", certificate, archive); 50 | for (var index = 0; index < crls.Count; index++) 51 | { 52 | var crl = crls[index]; 53 | WriteEntry($"crl_{index}.crl", crl.Crl, archive); 54 | } 55 | for (var index = 0; index < intermediateCertificates.Count; index++) 56 | { 57 | var intermediate = intermediateCertificates[index]; 58 | WriteEntry($"intermediate_{index + 1}.crt", intermediate, archive); 59 | } 60 | WriteFullChainEntry("fullchain.crt", certificate, intermediateCertificates, archive); 61 | } 62 | return stream.ToArray(); 63 | } 64 | 65 | private async Task> GetCrls(CertificateSignerInfo signerInfo) 66 | { 67 | var serialNumberRequests = signerInfo.CertificateChain 68 | .Select(x => x.Certificate) 69 | .Select(x => x.SerialNumber.ToString(16)) 70 | .Select(x => new CrlBuilderRequest(signerInfo, x)); 71 | 72 | var result = new List(); 73 | foreach (var request in serialNumberRequests) 74 | { 75 | var crl = await mediator.Send(request); 76 | result.Add(crl); 77 | } 78 | return result; 79 | } 80 | 81 | private static void WriteEntry(string filename, object entry, ZipArchive archive) 82 | { 83 | var entryRecord = archive.CreateEntry(filename); 84 | using var entryStream = entryRecord.Open(); 85 | using var binaryWriter = new BinaryWriter(entryStream); 86 | var stringRepresentation = ConvertToPemFormat(entry); 87 | var result = Encoding.ASCII.GetBytes(stringRepresentation); 88 | binaryWriter.Write(result); 89 | } 90 | 91 | private static string ConvertToPemFormat(object input) 92 | { 93 | var generator = new MiscPemGenerator(input); 94 | 95 | string outputString; 96 | using (var textWriter = new StringWriter()) 97 | { 98 | var writer = new PemWriter(textWriter); 99 | writer.WriteObject(generator); 100 | writer.Writer.Flush(); 101 | outputString = textWriter.ToString().Trim('\r', '\n'); 102 | } 103 | 104 | if (string.IsNullOrWhiteSpace(outputString)) 105 | { 106 | throw new InvalidOperationException(); 107 | } 108 | 109 | return outputString; 110 | } 111 | 112 | private static void WriteFullChainEntry(string filename, X509Certificate certificate, List intermediate, ZipArchive archive) 113 | { 114 | var entryRecord = archive.CreateEntry(filename); 115 | using var entryStream = entryRecord.Open(); 116 | using var binaryWriter = new BinaryWriter(entryStream); 117 | var chain = new List { certificate }; 118 | chain.AddRange(intermediate); 119 | 120 | var stringBuilder = new StringBuilder(); 121 | foreach (var item in chain) 122 | { 123 | var stringRepresentation = ConvertToPemFormat(item); 124 | stringBuilder.AppendLine(stringRepresentation); 125 | } 126 | 127 | var result = Encoding.ASCII.GetBytes(stringBuilder.ToString()); 128 | binaryWriter.Write(result); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CertificateConverters/PfxConverter.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Crypto; 2 | using Org.BouncyCastle.Crypto.Parameters; 3 | using Org.BouncyCastle.Pkcs; 4 | using Org.BouncyCastle.X509; 5 | using TestAuthority.Application.Store; 6 | using TestAuthority.Domain.Models; 7 | using TestAuthority.Domain.Services; 8 | 9 | namespace TestAuthority.Application.CertificateConverters; 10 | 11 | public class PfxConverter : ICertificateConverter 12 | { 13 | private readonly IRandomService randomService; 14 | private readonly ISignerProvider signerProvider; 15 | public PfxConverter(ISignerProvider signerProvider, IRandomService randomService) 16 | { 17 | this.signerProvider = signerProvider; 18 | this.randomService = randomService; 19 | } 20 | 21 | public async Task Convert(CertificateWithKey input, ICertificateConverterOptions? options = null) 22 | { 23 | if (options is not PfxConverterOptions pfxOptions) 24 | { 25 | throw new ArgumentException($"Use {nameof(PfxContainerOptions)} for options"); 26 | } 27 | 28 | var password = pfxOptions.Password ?? string.Empty; 29 | 30 | return await ConvertToPfxCoreAsync(input.Certificate, (RsaPrivateCrtKeyParameters)input.KeyPair.Private, password); 31 | } 32 | 33 | private Task ConvertToPfxCoreAsync(X509Certificate certificate, AsymmetricKeyParameter rsaParams, string pfxPassword) 34 | { 35 | var store = new Pkcs12Store(); 36 | var random = randomService.GenerateRandom(); 37 | var friendlyName = certificate.SubjectDN.ToString(); 38 | 39 | var chain = BuildChainCertificate(certificate) 40 | .Select(x => new X509CertificateEntry(x)) 41 | .ToArray(); 42 | 43 | store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(rsaParams), chain); 44 | 45 | using var stream = new MemoryStream(); 46 | store.Save(stream, pfxPassword.ToCharArray(), random); 47 | return Task.FromResult(stream.ToArray()); 48 | } 49 | 50 | private IEnumerable BuildChainCertificate(X509Certificate certificate) 51 | { 52 | var signerInfo = signerProvider.GetCertificateSignerInfo(); 53 | 54 | var rootCertificate = signerInfo.GetRootCertificate(); 55 | var intermediateCertificates = signerInfo.GetIntermediateCertificates(); 56 | yield return certificate; 57 | foreach (var intermediateCertificate in intermediateCertificates) 58 | { 59 | yield return intermediateCertificate; 60 | } 61 | yield return rootCertificate; 62 | } 63 | 64 | public class PfxConverterOptions : ICertificateConverterOptions 65 | { 66 | public PfxConverterOptions(string? password) 67 | { 68 | Password = password; 69 | } 70 | public string? Password { get; } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CrlBuilders/CrlBuilderRequest.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.X509; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Application.CrlBuilders; 6 | 7 | /// 8 | /// What an awful naming. Please do something about it. //TODO: Do it. 9 | /// 10 | public record CrlBuilderRequest : IRequest 11 | { 12 | public CrlBuilderRequest(CertificateSignerInfo signerInfo) 13 | { 14 | SignerInfo = signerInfo; 15 | SerialNumber = signerInfo.CertificateChain.Last().Certificate.SerialNumber.ToString(16); 16 | } 17 | 18 | public CrlBuilderRequest(CertificateSignerInfo signerInfo, string serialNumber) 19 | { 20 | SignerInfo = signerInfo; 21 | SerialNumber = serialNumber; 22 | } 23 | 24 | public CertificateSignerInfo SignerInfo { get; } 25 | 26 | public X509V2CrlGenerator CrlGenerator { get; } = new(); 27 | 28 | public string SerialNumber { get; } = String.Empty; 29 | } 30 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CrlBuilders/CrlBuilderSteps/GenerateCrlBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using Org.BouncyCastle.Math; 4 | using Org.BouncyCastle.X509.Extension; 5 | using TestAuthority.Application.SignatureFactoryProviders; 6 | using TestAuthority.Domain; 7 | using TestAuthority.Domain.Models; 8 | 9 | namespace TestAuthority.Application.CrlBuilders.CrlBuilderSteps; 10 | 11 | public class GenerateCrlBehaviour : IPipelineBehavior 12 | { 13 | private readonly ISignatureFactoryProvider signatureFactoryProvider; 14 | 15 | private readonly ITimeServer timeServer; 16 | 17 | public GenerateCrlBehaviour(ITimeServer timeServer, ISignatureFactoryProvider signatureFactoryProvider) 18 | { 19 | this.timeServer = timeServer; 20 | this.signatureFactoryProvider = signatureFactoryProvider; 21 | } 22 | 23 | public Task Handle(CrlBuilderRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 24 | { 25 | var signerInfo = request.SerialNumber == String.Empty 26 | ? request.SignerInfo.CertificateChain.Last().Certificate 27 | : request.SignerInfo.CertificateChain.First(x => string.Equals(x.Certificate.SerialNumber.ToString(16), request.SerialNumber)).Certificate; 28 | 29 | request.CrlGenerator.SetThisUpdate(timeServer.Now.Subtract(TimeSpan.FromHours(5)).DateTime); 30 | request.CrlGenerator.SetNextUpdate(timeServer.Now.AddYears(1).DateTime); 31 | request.CrlGenerator.SetIssuerDN(signerInfo.SubjectDN); 32 | 33 | request.CrlGenerator.AddCrlEntry(BigInteger.One, DateTime.Now, CrlReason.KeyCompromise); 34 | 35 | request.CrlGenerator.AddExtension( 36 | X509Extensions.AuthorityKeyIdentifier, 37 | false, 38 | new AuthorityKeyIdentifierStructure(signerInfo.GetPublicKey())); 39 | 40 | var crlNumber = new BigInteger(timeServer.Now.ToString("yyyyMMddHHmm")); 41 | request.CrlGenerator.AddExtension(X509Extensions.CrlNumber, false, new CrlNumber(crlNumber)); 42 | 43 | var crl = signatureFactoryProvider.Generate(request.CrlGenerator); 44 | var result = new CrlFileModel(crl); 45 | return Task.FromResult(result); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/CrlSettings.cs: -------------------------------------------------------------------------------- 1 | namespace TestAuthority.Application; 2 | 3 | public class CrlSettings 4 | { 5 | public string[]? CrlDistributionPoints { get; set; } 6 | public string? CaAddress { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/Extensions/CertificateSignerInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Crypto; 2 | using Org.BouncyCastle.X509; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Application.Extensions; 6 | 7 | public static class CertificateSignerInfoExtensions 8 | { 9 | public static X509Certificate GetRootCertificate(this CertificateSignerInfo signerInfo) 10 | { 11 | return signerInfo.CertificateChain.Select(x => x.Certificate).Last(); 12 | } 13 | 14 | public static IEnumerable GetIntermediateCertificates(this CertificateSignerInfo signerInfo) 15 | { 16 | if (signerInfo.CertificateChain.Count == 1) 17 | { 18 | return new List(); 19 | } 20 | 21 | return signerInfo.CertificateChain.Take(Range.EndAt(signerInfo.CertificateChain.Count - 1)); 22 | } 23 | 24 | public static X509Certificate GetSignerCertificate(this CertificateSignerInfo signerInfo) 25 | { 26 | return signerInfo.CertificateChain.First().Certificate; 27 | } 28 | 29 | public static AsymmetricCipherKeyPair GetSignerKeyPair(this CertificateSignerInfo signerInfo) 30 | { 31 | return signerInfo.CertificateChain.First().KeyPair; 32 | } 33 | 34 | public static AsymmetricKeyParameter GetSignerPrivateKey(this CertificateSignerInfo signerInfo) 35 | { 36 | return signerInfo.GetSignerKeyPair().Private; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/Random/RandomService.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Crypto.Prng; 2 | using Org.BouncyCastle.Security; 3 | using TestAuthority.Domain.Services; 4 | 5 | namespace TestAuthority.Application.Random; 6 | 7 | /// 8 | /// Service that provides random values. 9 | /// 10 | public class RandomService : IRandomService 11 | { 12 | private readonly SecureRandom random; 13 | private readonly CryptoApiRandomGenerator randomGenerator = new(); 14 | 15 | /// 16 | /// Ctor. 17 | /// 18 | public RandomService() 19 | { 20 | random = new SecureRandom(randomGenerator); 21 | } 22 | 23 | 24 | /// 25 | public SecureRandom GenerateRandom() 26 | { 27 | return random; 28 | } 29 | } -------------------------------------------------------------------------------- /source/TestAuthority.Application/RootWithIntermediateCertificateProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Org.BouncyCastle.Asn1; 3 | using Org.BouncyCastle.Asn1.X509; 4 | using Org.BouncyCastle.Math; 5 | using Org.BouncyCastle.Security; 6 | using TestAuthority.Application.Store; 7 | using TestAuthority.Domain.Models; 8 | using TestAuthority.Domain.Services; 9 | using TestAuthority.Domain.Store; 10 | 11 | namespace TestAuthority.Application; 12 | 13 | /// 14 | /// Provides methods for root certificate management. 15 | /// 16 | public class RootWithIntermediateCertificateProvider : ISignerProvider 17 | { 18 | private const string RootCertificateName = "Root"; 19 | private const string IntermediateCertificateName = "intermediate"; 20 | private const string Password = "123123123"; 21 | 22 | private readonly Func builderFactory; 23 | private readonly ICertificateStore certificateStore; 24 | private readonly IRandomService randomService; 25 | private readonly IOptions crlSettings; 26 | 27 | /// 28 | /// Ctor. 29 | /// 30 | public RootWithIntermediateCertificateProvider(ICertificateStore certificateStore, IRandomService randomService, IOptions crlSettings) 31 | { 32 | this.certificateStore = certificateStore; 33 | this.randomService = randomService; 34 | this.crlSettings = crlSettings; 35 | builderFactory = random => new CertificateBuilder2(random); 36 | } 37 | 38 | /// 39 | /// Get root certificate. 40 | /// 41 | /// Root certificate. 42 | public CertificateSignerInfo GetCertificateSignerInfo() 43 | { 44 | if (!certificateStore.TryGet(RootCertificateName, new PfxContainerOptions{ PfxPassword = Password}, out var rootCertificate)) 45 | { 46 | rootCertificate = GenerateRootCertificate(); 47 | certificateStore.SaveCertificate(RootCertificateName, rootCertificate, new PfxContainerOptions{ PfxPassword = Password}); 48 | } 49 | 50 | if (!certificateStore.TryGet(IntermediateCertificateName, new PfxContainerOptions{ PfxPassword = Password}, out var intermediateCertificate)) 51 | { 52 | intermediateCertificate = GenerateIntermediateCertificate(rootCertificate); 53 | certificateStore.SaveCertificate(IntermediateCertificateName, intermediateCertificate, new PfxContainerOptions{ PfxPassword = Password}); 54 | } 55 | 56 | var chain = new List { intermediateCertificate, rootCertificate }; 57 | 58 | 59 | return new CertificateSignerInfo(chain); 60 | } 61 | 62 | private CertificateWithKey GenerateIntermediateCertificate(CertificateWithKey signerCertificate) 63 | { 64 | var random = randomService.GenerateRandom(); 65 | var commonName = $"Intermediate Test Authority {DateTime.UtcNow:MM/yyyy}"; 66 | var notBefore = DateTimeOffset.UtcNow.AddHours(-2); 67 | var notAfter = DateTimeOffset.UtcNow.AddYears(10); 68 | 69 | var builder = builderFactory(random); 70 | 71 | var keyPair = CertificateBuilder2.GenerateKeyPair(2048, random); 72 | 73 | var certificate = builder 74 | .WithSubjectCommonName(commonName) 75 | .WithKeyPair(keyPair) 76 | .WithNotAfter(notAfter) 77 | .WithNotBefore(notBefore) 78 | .WithBasicConstraints(BasicConstrainsConstants.CertificateAuthority) 79 | .WithAuthorityKeyIdentifier(signerCertificate.KeyPair) 80 | .WithSubjectKeyIdentifier() 81 | .WithIssuerName(signerCertificate.Certificate.SubjectDN) 82 | .WithCrlDistributionPoint(GetCrlAddresses(crlSettings.Value, signerCertificate.Certificate.SerialNumber)) 83 | .Generate(signerCertificate.KeyPair); 84 | 85 | return certificate; 86 | } 87 | 88 | private static List GetCrlAddresses(CrlSettings settings, BigInteger certificateSerialNumber) 89 | { 90 | return new List { $"{settings.CaAddress}/api/crl/{certificateSerialNumber.ToString(16)}" }; 91 | } 92 | 93 | private CertificateWithKey GenerateRootCertificate() 94 | { 95 | var random = randomService.GenerateRandom(); 96 | var commonName = $"Test Authority {DateTime.UtcNow:MM/yyyy}"; 97 | var notBefore = DateTimeOffset.UtcNow.AddHours(-2); 98 | var notAfter = DateTimeOffset.UtcNow.AddYears(15); 99 | 100 | var builder = builderFactory(random); 101 | 102 | var keyPair = CertificateBuilder2.GenerateKeyPair(2048, random); 103 | 104 | var issuerName = BuildCommonName(commonName); 105 | 106 | var certificate = builder 107 | .WithSubject(issuerName) 108 | .WithKeyPair(keyPair) 109 | .WithNotAfter(notAfter) 110 | .WithNotBefore(notBefore) 111 | .WithBasicConstraints(BasicConstrainsConstants.CertificateAuthority) 112 | .WithAuthorityKeyIdentifier(keyPair) 113 | .WithSubjectKeyIdentifier() 114 | .WithIssuerName(issuerName) 115 | .Generate(keyPair); 116 | 117 | return certificate; 118 | } 119 | 120 | private static X509Name BuildCommonName(string commonName) 121 | { 122 | ArgumentNullException.ThrowIfNull(commonName); 123 | var issuerName = new Dictionary 124 | { 125 | { X509Name.CN, commonName } 126 | }; 127 | return CertificateBuilder2Extensions.GetX509Name(issuerName); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/SignatureFactoryProviders/ISignatureFactoryProvider.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.X509; 2 | 3 | namespace TestAuthority.Application.SignatureFactoryProviders; 4 | 5 | /// 6 | /// Signature factory provider. 7 | /// 8 | public interface ISignatureFactoryProvider 9 | { 10 | /// 11 | /// Generate Crl. 12 | /// 13 | /// . 14 | /// . 15 | X509Crl Generate(X509V2CrlGenerator crlGenerator); 16 | 17 | /// 18 | /// Generate certificate. 19 | /// 20 | /// . 21 | /// . 22 | X509Certificate Generate(X509V3CertificateGenerator certificateGenerator); 23 | } 24 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/SignatureFactoryProviders/RsaSignatureFactoryProvider.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Crypto.Operators; 2 | using Org.BouncyCastle.X509; 3 | using TestAuthority.Application.Extensions; 4 | using TestAuthority.Domain.Services; 5 | 6 | namespace TestAuthority.Application.SignatureFactoryProviders; 7 | 8 | /// 9 | /// Provides a factory that signs crls and certificates using SHA256WithRSA signature algorithm. 10 | /// 11 | public class RsaSignatureFactoryProvider : ISignatureFactoryProvider 12 | { 13 | private const string SignatureAlgorithm = "SHA256WithRSA"; 14 | private readonly Asn1SignatureFactory signatureFactory; 15 | 16 | public RsaSignatureFactoryProvider(ISignerProvider signerProvider, IRandomService randomService) 17 | { 18 | var privateKey = signerProvider.GetCertificateSignerInfo().GetSignerPrivateKey(); 19 | signatureFactory = new Asn1SignatureFactory(SignatureAlgorithm, privateKey, randomService.GenerateRandom()); 20 | } 21 | 22 | /// 23 | public X509Crl Generate(X509V2CrlGenerator crlGenerator) 24 | { 25 | return crlGenerator.Generate(signatureFactory); 26 | } 27 | 28 | /// 29 | public X509Certificate Generate(X509V3CertificateGenerator certificateGenerator) 30 | { 31 | return certificateGenerator.Generate(signatureFactory); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/Store/PfxCertificateStore.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text; 3 | using Org.BouncyCastle.Crypto; 4 | using Org.BouncyCastle.Crypto.Parameters; 5 | using Org.BouncyCastle.Pkcs; 6 | using Org.BouncyCastle.X509; 7 | using TestAuthority.Domain.Models; 8 | using TestAuthority.Domain.Services; 9 | using TestAuthority.Domain.Store; 10 | 11 | namespace TestAuthority.Application.Store; 12 | 13 | /// 14 | /// Certificate store. Should be a vault of some sort. But... ehh. 15 | /// 16 | public class PfxCertificateStore : ICertificateStore 17 | { 18 | private readonly IRandomService randomService; 19 | 20 | public PfxCertificateStore(IRandomService randomService) 21 | { 22 | this.randomService = randomService; 23 | } 24 | 25 | /// 26 | public bool TryGet(string certificateName, IContainerOptions options, [MaybeNullWhen(false)] out CertificateWithKey certificateWithKey) 27 | { 28 | if (Exists(certificateName) == false) 29 | { 30 | certificateWithKey = default; 31 | return false; 32 | } 33 | certificateWithKey = GetCertificate(certificateName, options); 34 | return true; 35 | } 36 | 37 | /// 38 | public void SaveCertificate(ReadOnlySpan certificateName, CertificateWithKey certificateWithKey, IContainerOptions options) 39 | { 40 | if (options is PfxContainerOptions pfxOptions == false) 41 | { 42 | throw new ArgumentException("Options must be of type PfxContainerOptions", nameof(options)); 43 | } 44 | ArgumentNullException.ThrowIfNull(pfxOptions.PfxPassword); 45 | 46 | var path = GetCertificatePath(certificateName); 47 | var rawData = ConvertToPfx(certificateWithKey.Certificate, (RsaPrivateCrtKeyParameters)certificateWithKey.KeyPair.Private, pfxOptions.PfxPassword); 48 | 49 | File.WriteAllBytes(path, rawData); 50 | } 51 | 52 | 53 | private bool Exists(string certificateName) 54 | { 55 | var path = GetCertificatePath(certificateName); 56 | return File.Exists(path); 57 | } 58 | 59 | private CertificateWithKey GetCertificate(string certificateName, IContainerOptions options) 60 | { 61 | if (options is PfxContainerOptions pfxOptions == false) 62 | { 63 | throw new ArgumentException("Options must be of type PfxContainerOptions", nameof(options)); 64 | } 65 | var path = GetCertificatePath(certificateName); 66 | var rawData = File.ReadAllBytes(path); 67 | return Convert(rawData, pfxOptions.PfxPassword); 68 | } 69 | 70 | private static string GetCertificatePath(ReadOnlySpan certificateName) 71 | { 72 | var sb = new StringBuilder(); 73 | sb.Append(certificateName); 74 | sb.Append(".pfx"); 75 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "test-authority", sb.ToString()); 76 | } 77 | 78 | private static CertificateWithKey Convert(byte[] pfxCertificate, ReadOnlySpan password) 79 | { 80 | using var stream = new MemoryStream(pfxCertificate); 81 | var store = new Pkcs12Store(); 82 | store.Load(stream, password.ToArray()); 83 | var alias = store.Aliases.OfType().Single(); 84 | var certificateEntry = store.GetCertificate(alias); 85 | var keyEntry = store.GetKey(alias); 86 | 87 | var asymmetricCipherKeyPair = new AsymmetricCipherKeyPair(certificateEntry.Certificate.GetPublicKey(), keyEntry.Key); 88 | var result = new CertificateWithKey(certificateEntry.Certificate, asymmetricCipherKeyPair); 89 | return result; 90 | } 91 | 92 | private byte[] ConvertToPfx(X509Certificate cert, AsymmetricKeyParameter rsaParams, string pfxPassword) 93 | { 94 | var store = new Pkcs12Store(); 95 | var random = randomService.GenerateRandom(); 96 | var friendlyName = cert.SubjectDN.ToString(); 97 | var certificateEntry = new X509CertificateEntry(cert); 98 | 99 | store.SetCertificateEntry(friendlyName, certificateEntry); 100 | store.SetKeyEntry( 101 | friendlyName, 102 | new AsymmetricKeyEntry(rsaParams), 103 | new[] { certificateEntry }); 104 | 105 | using var stream = new MemoryStream(); 106 | store.Save(stream, pfxPassword.ToCharArray(), random); 107 | 108 | return stream.ToArray(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/Store/PfxContainerOptions.cs: -------------------------------------------------------------------------------- 1 | using TestAuthority.Domain.Store; 2 | 3 | namespace TestAuthority.Application.Store; 4 | 5 | /// 6 | /// Options that are used in . 7 | /// 8 | public class PfxContainerOptions : IContainerOptions 9 | { 10 | /// 11 | /// Pfx password. 12 | /// 13 | public string? PfxPassword { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/TestAuthority.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | .editorconfig 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /source/TestAuthority.Application/TimeServer.cs: -------------------------------------------------------------------------------- 1 | using TestAuthority.Domain; 2 | 3 | namespace TestAuthority.Application; 4 | 5 | /// 6 | public class TimeServer: ITimeServer 7 | { 8 | /// 9 | public DateTimeOffset Now => DateTimeOffset.Now; 10 | } 11 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/CertificateConverters/ICertificateConverterService.cs: -------------------------------------------------------------------------------- 1 | using TestAuthority.Domain.Models; 2 | 3 | namespace TestAuthority.Domain.CertificateConverters; 4 | 5 | /// 6 | /// Provides methods to convert certificates and keys. 7 | /// 8 | public interface ICertificateConverterService 9 | { 10 | /// 11 | /// Convert certificate with key to ZIP archive containing certificate and key in PEM format. 12 | /// 13 | /// . 14 | /// Zip archive as a byte array. 15 | Task ConvertToPemArchiveAsync(CertificateWithKey certificate); 16 | 17 | /// 18 | /// Convert certificate with key to pfx file. 19 | /// 20 | /// . 21 | /// Pfx password. 22 | /// Pfx file as a byte array. 23 | Task ConvertToPfx(CertificateWithKey certificate, string password); 24 | 25 | /// 26 | /// Convert CRL to pem format. 27 | /// 28 | /// . 29 | /// Pem representation of Crl. 30 | Task ConvertToPem(CrlFileModel crl); 31 | } 32 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/ITimeServer.cs: -------------------------------------------------------------------------------- 1 | namespace TestAuthority.Domain; 2 | 3 | public interface ITimeServer 4 | { 5 | DateTimeOffset Now { get; } 6 | } 7 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Models/CertificateRequestModel.cs: -------------------------------------------------------------------------------- 1 | namespace TestAuthority.Domain.Models; 2 | 3 | /// 4 | /// Request for certificate. 5 | /// 6 | public record CertificateRequestModel 7 | { 8 | /// 9 | /// Common name of the certificate. 10 | /// 11 | public string? CommonName { get; init; } 12 | 13 | /// 14 | /// List of domain names to be included in Subject Alternative Name extension 15 | /// 16 | public List Hostnames { get; init; } = new(); 17 | 18 | /// 19 | /// List of IP addresses to be included in Subject Alternative Name extension 20 | /// 21 | public List IpAddresses { get; init; } = new(); 22 | 23 | /// 24 | /// Certificate validity in days. 25 | /// 26 | /// Default value of 364. 27 | public int ValidityInDays { get; init; } = 364; 28 | } 29 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Models/CertificateSignerInfo.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Asn1.X509; 2 | using Org.BouncyCastle.X509; 3 | 4 | namespace TestAuthority.Domain.Models; 5 | 6 | /// 7 | /// Signer information. 8 | /// 9 | /// 10 | public record CertificateSignerInfo(List CertificateChain) 11 | { 12 | public X509Name? Subject => CertificateChain.First().Certificate.SubjectDN; 13 | 14 | /// 15 | /// Chain of the certificates with keys. 16 | /// 17 | /// 18 | /// The first element of the list is a certificate used to sign certificates and crls. 19 | /// The last element is the root certificate. 20 | /// If the list contains only one certificate, then the root certificate will be used to sign end certifcates. 21 | /// Elements [1..n-1] are intermediate certificates. 22 | /// 23 | public List CertificateChain { get; } = CertificateChain; 24 | 25 | public CertificateWithKey GetSignerCertificate() 26 | { 27 | return CertificateChain.First(); 28 | } 29 | 30 | public X509Certificate GetRootCertificate() 31 | { 32 | return CertificateChain.Last().Certificate; 33 | } 34 | 35 | public List GetIntermediateCertificates() 36 | { 37 | if (CertificateChain.Count < 2) 38 | { 39 | return new List(); 40 | } 41 | return CertificateChain.GetRange(0, CertificateChain.Count - 1).ConvertAll(x => x.Certificate); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Models/CertificateWithKey.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Crypto; 2 | using Org.BouncyCastle.X509; 3 | 4 | namespace TestAuthority.Domain.Models; 5 | 6 | /// 7 | /// Wrapper for certificate with keys. 8 | /// 9 | public record CertificateWithKey(X509Certificate Certificate, AsymmetricCipherKeyPair KeyPair) 10 | { 11 | /// 12 | /// Certificate. 13 | /// 14 | public X509Certificate Certificate { get; } = Certificate; 15 | 16 | /// 17 | /// Key pair. 18 | /// 19 | public AsymmetricCipherKeyPair KeyPair { get; } = KeyPair; 20 | } 21 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Models/CrlFileModel.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.X509; 2 | 3 | namespace TestAuthority.Domain.Models; 4 | 5 | /// 6 | /// Crl wrapper. 7 | /// 8 | public record CrlFileModel(X509Crl Crl); 9 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Models/OutputFormat.cs: -------------------------------------------------------------------------------- 1 | namespace TestAuthority.Domain.Models; 2 | 3 | /// 4 | /// Represent certificate output format 5 | /// 6 | public enum OutputFormat 7 | { 8 | /// 9 | /// Pfx file. 10 | /// 11 | Pfx, 12 | 13 | /// 14 | /// Zip archive with certificate and key in PEM format. 15 | /// 16 | Pem 17 | } -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Services/ICertificateBuilder.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Asn1; 2 | using Org.BouncyCastle.Asn1.X509; 3 | using Org.BouncyCastle.Crypto; 4 | using TestAuthority.Domain.Models; 5 | 6 | namespace TestAuthority.Domain.Services; 7 | 8 | /// 9 | /// Builds certificates since 1968. 10 | /// 11 | public interface ICertificateBuilder 12 | { 13 | /// 14 | /// Add extension. 15 | /// 16 | /// Oid. 17 | /// Whether this extension should be marked as critical. 18 | /// Extension value. 19 | /// . 20 | ICertificateBuilder AddExtension(string oid, Asn1Encodable value, bool isCritical = false); 21 | 22 | /// 23 | /// Generate certificate. 24 | /// 25 | /// Signer. 26 | /// . 27 | CertificateWithKey Generate(AsymmetricCipherKeyPair signerPrivateKey); 28 | 29 | /// 30 | /// Set issuer. 31 | /// 32 | /// Issuer. 33 | /// . 34 | ICertificateBuilder WithIssuerName(X509Name issuer); 35 | 36 | /// 37 | /// Set certificate not after date. 38 | /// 39 | /// Date. 40 | /// . 41 | ICertificateBuilder WithNotAfter(DateTimeOffset notAfter); 42 | 43 | 44 | /// 45 | /// Set certificate not before date. 46 | /// 47 | /// Date. 48 | /// . 49 | ICertificateBuilder WithNotBefore(DateTimeOffset notBefore); 50 | 51 | /// 52 | /// Set subject. 53 | /// 54 | /// Subject. 55 | /// . 56 | ICertificateBuilder WithSubject(X509Name subject); 57 | 58 | /// 59 | /// Set basic constraints for the certificate. 60 | /// 61 | /// . 62 | /// . 63 | ICertificateBuilder WithBasicConstraints(BasicConstrainsConstants constrains); 64 | 65 | /// 66 | /// Provide keypair for the certificate. 67 | /// 68 | /// Keypair 69 | /// . 70 | ICertificateBuilder WithKeyPair(AsymmetricCipherKeyPair keyPair); 71 | 72 | /// 73 | /// Get public key. 74 | /// 75 | /// 76 | AsymmetricKeyParameter? GetPublicKeyInfo(); 77 | } 78 | 79 | /// 80 | /// Enum for basic constrains. 81 | /// 82 | public enum BasicConstrainsConstants 83 | { 84 | /// 85 | /// End entity. 86 | /// 87 | EndEntity = 1, 88 | 89 | /// 90 | /// Certificate authority. 91 | /// 92 | CertificateAuthority 93 | } 94 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Services/IRandomService.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Security; 2 | 3 | namespace TestAuthority.Domain.Services; 4 | 5 | public interface IRandomService 6 | { 7 | /// 8 | /// Generate random value. 9 | /// 10 | /// Random value. 11 | SecureRandom GenerateRandom(); 12 | } -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Services/ISignerProvider.cs: -------------------------------------------------------------------------------- 1 | using TestAuthority.Domain.Models; 2 | 3 | namespace TestAuthority.Domain.Services; 4 | 5 | public interface ISignerProvider 6 | { 7 | /// 8 | /// Get root certificate. 9 | /// 10 | /// Root certificate. 11 | CertificateSignerInfo GetCertificateSignerInfo(); 12 | } 13 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Store/ICertificateStore.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using TestAuthority.Domain.Models; 3 | 4 | namespace TestAuthority.Domain.Store; 5 | 6 | /// 7 | /// Certificate store. 8 | /// 9 | public interface ICertificateStore 10 | { 11 | /// 12 | /// Save certificate to the store. 13 | /// 14 | /// Certificate name. 15 | /// Certificate. 16 | /// Options. 17 | void SaveCertificate(ReadOnlySpan certificateName, CertificateWithKey certificateWithKey, IContainerOptions options); 18 | 19 | /// 20 | /// Returns certificate via out parameter. 21 | /// 22 | /// Certificate name. 23 | /// . 24 | /// Certificate. 25 | /// True if certificate was found. False otherwise 26 | bool TryGet(string certificateName, IContainerOptions options, [MaybeNullWhen(false)] out CertificateWithKey certificateWithKey); 27 | } 28 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/Store/IContainerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace TestAuthority.Domain.Store; 2 | 3 | public interface IContainerOptions 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /source/TestAuthority.Domain/TestAuthority.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | .editorconfig 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/.idea 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /source/TestAuthority.Host/Contracts/CertificateRequestModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using TestAuthority.Domain.Models; 4 | 5 | namespace TestAuthority.Host.Contracts; 6 | 7 | /// 8 | /// CertificateRequest 9 | /// 10 | public class CertificateRequestModel 11 | { 12 | /// 13 | /// Common Name 14 | /// 15 | public string CommonName { get; set; } 16 | 17 | /// 18 | /// Password that will be used for PFX file. 19 | /// 20 | [DefaultValue("123123123")] 21 | public string Password { get; set; } 22 | 23 | /// 24 | /// List of domain names to include in Subject Alternative Name extension. 25 | /// 26 | public string[] Hostname { get; set; } = Array.Empty(); 27 | 28 | /// 29 | /// List of IP addresses to include in Subject Alternative Name extension. 30 | /// 31 | /// 32 | public string[] IpAddress { get; set; } = Array.Empty(); 33 | 34 | /// 35 | /// Output filename (without extension). 36 | /// 37 | [DefaultValue("certificate")] 38 | public string Filename { get; set; } = "certificate"; 39 | 40 | /// 41 | /// Certificate validity in days. 42 | /// 43 | [DefaultValue(365)] 44 | public int ValidityInDays { get; set; } 45 | 46 | /// 47 | /// Output format. 48 | /// 49 | /// 50 | /// Pfx will produce PFX file 51 | /// Pem will produce ZIP file with certificate,key and root certificate. 52 | /// 53 | [DefaultValue(OutputFormat.Pem)] 54 | public OutputFormat Format { get; set; } = OutputFormat.Pem; 55 | } 56 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Contracts/ErrorModel.cs: -------------------------------------------------------------------------------- 1 | namespace TestAuthority.Host.Contracts; 2 | 3 | /// 4 | /// Error model. 5 | /// 6 | public class ErrorModel 7 | { 8 | /// 9 | /// Name of the field. 10 | /// 11 | public string FieldName { get; set; } 12 | 13 | /// 14 | /// Error Message. 15 | /// 16 | public string Message { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Contracts/ErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace TestAuthority.Host.Contracts; 4 | 5 | /// 6 | /// Error response contract. 7 | /// 8 | public class ErrorResponse 9 | { 10 | /// 11 | /// List of errors. 12 | /// 13 | // ReSharper disable once CollectionNeverQueried.Global 14 | public List Errors { get; } = new(); 15 | } 16 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Controllers/CertificateController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Mime; 3 | using System.Threading.Tasks; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Mvc; 6 | using TestAuthority.Application.CertificateBuilders; 7 | using TestAuthority.Domain.CertificateConverters; 8 | using TestAuthority.Domain.Models; 9 | using TestAuthority.Domain.Services; 10 | using CertificateRequestModel = TestAuthority.Host.Contracts.CertificateRequestModel; 11 | 12 | namespace TestAuthority.Host.Controllers; 13 | 14 | /// 15 | /// Provides functionality to work with certificates 16 | /// 17 | [Route("api/certificate")] 18 | public class CertificateController : Controller 19 | { 20 | private readonly ICertificateConverterService converterService; 21 | private readonly IMediator mediator; 22 | private readonly ISignerProvider signerProvider; 23 | 24 | /// 25 | /// Ctor. 26 | /// 27 | /// . 28 | /// . 29 | /// . 30 | public CertificateController(ISignerProvider signerProvider, 31 | ICertificateConverterService converterService, 32 | IMediator mediator) 33 | { 34 | this.signerProvider = signerProvider; 35 | this.converterService = converterService; 36 | this.mediator = mediator; 37 | } 38 | 39 | /// 40 | /// Download root certificate. 41 | /// 42 | /// Root certificate. 43 | [HttpGet("/api/certificate/root")] 44 | public IActionResult GetRootCertificate() 45 | { 46 | var result = signerProvider.GetCertificateSignerInfo().GetRootCertificate().GetEncoded(); 47 | return File(result, MediaTypeNames.Application.Octet, "root.cer"); 48 | } 49 | 50 | /// 51 | /// Issue a certificate. Export in PFX format. 52 | /// 53 | /// Certificate request. 54 | /// Result. 55 | [HttpGet] 56 | public async Task IssueCertificate(CertificateRequestModel request) 57 | { 58 | var certificateRequest = new Domain.Models.CertificateRequestModel 59 | { 60 | CommonName = request.CommonName, 61 | Hostnames = request.Hostname.ToList(), 62 | IpAddresses = request.IpAddress.ToList(), 63 | ValidityInDays = request.ValidityInDays 64 | }; 65 | 66 | 67 | var crtRequest = new CertificateBuilderRequest(certificateRequest, signerProvider.GetCertificateSignerInfo().GetSignerCertificate().Certificate); 68 | var result = await mediator.Send(crtRequest); 69 | 70 | 71 | if (request.Format == OutputFormat.Pfx) 72 | { 73 | var resultFilename = string.Concat(request.Filename.Trim('.'), ".pfx"); 74 | var pfx = await converterService.ConvertToPfx(result, request.Password); 75 | return File(pfx, MediaTypeNames.Application.Octet, resultFilename); 76 | } 77 | else 78 | { 79 | var resultFilename = string.Concat(request.Filename.Trim('.'), ".zip"); 80 | var pem = await converterService.ConvertToPemArchiveAsync(result); 81 | return File(pem, MediaTypeNames.Application.Zip, resultFilename); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Controllers/CrlController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Mime; 3 | using System.Threading.Tasks; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Mvc; 6 | using TestAuthority.Application.CrlBuilders; 7 | using TestAuthority.Domain.CertificateConverters; 8 | using TestAuthority.Domain.Services; 9 | 10 | namespace TestAuthority.Host.Controllers; 11 | 12 | /// 13 | /// Provides functionality to work with Crl. 14 | /// 15 | [Route("api/crl")] 16 | public class CrlController: Controller 17 | { 18 | private readonly IMediator mediator; 19 | private readonly ISignerProvider signerProvider; 20 | private readonly ICertificateConverterService converterService; 21 | 22 | /// 23 | /// Ctor. 24 | /// 25 | /// . 26 | /// . 27 | /// . 28 | public CrlController(IMediator mediator, ISignerProvider signerProvider, ICertificateConverterService converterService) 29 | { 30 | this.mediator = mediator; 31 | this.signerProvider = signerProvider; 32 | this.converterService = converterService; 33 | } 34 | 35 | /// 36 | /// Issue a Certificate Revocation List in PEM. 37 | /// 38 | [HttpGet] 39 | public async Task Get() 40 | { 41 | var signer = signerProvider.GetCertificateSignerInfo(); 42 | var crl = await mediator.Send(new CrlBuilderRequest(signer)); 43 | 44 | var result = await converterService.ConvertToPem(crl); 45 | return File(result, MediaTypeNames.Application.Octet, "root.crl"); 46 | } 47 | 48 | /// 49 | /// Issue a Certificate Revocation List in DER. 50 | /// 51 | [HttpGet("{serialNumber}")] 52 | public async Task GetCrl(string serialNumber) 53 | { 54 | var signer = signerProvider.GetCertificateSignerInfo(); 55 | var crlModel = await mediator.Send(new CrlBuilderRequest(signer, serialNumber)); 56 | 57 | return File(crlModel.Crl.GetEncoded(),"application/pkix-crl" , $"{serialNumber}-{DateTimeOffset.UnixEpoch}.crl"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Controllers/DerToolsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Mime; 4 | using System.Text; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Org.BouncyCastle.OpenSsl; 8 | using Org.BouncyCastle.X509; 9 | 10 | namespace TestAuthority.Host.Controllers; 11 | 12 | /// 13 | /// Provides some useful tools like certificateName conversion. 14 | /// 15 | [Route("api/tools")] 16 | public class DerToolsController : Controller 17 | { 18 | /// 19 | /// Convert certificate from DER to PEM encoding.` 20 | /// 21 | /// Request with certificate. 22 | /// Output name of the certificate. 23 | /// Result of conversion. 24 | [HttpPost("der-to-pem")] 25 | public IActionResult ConvertCertificateToPem(IFormFile request, string certificateName = "certificate.crt") 26 | { 27 | using var streamReader = new StreamReader(request.OpenReadStream()); 28 | X509Certificate certificate = new X509CertificateParser().ReadCertificate(streamReader.BaseStream); 29 | string certificateString = ConvertCertificateToPem(certificate); 30 | byte[] result = Encoding.ASCII.GetBytes(certificateString); 31 | return File(result, MediaTypeNames.Application.Octet, certificateName); 32 | } 33 | 34 | private static string ConvertCertificateToPem(X509Certificate certificate) 35 | { 36 | var generator = new MiscPemGenerator(certificate); 37 | 38 | string certificateString; 39 | using (var textWriter = new StringWriter()) 40 | { 41 | var writer = new PemWriter(textWriter); 42 | writer.WriteObject(generator); 43 | writer.Writer.Flush(); 44 | certificateString = textWriter.ToString(); 45 | } 46 | 47 | if (string.IsNullOrWhiteSpace(certificateString)) 48 | { 49 | throw new InvalidOperationException(); 50 | } 51 | 52 | return certificateString; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Controllers/PemToolsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Mime; 5 | using System.Text; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Org.BouncyCastle.OpenSsl; 9 | using Org.BouncyCastle.Pkcs; 10 | 11 | namespace TestAuthority.Host.Controllers; 12 | 13 | /// 14 | /// Tools to work with Pem certificates. 15 | /// 16 | public class PemToolsController : Controller 17 | { 18 | /// 19 | /// Convert pfx to pem certificate. 20 | /// 21 | /// Request. 22 | /// Pfx password. 23 | /// Name of the output certificate. 24 | /// Certificate. 25 | [HttpPost("pfx-to-certificate")] 26 | public IActionResult GetCertificateFromPfx(IFormFile request, string password, string certificateName = "certificate.crt") 27 | { 28 | using var streamReader = new StreamReader(request.OpenReadStream()); 29 | var store = new Pkcs12Store(streamReader.BaseStream, password.ToCharArray()); 30 | string firstAlias = store.Aliases.OfType().FirstOrDefault(); 31 | if (string.IsNullOrWhiteSpace(firstAlias)) 32 | { 33 | throw new InvalidOperationException("Unable to find any certificateName in PFX store"); 34 | } 35 | 36 | X509CertificateEntry certificateEntry = store.GetCertificate(firstAlias); 37 | 38 | string certificateString = ConvertToPemFormat(certificateEntry.Certificate); 39 | byte[] result = Encoding.ASCII.GetBytes(certificateString); 40 | return File(result, MediaTypeNames.Application.Octet, certificateName); 41 | } 42 | 43 | /// 44 | /// Convert pfx to pem key. 45 | /// 46 | /// Request. 47 | /// Pfx password. 48 | /// Name of the output key. 49 | /// Key. 50 | [HttpPost("pfx-to-key")] 51 | public IActionResult GetKeyFromPfx(IFormFile request, string password, string filename = "certificate.key") 52 | { 53 | using var streamReader = new StreamReader(request.OpenReadStream()); 54 | var store = new Pkcs12Store(streamReader.BaseStream, password.ToCharArray()); 55 | string firstAlias = store.Aliases.OfType().FirstOrDefault(); 56 | if (string.IsNullOrWhiteSpace(firstAlias)) 57 | { 58 | throw new InvalidOperationException("Unable to find any certificateName in PFX store"); 59 | } 60 | 61 | AsymmetricKeyEntry key = store.GetKey(firstAlias); 62 | string convertedKey = ConvertToPemFormat(key.Key); 63 | 64 | byte[] result = Encoding.ASCII.GetBytes(convertedKey); 65 | return File(result, MediaTypeNames.Application.Octet, filename); 66 | } 67 | 68 | private static string ConvertToPemFormat(object input) 69 | { 70 | var generator = new MiscPemGenerator(input); 71 | 72 | string certificateString; 73 | using (var textWriter = new StringWriter()) 74 | { 75 | var writer = new PemWriter(textWriter); 76 | writer.WriteObject(generator); 77 | writer.Writer.Flush(); 78 | certificateString = textWriter.ToString(); 79 | } 80 | 81 | if (string.IsNullOrWhiteSpace(certificateString)) 82 | { 83 | throw new InvalidOperationException(); 84 | } 85 | 86 | return certificateString; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Controllers/Pkcs12ToolsController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net.Mime; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Org.BouncyCastle.Crypto; 6 | using Org.BouncyCastle.OpenSsl; 7 | using Org.BouncyCastle.Pkcs; 8 | using Org.BouncyCastle.Security; 9 | using Org.BouncyCastle.X509; 10 | 11 | namespace TestAuthority.Host.Controllers; 12 | 13 | /// 14 | /// Provides API for pfx tooling. 15 | /// 16 | [Route("api/pkcs12")] 17 | public class Pkcs12ToolsController : Controller 18 | { 19 | /// 20 | /// Convert certificate and key in Pem format to Pfx(Pkcs12). 21 | /// 22 | /// Certificate in Pem format. 23 | /// Private key in Pem format. 24 | /// Password for the private key. 25 | /// Output filename. 26 | /// Certificate with private key in Pkcs12 container. 27 | [HttpPost("from-pem")] 28 | public IActionResult ConvertToPfx(IFormFile pemCertificate, 29 | IFormFile pemKey, 30 | string password, 31 | string filename = "certificate.pfx") 32 | { 33 | var certificate = ToArray(pemCertificate.OpenReadStream()); 34 | var key = ToArray(pemKey.OpenReadStream()); 35 | 36 | var result = ConvertToPfxImpl(certificate, key, password); 37 | return File(result, MediaTypeNames.Application.Octet, filename); 38 | } 39 | 40 | private static byte[] ToArray(Stream stream) 41 | { 42 | using var memoryStream = new MemoryStream(); 43 | stream.CopyTo(memoryStream); 44 | memoryStream.Position = 0; 45 | return memoryStream.ToArray(); 46 | } 47 | 48 | private static TOutput ToCrypto(byte[] input) 49 | where TOutput : class 50 | { 51 | using var stream = new MemoryStream(input); 52 | using var streamReader = new StreamReader(stream); 53 | var value = new PemReader(streamReader).ReadObject(); 54 | if (value is TOutput result) 55 | { 56 | return result; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | private static byte[] ConvertToPfxImpl(byte[] certificate, byte[] privateKey, string password) 63 | { 64 | var store = new Pkcs12StoreBuilder().Build(); 65 | 66 | var certificateEntry = new X509CertificateEntry[1]; 67 | var x509Certificate = ToCrypto(certificate); 68 | certificateEntry[0] = new X509CertificateEntry(x509Certificate); 69 | 70 | var asymmetricCipherKeyPair = ToCrypto(privateKey); 71 | 72 | store.SetKeyEntry( 73 | x509Certificate.SubjectDN.ToString(), 74 | new AsymmetricKeyEntry(asymmetricCipherKeyPair.Private), certificateEntry); 75 | var result = new MemoryStream(); 76 | store.Save(result, password.ToCharArray(), new SecureRandom()); 77 | result.Position = 0; 78 | return result.ToArray(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base 2 | 3 | WORKDIR /app 4 | 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 7 | 8 | WORKDIR /src 9 | COPY ["source/TestAuthority.Host/TestAuthority.Host.csproj", "TestAuthority.Host/"] 10 | COPY ["source/TestAuthority.Application/TestAuthority.Application.csproj", "TestAuthority.Application/"] 11 | COPY ["source/TestAuthority.Domain/TestAuthority.Domain.csproj", "TestAuthority.Domain/"] 12 | COPY ["source/TestAuthority.Infrastructure/TestAuthority.Infrastructure.csproj", "TestAuthority.Infrastructure/"] 13 | RUN dotnet restore "/src/TestAuthority.Host/TestAuthority.Host.csproj" 14 | COPY . . 15 | RUN ls -al 16 | RUN pwd 17 | WORKDIR "/src/source/TestAuthority.Host" 18 | RUN ls -al 19 | RUN dotnet build "TestAuthority.Host.csproj" -c Release -o /app/build 20 | 21 | FROM build AS publish 22 | RUN dotnet publish "TestAuthority.Host.csproj" -c Release -o /app/publish 23 | 24 | FROM base AS final 25 | MAINTAINER Iskanders Jarmuhametovs (nomail86<-at->gmail.com) 26 | 27 | EXPOSE 80 28 | 29 | WORKDIR /app 30 | COPY --from=publish /app/publish . 31 | RUN mkdir -p /usr/share/test-authority 32 | ENTRYPOINT ["dotnet", "TestAuthority.Host.dll"] -------------------------------------------------------------------------------- /source/TestAuthority.Host/Extensions/SwaggerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.OpenApi.Models; 5 | using TestAuthority.Host.Swagger; 6 | 7 | namespace TestAuthority.Host.Extensions; 8 | 9 | /// 10 | /// Extension methods for swagger. 11 | /// 12 | public static class SwaggerExtensions 13 | { 14 | /// 15 | /// Register swagger. 16 | /// 17 | /// . 18 | public static void AddSwagger(this IServiceCollection services) 19 | { 20 | services.AddSwaggerGen( 21 | config => 22 | { 23 | config.DescribeAllParametersInCamelCase(); 24 | config.SwaggerDoc( 25 | "v1", new OpenApiInfo 26 | { 27 | Title = "Personal Signing Center", 28 | Version = "v1", 29 | Contact = new OpenApiContact { Name = "Iskander Yarmukhametov", Url = new Uri("https://github.com/nomailme/TestAuthority") } 30 | }); 31 | config.OperationFilter(); 32 | var filePath = Path.Combine(AppContext.BaseDirectory, "TestAuthority.Host.xml"); 33 | config.IncludeXmlComments(filePath); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Filters/ValidationFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using TestAuthority.Host.Contracts; 6 | 7 | namespace TestAuthority.Host.Filters; 8 | 9 | /// 10 | /// Validation filter. 11 | /// 12 | // ReSharper disable once ClassNeverInstantiated.Global 13 | public class ValidationFilter : IAsyncActionFilter 14 | { 15 | /// 16 | public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 17 | { 18 | if (!context.ModelState.IsValid) 19 | { 20 | var errorsInModelState = context.ModelState 21 | .Where(x => x.Value?.Errors.Count > 0) 22 | .ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.Errors.Select(x => x.ErrorMessage)).ToArray(); 23 | 24 | var errorResponse = new ErrorResponse(); 25 | 26 | foreach (var (key, value) in errorsInModelState) 27 | { 28 | foreach (var subError in value) 29 | { 30 | var errorModel = new ErrorModel 31 | { 32 | FieldName = key, 33 | Message = subError 34 | }; 35 | 36 | errorResponse.Errors.Add(errorModel); 37 | } 38 | } 39 | context.Result = new BadRequestObjectResult(errorResponse); 40 | return; 41 | } 42 | 43 | await next(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using FluentValidation.AspNetCore; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Serilog; 10 | using TestAuthority.Application; 11 | using TestAuthority.Application.Random; 12 | using TestAuthority.Domain; 13 | using TestAuthority.Domain.Services; 14 | using TestAuthority.Host.Extensions; 15 | using TestAuthority.Host.Filters; 16 | using TestAuthority.Infrastructure.Extensions; 17 | 18 | var builder = WebApplication.CreateBuilder(args); 19 | builder.Host.UseSerilog( 20 | ((context, services, configuration) => 21 | { 22 | configuration.ReadFrom.Configuration(context.Configuration); 23 | configuration.ReadFrom.Services(services); 24 | configuration.Enrich.FromLogContext(); 25 | configuration.WriteTo.Console(); 26 | })); 27 | 28 | builder.Services 29 | .AddControllers(x => x.Filters.Add()) 30 | .AddJsonOptions(ConfigureJson) 31 | .AddFluentValidation(config => config.RegisterValidatorsFromAssemblyContaining()); 32 | 33 | builder.Services.AddEndpointsApiExplorer(); 34 | builder.Services.AddSwagger(); 35 | 36 | builder.Services.AddMediatR(typeof(Program)); 37 | builder.Services.AddSingleton(); 38 | builder.Services.AddSingleton(); 39 | builder.Services.AddCertificateAuthorityService(); 40 | builder.Services.AddCertificateGenerationPipeline(); 41 | builder.Services.AddCrlGenerationPipeline(); 42 | 43 | builder.Services.Configure(builder.Configuration.GetSection("CrlSettings")); 44 | builder.Configuration.AddEnvironmentVariables(); 45 | 46 | 47 | var app = builder.Build(); 48 | app.UseSwagger(); 49 | app.UseSwaggerUI( 50 | config => 51 | { 52 | config.SwaggerEndpoint("/swagger/v1/swagger.json", "Personal Signing Center"); 53 | config.RoutePrefix = string.Empty; 54 | }); 55 | 56 | app.MapControllers(); 57 | app.Run(); 58 | 59 | void ConfigureJson(JsonOptions options) 60 | { 61 | options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); 62 | options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; 63 | options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; 64 | } 65 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Properties/PublishProfiles/10.0.15.211.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Custom 9 | true 10 | https://10.0.15.211/ 11 | admin 12 | latest 13 | ContainerRegistry 14 | Release 15 | Any CPU 16 | b16e4c07-b615-4e92-8077-7a7510c06e7f 17 | 18 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54519", 7 | "sslPort": 0 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "TestAuthorityCore": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "http://localhost:5000" 28 | }, 29 | "Docker": { 30 | "commandName": "Docker", 31 | "launchBrowser": true, 32 | "launchUrl": "{Scheme}://localhost:{ServicePort}/api/values" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /source/TestAuthority.Host/Swagger/FormFileSwaggerFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.OpenApi.Models; 5 | using Swashbuckle.AspNetCore.SwaggerGen; 6 | 7 | namespace TestAuthority.Host.Swagger; 8 | 9 | /// 10 | /// AddSwaggerFileUploadButton. 11 | /// 12 | [AttributeUsage(AttributeTargets.Method)] 13 | public class AddSwaggerFileUploadButtonAttribute : Attribute 14 | { 15 | } 16 | 17 | /// 18 | /// Filter to enable handling file upload in swagger 19 | /// 20 | public class AddFileParamTypesOperationFilter : IOperationFilter 21 | { 22 | // ReSharper disable once InconsistentNaming 23 | private static readonly string[] fileParameters = { "ContentType", "ContentDisposition", "Headers", "Length", "Name", "FileName" }; 24 | 25 | /// 26 | /// Apply filter. 27 | /// 28 | /// . 29 | /// . 30 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 31 | { 32 | var operationHasFileUploadButton = context.MethodInfo.GetCustomAttributes(true).OfType().Any(); 33 | 34 | if (!operationHasFileUploadButton) 35 | { 36 | return; 37 | } 38 | RemoveExistingFileParameters(operation.Parameters); 39 | operation.RequestBody = new OpenApiRequestBody 40 | { 41 | Content = 42 | { 43 | ["multipart/form-data"] = new OpenApiMediaType 44 | { 45 | Schema = new OpenApiSchema 46 | { 47 | Type = "object", 48 | Properties = 49 | { 50 | ["file"] = new OpenApiSchema 51 | { 52 | Description = "Select file", 53 | Type = "string", 54 | Format = "binary" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }; 61 | 62 | } 63 | 64 | private static void RemoveExistingFileParameters(ICollection operationParameters) 65 | { 66 | foreach (var parameter in operationParameters.Where(p => p.In == 0 && fileParameters.Contains(p.Name)).ToList()) 67 | { 68 | operationParameters.Remove(parameter); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/TestAuthority.Host.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | Linux 6 | true 7 | 10 8 | Linux 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | .editorconfig 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/Validators/CertificateRequestValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using TestAuthority.Domain.Models; 3 | 4 | namespace TestAuthority.Host.Validators; 5 | 6 | /// 7 | /// Validation for certificate request. 8 | /// 9 | public class CertificateRequestValidator : AbstractValidator 10 | { 11 | /// 12 | /// Ctor. 13 | /// 14 | public CertificateRequestValidator() 15 | { 16 | RuleFor(x => x.CommonName) 17 | .NotEmpty().WithMessage("You must provide a Common Name"); 18 | RuleFor(x => x) 19 | .Must(AnyHostnamesOrIpAddresses).WithMessage("You must provide at least one hostname or IP address."); 20 | RuleFor(x => x.Password) 21 | .MinimumLength(1).When(x => x.Format == OutputFormat.Pfx) 22 | .WithMessage("You must provide a password for PFX"); 23 | } 24 | 25 | private static bool AnyHostnamesOrIpAddresses(Contracts.CertificateRequestModel request) 26 | { 27 | var sanRecords = 0; 28 | sanRecords += request.Hostname?.Length ?? 0; 29 | sanRecords += request.IpAddress?.Length ?? 0; 30 | return sanRecords > 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/TestAuthority.Host/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | }, 9 | "Serilog": { 10 | "MinimumLevel": "Information" 11 | } 12 | } -------------------------------------------------------------------------------- /source/TestAuthority.Host/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "CrlSettings": { 9 | "CaAddress": "http://localhost:5000" 10 | } 11 | } -------------------------------------------------------------------------------- /source/TestAuthority.Infrastructure/Extensions/CertificateAuthorityExtensions.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using MediatR.Pipeline; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using TestAuthority.Application; 5 | using TestAuthority.Application.CertificateBuilders; 6 | using TestAuthority.Application.CertificateBuilders.CertificateBuilderSteps; 7 | using TestAuthority.Application.CertificateConverters; 8 | using TestAuthority.Application.CrlBuilders; 9 | using TestAuthority.Application.CrlBuilders.CrlBuilderSteps; 10 | using TestAuthority.Application.SignatureFactoryProviders; 11 | using TestAuthority.Application.Store; 12 | using TestAuthority.Domain.CertificateConverters; 13 | using TestAuthority.Domain.Models; 14 | using TestAuthority.Domain.Services; 15 | using TestAuthority.Domain.Store; 16 | 17 | namespace TestAuthority.Infrastructure.Extensions; 18 | 19 | /// 20 | /// Extension methods for CA. 21 | /// 22 | public static class CertificateAuthorityExtensions 23 | { 24 | /// 25 | /// Register services required for CA. 26 | /// 27 | /// 28 | public static void AddCertificateAuthorityService(this IServiceCollection services) 29 | { 30 | services.AddSingleton(); 31 | services.AddSingleton(); 32 | services.AddSingleton(); 33 | 34 | services.AddSingleton(); 35 | } 36 | 37 | /// 38 | /// Register certificate generation pipeline. 39 | /// 40 | /// . 41 | public static void AddCertificateGenerationPipeline(this IServiceCollection services) 42 | { 43 | services.AddTransient, KeyPairGenerationBehaviour>(); 44 | 45 | services.AddTransient, CommonNameBehaviour>(); 46 | services.AddTransient, IssuerNameBehaviour>(); 47 | services.AddTransient, SerialNumberBehaviour>(); 48 | services.AddTransient, CertificateValidityBehaviour>(); 49 | services.AddTransient, SubjectAlternativeNameBehaviour>(); 50 | services.AddTransient, CrlDistributionPointExtensionBehaviour>(); 51 | services.AddTransient, BasicConstraintsExtensionBehaviour>(); 52 | services.AddTransient, ExtendedKeyUsageExtensionBehaviour>(); 53 | services.AddTransient, AuthorityKeyIdentifierExtensionBehavior>(); 54 | services.AddTransient, SignCertificateBehaviour>(); 55 | 56 | services.AddTransient, CertificateLoggingPostProcessor>(); 57 | } 58 | 59 | /// 60 | /// Register Crl generation pipeline. 61 | /// 62 | /// . 63 | public static void AddCrlGenerationPipeline(this IServiceCollection services) 64 | { 65 | services.AddTransient, GenerateCrlBehaviour>(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /source/TestAuthority.Infrastructure/TestAuthority.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/TestAuthority.UnitTests/CertificateHelper.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Asn1.X509; 2 | using Org.BouncyCastle.Crypto; 3 | using Org.BouncyCastle.Crypto.Generators; 4 | using Org.BouncyCastle.Crypto.Operators; 5 | using Org.BouncyCastle.Security; 6 | using Org.BouncyCastle.Utilities; 7 | using Org.BouncyCastle.X509; 8 | using TestAuthority.Domain.Models; 9 | 10 | namespace TestAuthority.UnitTests; 11 | 12 | public static class CertificateHelper 13 | { 14 | public static CertificateWithKey GenerateDummyCertificate(string commonName) 15 | { 16 | X509V3CertificateGenerator generator = new(); 17 | var keyPair = GenerateKeyPair(2048, new SecureRandom()); 18 | Asn1SignatureFactory signatureFactory = new("SHA256WithRSA",keyPair.Private); 19 | 20 | var dn = GenerateName(commonName); 21 | generator.SetIssuerDN(dn); 22 | generator.SetSubjectDN(dn); 23 | generator.SetSerialNumber(BigIntegers.CreateRandomBigInteger(128,new SecureRandom())); 24 | generator.SetNotBefore(DateTime.Now.Subtract(TimeSpan.FromDays(1))); 25 | generator.SetNotAfter(DateTime.Now.Add(TimeSpan.FromDays(1))); 26 | generator.SetPublicKey(keyPair.Public); 27 | var certificate = generator.Generate(signatureFactory); 28 | return new CertificateWithKey(certificate, keyPair); 29 | 30 | } 31 | 32 | private static X509Name GenerateName(string commonName) => new($"CN={commonName}"); 33 | 34 | 35 | private static AsymmetricCipherKeyPair GenerateKeyPair(int keyStrength, SecureRandom random) 36 | { 37 | var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength); 38 | var keyPairGenerator = new RsaKeyPairGenerator(); 39 | keyPairGenerator.Init(keyGenerationParameters); 40 | var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); 41 | return subjectKeyPair; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/TestAuthority.UnitTests/CertificateSignerInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using TestAuthority.Domain.Models; 2 | 3 | namespace TestAuthority.UnitTests; 4 | 5 | public class SignerInfoExtensionsTests 6 | { 7 | [Fact] 8 | public void GetRootCertificate_CertificateChainWithThreeCertificates_RootCertificate() 9 | { 10 | var chain = new List 11 | { 12 | CertificateHelper.GenerateDummyCertificate("Intermediate1"), 13 | CertificateHelper.GenerateDummyCertificate("Intermediate2"), 14 | CertificateHelper.GenerateDummyCertificate("Root") 15 | }; 16 | CertificateSignerInfo signerInfo = new CertificateSignerInfo(chain); 17 | 18 | var rootCertificate = signerInfo.GetRootCertificate(); 19 | Assert.Equal("CN=Root",rootCertificate.SubjectDN.ToString(), StringComparer.OrdinalIgnoreCase); 20 | } 21 | 22 | [Fact] 23 | public void GetIntermediateCertificates_CertificateChainWithThreeCertificates_IntermediateCertificates() 24 | { 25 | var chain = new List 26 | { 27 | CertificateHelper.GenerateDummyCertificate("Intermediate1"), 28 | CertificateHelper.GenerateDummyCertificate("Intermediate2"), 29 | CertificateHelper.GenerateDummyCertificate("Root") 30 | }; 31 | CertificateSignerInfo signerInfo = new CertificateSignerInfo(chain); 32 | 33 | var intermediateCertificates = signerInfo.GetIntermediateCertificates(); 34 | Assert.DoesNotContain(intermediateCertificates, x=>x.SubjectDN.ToString().Contains("CN=Root")); 35 | Assert.Equal(2,intermediateCertificates.Count); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/TestAuthority.UnitTests/TestAuthority.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/TestAuthority.UnitTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | --------------------------------------------------------------------------------