├── .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 |
--------------------------------------------------------------------------------