├── nuget.config
├── ConcertoCLI
├── ConcertoCLI.csproj
├── CommandLineHelper.cs
└── Program.cs
├── LICENSE
├── Concerto.sln
├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── Concerto
├── Concerto.csproj
├── CertificateFileStore.cs
└── CertificateCreator.cs
├── README.md
├── .gitattributes
└── .gitignore
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ConcertoCLI/ConcertoCLI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net5.0
6 | enable
7 | LowLevelDesign.Concerto
8 | Sebastian Solnica (@lowleveldesign)
9 | 1.0.0.0
10 | 1.0.0.0
11 | concerto
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | CertificateCreator.cs
21 |
22 |
23 | CertificateFileStore.cs
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | MIT License
3 |
4 | Copyright (c) 2019 Sebastian Solnica
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Concerto.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConcertoCLI", "ConcertoCLI\ConcertoCLI.csproj", "{4F4A37EF-8BFA-4268-95F9-F4F453BDB19A}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Concerto", "Concerto\Concerto.csproj", "{C85AEE9B-B2A9-4A0E-B1B0-2B66EB6278DE}"
6 | EndProject
7 | Global
8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 | Debug|Any CPU = Debug|Any CPU
10 | Release|Any CPU = Release|Any CPU
11 | EndGlobalSection
12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
13 | {4F4A37EF-8BFA-4268-95F9-F4F453BDB19A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14 | {4F4A37EF-8BFA-4268-95F9-F4F453BDB19A}.Debug|Any CPU.Build.0 = Debug|Any CPU
15 | {4F4A37EF-8BFA-4268-95F9-F4F453BDB19A}.Release|Any CPU.ActiveCfg = Release|Any CPU
16 | {4F4A37EF-8BFA-4268-95F9-F4F453BDB19A}.Release|Any CPU.Build.0 = Release|Any CPU
17 | {C85AEE9B-B2A9-4A0E-B1B0-2B66EB6278DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18 | {C85AEE9B-B2A9-4A0E-B1B0-2B66EB6278DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
19 | {C85AEE9B-B2A9-4A0E-B1B0-2B66EB6278DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {C85AEE9B-B2A9-4A0E-B1B0-2B66EB6278DE}.Release|Any CPU.Build.0 = Release|Any CPU
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build-all:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@main
12 | with:
13 | fetch-depth: 1
14 |
15 | - uses: actions/setup-dotnet@v1
16 | with:
17 | dotnet-version: '5.0.x' # SDK Version to use.
18 |
19 | - run: dotnet publish -c release -r linux-x64 -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true -p:IncludeNativeLibrariesForSelfExtract=true
20 | working-directory: ./ConcertoCLI
21 |
22 | - run: dotnet publish -c release -r win-x64 -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true -p:IncludeNativeLibrariesForSelfExtract=true
23 | working-directory: ./ConcertoCLI
24 |
25 | - run: dotnet publish -c release -r osx-x64 -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true -p:IncludeNativeLibrariesForSelfExtract=true
26 | working-directory: ./ConcertoCLI
27 |
28 | - uses: actions/upload-artifact@main
29 | with:
30 | name: concerto-linux64
31 | path: ConcertoCLI/bin/release/net5.0/linux-x64/publish
32 |
33 | - uses: actions/upload-artifact@main
34 | with:
35 | name: concerto-win64
36 | path: ConcertoCLI/bin/release/net5.0/win-x64/publish
37 |
38 | - uses: actions/upload-artifact@main
39 | with:
40 | name: concerto-macos64
41 | path: ConcertoCLI/bin/release/net5.0/osx-x64/publish
42 |
--------------------------------------------------------------------------------
/Concerto/Concerto.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 | enable
6 | LowLevelDesign.Concerto
7 | Sebastian Solnica (@lowleveldesign)
8 | 1.0.0.0
9 | 1.0.0.0
10 | concerto
11 | true
12 | Concerto
13 | 1.0.0.0
14 | Concerto
15 | Sebastian Solnica (@lowleveldesign)
16 | A library to generate TLS certificates for development purposes.
17 | Sebastian Solnica (@lowleveldesign)
18 | https://github.com/lowleveldesign/concerto
19 | MIT
20 | https://github.com/lowleveldesign/concerto
21 | x509, https, tls, certificate
22 |
23 |
24 |
25 | true
26 | snupkg
27 |
28 |
29 |
30 |
31 | all
32 | runtime; build; native; contentfiles; analyzers; buildtransitive
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ConcertoCLI/CommandLineHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 |
6 | namespace LowLevelDesign.Concerto
7 | {
8 | public sealed class CommandLineArgumentException : Exception
9 | {
10 | public CommandLineArgumentException(string message) : base(message) { }
11 | }
12 |
13 | public static class CommandLineHelper
14 | {
15 | public static Dictionary ParseArgs(string[] flagArgs, string[] rawArgs)
16 | {
17 | var args = rawArgs.SelectMany(arg => arg.Split(new[] { '=' },
18 | StringSplitOptions.RemoveEmptyEntries)).ToArray();
19 | bool IsFlag(string v) => Array.IndexOf(flagArgs, v) >= 0;
20 |
21 | var result = new Dictionary(StringComparer.Ordinal);
22 | var lastArg = string.Empty;
23 | foreach (var arg in args) {
24 | switch (arg) {
25 | case var s when s.StartsWith("-", StringComparison.Ordinal):
26 | var option = s.TrimStart('-');
27 | if (IsFlag(option)) {
28 | Debug.Assert(lastArg == string.Empty);
29 | result.Add(option, string.Empty);
30 | } else {
31 | Debug.Assert(lastArg == string.Empty);
32 | lastArg = option;
33 | }
34 | break;
35 | default:
36 | if (lastArg != string.Empty) {
37 | result.Add(lastArg, arg);
38 | lastArg = string.Empty;
39 | } else {
40 | result[string.Empty] = !result.TryGetValue(string.Empty, out var freeArgs) ? arg : $"{freeArgs},{arg}";
41 | }
42 | break;
43 | }
44 | }
45 | return result;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 |
10 | build-all:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@main
16 | with:
17 | fetch-depth: 1
18 |
19 | - uses: actions/setup-dotnet@v1
20 | with:
21 | dotnet-version: '5.0.x' # SDK Version to use.
22 |
23 | - run: |
24 | Invoke-WebRequest -OutFile Update-AssemblyInfoVersionFiles.ps1 https://gist.githubusercontent.com/lowleveldesign/663de4e0d5a071f938e6f7c82d7ca9a0/raw/Update-AssemblyInfoVersionFiles.ps1
25 | ./Update-AssemblyInfoVersionFiles.ps1
26 | shell: pwsh
27 |
28 | - run: dotnet build -c release
29 | working-directory: ./Concerto
30 |
31 | - run: dotnet nuget push -s https://api.nuget.org/v3/index.json -k "$NUGET_KEY" Concerto.*.nupkg
32 | env:
33 | NUGET_KEY: ${{ secrets.NUGET_KEY }}
34 | working-directory: ./Concerto/bin/release
35 |
36 | - run: dotnet publish -c release -r linux-x64 -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true -p:IncludeNativeLibrariesForSelfExtract=true
37 | working-directory: ./ConcertoCLI
38 |
39 | - run: dotnet publish -c release -r osx-x64 -f netcoreapp3.1 -p:PublishSingleFile=true -p:PublishTrimmed=true
40 | working-directory: ./dotnet-wtrace
41 |
42 | - run: dotnet publish -c release -r win-x64 -f netcoreapp3.1 -p:PublishSingleFile=true -p:PublishTrimmed=true
43 | working-directory: ./dotnet-wtrace
44 |
45 | - uses: actions/upload-artifact@main
46 | with:
47 | name: dotnet-wtrace-linux
48 | path: dotnet-wtrace/bin/release/netcoreapp3.1/linux-x64/publish
49 |
50 | - uses: actions/upload-artifact@main
51 | with:
52 | name: dotnet-wtrace-windows
53 | path: dotnet-wtrace/bin/release/netcoreapp3.1/win-x64/publish
54 |
55 | - uses: actions/upload-artifact@main
56 | with:
57 | name: dotnet-wtrace-osx
58 | path: dotnet-wtrace/bin/release/netcoreapp3.1/osx-x64/publish
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # concerto
3 |
4 | 
5 |
6 | A command line tool and a library to generate TLS certificates for development purposes.
7 |
8 | Inspired by [mkcert](https://github.com/FiloSottile/mkcert) by Filippo Valsorda,
9 | but written in C# using the [Bouncy Castle](https://www.bouncycastle.org/csharp/)
10 | library.
11 |
12 | ## Command Line Tool
13 |
14 | ### Create a site certificate
15 |
16 | ```
17 | $ concerto www.test.com
18 | ```
19 |
20 | This will create a concertoCA.pem root certificate and a www.test.com.pem
21 | certificate for your domain. You may add multiple domains, if needed.
22 | IPs and URIs are accepted too.
23 |
24 | Some more examples:
25 |
26 | ```
27 | $ concerto localhost 127.0.0.1
28 | $ concerto '*.example.com' 192.168.0.12
29 | $ concerto https://www.example.com 192.168.0.12
30 | ```
31 |
32 | ### Create a site certificate with an intermediate CA
33 |
34 | ```
35 | $ concerto -int myIntCA
36 | $ concerto -chain -ca myIntCA.pem www.test.com
37 | ```
38 |
39 | This will create a concertoCA.pem root certificate, an intermediate
40 | CA certificate (myIntCA.pem), a site certificate with a certificate
41 | trust chain (www.test.com.pem).
42 |
43 | ### Available options
44 |
45 | ```
46 | -ca Specifies which CA certificate to use.
47 | -client Allow a client to authenticate using the certificate.
48 | -chain Add the certificate chain to the certificate file.
49 | -ecdsa Use Elliptic Curve key instead of RSA.
50 | -pfx Save the certificate and the key in a .pfx file.
51 | -help Shows the help screen.
52 | ```
53 |
54 | ## NuGet package ([Concerto](https://www.nuget.org/packages/Concerto))
55 |
56 | The NuGet package contains two classes: `CertificateCreator` and `CertificateFileStore`. They provide a straightforward API to create TLS certificates and save them to and read them from a file system.
57 |
58 | Example usage:
59 |
60 | ```csharp
61 |
62 | var workingDir = @"C:\temp";
63 |
64 | CertificateChainWithPrivateKey rootCA;
65 | if (File.Exists($@"{workingDir}\myCA.pem") && File.Exists($@"{workingDir}\myCA.key")) {
66 | rootCA = CertificateFileStore.LoadCertificate($@"{workingDir}\myCA.pem");
67 | } else {
68 | rootCA = CertificateCreator.CreateCACertificate("MyCA");
69 | CertificateFileStore.SaveCertificate(rootCA, $@"{workingDir}\myCA.pem");
70 | }
71 |
72 | var cert = CertificateCreator.CreateCertificate(new [] { "www.test.com", "localhost" }, rootCA);
73 | CertificateFileStore.SaveCertificate(cert, $@"{workingDir}\www.test.com.pem");
74 | ```
75 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | [Xx]64/
19 | [Xx]86/
20 | [Bb]uild/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 |
85 | # Visual Studio profiler
86 | *.psess
87 | *.vsp
88 | *.vspx
89 | *.sap
90 |
91 | # TFS 2012 Local Workspace
92 | $tf/
93 |
94 | # Guidance Automation Toolkit
95 | *.gpState
96 |
97 | # ReSharper is a .NET coding add-in
98 | _ReSharper*/
99 | *.[Rr]e[Ss]harper
100 | *.DotSettings.user
101 |
102 | # JustCode is a .NET coding add-in
103 | .JustCode
104 |
105 | # TeamCity is a build add-in
106 | _TeamCity*
107 |
108 | # DotCover is a Code Coverage Tool
109 | *.dotCover
110 |
111 | # NCrunch
112 | _NCrunch_*
113 | .*crunch*.local.xml
114 | nCrunchTemp_*
115 |
116 | # MightyMoose
117 | *.mm.*
118 | AutoTest.Net/
119 |
120 | # Web workbench (sass)
121 | .sass-cache/
122 |
123 | # Installshield output folder
124 | [Ee]xpress/
125 |
126 | # DocProject is a documentation generator add-in
127 | DocProject/buildhelp/
128 | DocProject/Help/*.HxT
129 | DocProject/Help/*.HxC
130 | DocProject/Help/*.hhc
131 | DocProject/Help/*.hhk
132 | DocProject/Help/*.hhp
133 | DocProject/Help/Html2
134 | DocProject/Help/html
135 |
136 | # Click-Once directory
137 | publish/
138 |
139 | # Publish Web Output
140 | *.[Pp]ublish.xml
141 | *.azurePubxml
142 |
143 | *.publishproj
144 |
145 | # NuGet Packages
146 | *.nupkg
147 | # The packages folder can be ignored because of Package Restore
148 | **/packages/*
149 | # except build/, which is used as an MSBuild target.
150 | !**/packages/build/
151 | # Uncomment if necessary however generally it will be regenerated when needed
152 | #!**/packages/repositories.config
153 | # NuGet v3's project.json files produces more ignoreable files
154 | *.nuget.props
155 | *.nuget.targets
156 |
157 | !wtrace/binaries/**/*
158 |
159 | # Microsoft Azure Build Output
160 | csx/
161 | *.build.csdef
162 |
163 | # Microsoft Azure Emulator
164 | ecf/
165 | rcf/
166 |
167 | # Microsoft Azure ApplicationInsights config file
168 | ApplicationInsights.config
169 |
170 | # Windows Store app package directory
171 | AppPackages/
172 | BundleArtifacts/
173 |
174 | # Visual Studio cache files
175 | # files ending in .cache can be ignored
176 | *.[Cc]ache
177 | # but keep track of directories ending in .cache
178 | !*.[Cc]ache/
179 |
180 | # Others
181 | ClientBin/
182 | [Ss]tyle[Cc]op.*
183 | ~$*
184 | *~
185 | *.dbmdl
186 | *.dbproj.schemaview
187 | *.pfx
188 | *.publishsettings
189 | node_modules/
190 | orleans.codegen.cs
191 |
192 | # RIA/Silverlight projects
193 | Generated_Code/
194 |
195 | # Backup & report files from converting an old project file
196 | # to a newer Visual Studio version. Backup files are not needed,
197 | # because we have git ;-)
198 | _UpgradeReport_Files/
199 | Backup*/
200 | UpgradeLog*.XML
201 | UpgradeLog*.htm
202 |
203 | # SQL Server files
204 | *.mdf
205 | *.ldf
206 |
207 | # Business Intelligence projects
208 | *.rdl.data
209 | *.bim.layout
210 | *.bim_*.settings
211 |
212 | # Microsoft Fakes
213 | FakesAssemblies/
214 |
215 | # GhostDoc plugin setting file
216 | *.GhostDoc.xml
217 |
218 | # Node.js Tools for Visual Studio
219 | .ntvs_analysis.dat
220 |
221 | # Visual Studio 6 build log
222 | *.plg
223 |
224 | # Visual Studio 6 workspace options file
225 | *.opt
226 |
227 | # Visual Studio LightSwitch build output
228 | **/*.HTMLClient/GeneratedArtifacts
229 | **/*.DesktopClient/GeneratedArtifacts
230 | **/*.DesktopClient/ModelManifest.xml
231 | **/*.Server/GeneratedArtifacts
232 | **/*.Server/ModelManifest.xml
233 | _Pvt_Extensions
234 |
235 | # LightSwitch generated files
236 | GeneratedArtifacts/
237 | ModelManifest.xml
238 |
239 | # Paket dependency manager
240 | .paket/paket.exe
241 |
242 | # FAKE - F# Make
243 | .fake/
244 |
245 | # Idea
246 | .idea
247 |
248 | # backup files
249 | *.bak
--------------------------------------------------------------------------------
/Concerto/CertificateFileStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Linq;
5 | using Org.BouncyCastle.Crypto.Parameters;
6 | using Org.BouncyCastle.OpenSsl;
7 | using Org.BouncyCastle.Pkcs;
8 | using Org.BouncyCastle.Security;
9 | using Org.BouncyCastle.X509;
10 |
11 | namespace LowLevelDesign.Concerto
12 | {
13 | public static class CertificateFileStore
14 | {
15 | private static readonly TraceSource Logger = new TraceSource("LowLevelDesign.Concerto");
16 |
17 | ///
18 | /// Saves certificate to a file on a disk.
19 | ///
20 | /// A certificate to save.
21 | ///
22 | /// The path to the destination file. The file extension is important and defines the format
23 | /// of the encoding (currently we support only PKCS12 (.pfx) and PEM (.pem, .cer) formats). If it's PEM
24 | /// a new file will be created next to the certificate file with a .key extension.
25 | ///
26 | ///
27 | /// Defines whether the certificate chain should be included in the certificate file.
28 | ///
29 | /// Password for encrypting the certificate private key. If null, the private key won't be encrypted.
30 | public static void SaveCertificate(CertificateChainWithPrivateKey cert, string path, bool chain = false, string? password = null)
31 | {
32 | var extension = Path.GetExtension(path);
33 |
34 | if (string.IsNullOrEmpty(extension) || string.Equals(".pem", extension, StringComparison.OrdinalIgnoreCase)
35 | || string.Equals(".cer", extension, StringComparison.OrdinalIgnoreCase))
36 | {
37 | SavePemCertificate(cert, password, path, chain);
38 | }
39 | else if (string.Equals(".pfx", extension, StringComparison.OrdinalIgnoreCase))
40 | {
41 | SavePkcs12Certificate(cert, password, path, chain);
42 | }
43 | else
44 | {
45 | throw new ArgumentException(
46 | $"Unknown certificate format. Accepted extensions for {nameof(path)} are: .pfx (PKCS12), .pem, or .cer (PEM).");
47 | }
48 | }
49 |
50 | private static void SavePkcs12Certificate(CertificateChainWithPrivateKey certChainWithKey, string? password,
51 | string certFilePath, bool chain)
52 | {
53 | if (File.Exists(certFilePath))
54 | {
55 | throw new ArgumentException("Cert file already exists. Please remove it or switch directories.");
56 | }
57 |
58 | var store = new Pkcs12StoreBuilder().Build();
59 |
60 | // cert chain
61 | var chainLen = 1;
62 | if (chain)
63 | {
64 | chainLen = certChainWithKey.Certificates.Length;
65 | }
66 |
67 | for (var i = 0; i < chainLen; i++)
68 | {
69 | var cert = certChainWithKey.Certificates[i];
70 | var certEntry = new X509CertificateEntry(cert);
71 | store.SetCertificateEntry(cert.SubjectDN.ToString(), certEntry);
72 | }
73 |
74 | // private key
75 | var primaryCert = certChainWithKey.PrimaryCertificate;
76 | var keyEntry = new AsymmetricKeyEntry(certChainWithKey.PrivateKey);
77 | store.SetKeyEntry(primaryCert.SubjectDN.ToString(), keyEntry,
78 | new[] { new X509CertificateEntry(primaryCert) });
79 |
80 | using var stream = File.OpenWrite(certFilePath);
81 | store.Save(stream, password?.ToCharArray(), new SecureRandom());
82 | }
83 |
84 | private static void SavePemCertificate(CertificateChainWithPrivateKey certChainWithKey, string? password,
85 | string certFilePath, bool chain)
86 | {
87 | var keyFilePath = Path.ChangeExtension(certFilePath, ".key");
88 |
89 | Logger.TraceInformation($"saving key to {keyFilePath}");
90 | Logger.TraceInformation($"saving cert to {certFilePath}");
91 | if (File.Exists(certFilePath) || File.Exists(keyFilePath))
92 | {
93 | throw new ArgumentException("Cert or key file already exists. Please remove it or switch directories.");
94 | }
95 |
96 |
97 | Debug.Assert(certChainWithKey.Certificates.Length > 0);
98 | if (chain)
99 | {
100 | using var writer = new StreamWriter(certFilePath);
101 | var pem = new PemWriter(writer);
102 | foreach (var cert in certChainWithKey.Certificates)
103 | {
104 | pem.WriteObject(cert);
105 | }
106 | }
107 | else
108 | {
109 | using var writer = new StreamWriter(certFilePath);
110 | var pem = new PemWriter(writer);
111 | pem.WriteObject(certChainWithKey.Certificates[0]);
112 | }
113 |
114 | using (var writer = new StreamWriter(keyFilePath))
115 | {
116 | var pem = new PemWriter(writer);
117 | var pemObjGenerator = password == null ?
118 | (Org.BouncyCastle.Utilities.IO.Pem.PemObjectGenerator)new Pkcs8Generator(certChainWithKey.PrivateKey) :
119 | new MiscPemGenerator(certChainWithKey.PrivateKey, "AES-256-CBC", password.ToCharArray(), new SecureRandom());
120 | pem.WriteObject(pemObjGenerator);
121 | }
122 | }
123 |
124 | ///
125 | /// Loads a certificate from a file.
126 | ///
127 | ///
128 | /// A path to the certificate file. The format of the encoding is guessed from
129 | /// the file extension. Only PKCS12 (.pfx) and PEM (.pem, .cer) formats are recognized.
130 | ///
131 | /// The certificate representation.
132 | public static CertificateChainWithPrivateKey LoadCertificate(string path)
133 | {
134 | if (!File.Exists(path))
135 | {
136 | throw new ArgumentException($"The certificate file: '{path}' does not exist.");
137 | }
138 |
139 | return Path.GetExtension(path) switch {
140 | var s when string.IsNullOrEmpty(s) || string.Equals(".pem", s, StringComparison.OrdinalIgnoreCase)
141 | || string.Equals(".cer", s, StringComparison.OrdinalIgnoreCase)
142 | => LoadPemCertificate(path),
143 | var s when string.Equals(".pfx", s, StringComparison.OrdinalIgnoreCase) => LoadPfxCertificate(path),
144 | var s => throw new ArgumentException(
145 | $"Unknown certificate format: {s}. Accepted extensions for {nameof(path)} are: .pfx (PKCS12) and .pem (PEM).")
146 | };
147 | }
148 |
149 | private static CertificateChainWithPrivateKey LoadPfxCertificate(string certPath)
150 | {
151 | if (certPath == null)
152 | {
153 | throw new ArgumentNullException($"{nameof(certPath)}");
154 | }
155 | using var certStream = File.OpenRead(certPath);
156 | var store = new Pkcs12StoreBuilder().Build();
157 | store.Load(certStream, null);
158 |
159 | var aliases = store.Aliases.Cast().ToArray();
160 | if (aliases.Length == 0)
161 | {
162 | throw new ArgumentException("Invalid .pfx cert (no aliases)");
163 | }
164 |
165 | var certEntries = store.GetCertificateChain(aliases[0]);
166 | var certificates = new X509Certificate[certEntries.Length];
167 | for (var i = 0; i < certEntries.Length; i++)
168 | {
169 | certificates[i] = certEntries[i].Certificate;
170 | }
171 |
172 | return new CertificateChainWithPrivateKey(certificates, store.GetKey(aliases[0]).Key);
173 | }
174 |
175 | private static CertificateChainWithPrivateKey LoadPemCertificate(string certPath)
176 | {
177 | if (certPath == null)
178 | {
179 | throw new ArgumentNullException($"{nameof(certPath)}");
180 | }
181 | var keyPath = Path.ChangeExtension(certPath, ".key");
182 |
183 | if (!File.Exists(keyPath))
184 | {
185 | throw new ArgumentException("The key file does not exist.");
186 | }
187 |
188 | using var keyFileReader = File.OpenText(keyPath);
189 | var pemReader = new PemReader(keyFileReader);
190 | var keyParameters = (RsaPrivateCrtKeyParameters)pemReader.ReadObject();
191 |
192 | using var certFileStream = File.OpenRead(certPath);
193 | var certificates = new X509CertificateParser().ReadCertificates(certFileStream).OfType().ToArray();
194 | return new CertificateChainWithPrivateKey(certificates, keyParameters);
195 | }
196 | }
197 | }
--------------------------------------------------------------------------------
/ConcertoCLI/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Reflection;
7 | using System.Text;
8 | using Org.BouncyCastle.Crypto.Parameters;
9 | using Org.BouncyCastle.OpenSsl;
10 | using Org.BouncyCastle.X509;
11 |
12 | namespace LowLevelDesign.Concerto
13 | {
14 | internal static class Program
15 | {
16 | private static readonly Assembly AppAssembly = Assembly.GetExecutingAssembly();
17 | private static readonly AssemblyName AppName = AppAssembly.GetName();
18 |
19 | private static CertificateChainWithPrivateKey ReadOrCreateCA(string certPath)
20 | {
21 | var directory = Path.GetDirectoryName(certPath) ?? Environment.CurrentDirectory;
22 | var baseName = Path.GetFileNameWithoutExtension(certPath);
23 | var keyPath = Path.Combine(directory, baseName + ".key");
24 | certPath = Path.Combine(directory, baseName + ".pem");
25 |
26 | if (!File.Exists(keyPath) || !File.Exists(certPath))
27 | {
28 | Console.WriteLine($"[info] missing CA certificate or key, creating a new one: " +
29 | $"{Path.Combine(directory, baseName + ".pem")}");
30 | var certWithKey = CertificateCreator.CreateCACertificate();
31 | CertificateFileStore.SaveCertificate(certWithKey, certPath);
32 | return certWithKey;
33 | }
34 |
35 | using var keyFileReader = File.OpenText(keyPath);
36 | var pemReader = new PemReader(keyFileReader);
37 | var keyParameters = (RsaPrivateCrtKeyParameters)pemReader.ReadObject();
38 |
39 | using var certFileStream = File.OpenRead(certPath);
40 | var certificates = new X509CertificateParser().ReadCertificates(
41 | certFileStream).OfType().ToArray();
42 | return new CertificateChainWithPrivateKey(certificates, keyParameters);
43 | }
44 |
45 | private static string SanitizeFileName(string host)
46 | {
47 | return Path.GetInvalidFileNameChars().Aggregate(host, (host, ch) => host.Replace(ch, '_'));
48 | }
49 |
50 | private static void ShowInfoAndUsage()
51 | {
52 | Console.WriteLine($"{AppName.Name} v{AppName.Version} - creates certificates for development purposes");
53 | var customAttrs = AppAssembly.GetCustomAttributes(typeof(AssemblyCompanyAttribute), true);
54 | Debug.Assert(customAttrs.Length > 0);
55 | Console.WriteLine($"Copyright (C) 2021 {((AssemblyCompanyAttribute)customAttrs[0]).Company}");
56 | Console.WriteLine();
57 | Console.WriteLine("Certificates are always created in the current directory. If Root CA does not ");
58 | Console.WriteLine("exist, it will be automatically created.");
59 | Console.WriteLine();
60 | Console.WriteLine("Usage examples:");
61 | Console.WriteLine();
62 | Console.WriteLine($" $ {AppName.Name} www.test.com");
63 | Console.WriteLine(" Creates a certificate for www.test.com.");
64 | Console.WriteLine();
65 | Console.WriteLine($" $ {AppName.Name} -int my-intermediate");
66 | Console.WriteLine(" Creates an intermediate certificate.");
67 | Console.WriteLine();
68 | Console.WriteLine($" $ {AppName.Name} -ca my-intermediate.pem www.test.com");
69 | Console.WriteLine(" Creates a certificate for www.test.com and signs it with the my-intermediate CA.");
70 | Console.WriteLine();
71 | Console.WriteLine("Options:");
72 | Console.WriteLine(" -ca Specifies which CA certificate to use.");
73 | Console.WriteLine(" -client Allow a client to authenticate using the certificate.");
74 | Console.WriteLine(" -chain Add the certificate chain to the certificate file.");
75 | Console.WriteLine(" -ecdsa Use Elliptic Curve key instead of RSA.");
76 | Console.WriteLine(" -pfx Save the certificate and the key in a .pfx file.");
77 | Console.WriteLine(" -p Use a password to encrypt the certificate private key. ");
78 | Console.WriteLine(" Concerto will ask for the password.");
79 | Console.WriteLine(" -help Shows this help screen.");
80 | Console.WriteLine();
81 | }
82 |
83 | private static string ReadUserPassword()
84 | {
85 | Console.Write("Password: ");
86 | var buffer = new StringBuilder();
87 | while (true)
88 | {
89 | var k = Console.ReadKey(true);
90 | if (k.Key == ConsoleKey.Enter)
91 | {
92 | if (buffer.Length == 0)
93 | {
94 | throw new CommandLineArgumentException("password should contain at least one character");
95 | }
96 | else
97 | {
98 | return buffer.ToString();
99 | }
100 | }
101 | if (k.Key == ConsoleKey.Backspace && buffer.Length > 0)
102 | {
103 | buffer.Length -= 1;
104 | }
105 | else if (!char.IsControl(k.KeyChar))
106 | {
107 | buffer.Append(k.KeyChar);
108 | }
109 | }
110 | }
111 |
112 | private static int Main(string[] args)
113 | {
114 | var flags = new[] { "int", "client", "ecdsa", "chain", "pfx", "p", "h", "?", "help" };
115 | var parsedArgs = CommandLineHelper.ParseArgs(flags, args);
116 |
117 | if (parsedArgs.ContainsKey("h") || parsedArgs.ContainsKey("help") || parsedArgs.ContainsKey("?"))
118 | {
119 | ShowInfoAndUsage();
120 | return 1;
121 | }
122 |
123 | try
124 | {
125 | if (!parsedArgs.TryGetValue("ca", out var rootCertPath))
126 | {
127 | rootCertPath = Path.Combine(Environment.CurrentDirectory, "concertoCA.pem");
128 | }
129 |
130 | var password = parsedArgs.ContainsKey("p") ? ReadUserPassword() : null;
131 |
132 | if (parsedArgs.ContainsKey("int"))
133 | {
134 | // we are creating intermediate certificate
135 | if (!parsedArgs.TryGetValue(string.Empty, out var certName))
136 | {
137 | throw new CommandLineArgumentException(
138 | "-int: you need to provide a name for the intermediate certificate");
139 | }
140 |
141 | var rootCertWithKey = ReadOrCreateCA(rootCertPath);
142 | CertificateFileStore.SaveCertificate(
143 | CertificateCreator.CreateCACertificate(certName, rootCertWithKey),
144 | Path.Combine(Environment.CurrentDirectory, certName + ".pem"),
145 | parsedArgs.ContainsKey("chain"), password);
146 | }
147 | else
148 | {
149 | parsedArgs.TryGetValue(string.Empty, out var hostsStr);
150 | var hosts = (hostsStr ?? "").Split(new[] { ',', ' ', '\t' },
151 | StringSplitOptions.RemoveEmptyEntries);
152 | if (hosts.Length == 0)
153 | {
154 | throw new CommandLineArgumentException(
155 | "you need to provide at least one name to create a certificate");
156 | }
157 |
158 | var rootCertWithKey = ReadOrCreateCA(rootCertPath);
159 | var cert = CertificateCreator.CreateCertificate(hosts, rootCertWithKey,
160 | parsedArgs.ContainsKey("client"), parsedArgs.ContainsKey("ecdsa"));
161 |
162 | var extension = parsedArgs.ContainsKey("pfx") ? ".pfx" : ".pem";
163 | CertificateFileStore.SaveCertificate(cert, Path.Combine(Environment.CurrentDirectory,
164 | SanitizeFileName(hosts[0]) + extension), parsedArgs.ContainsKey("chain"), password);
165 | }
166 | return 0;
167 | }
168 | catch (Exception ex) when (ex is CommandLineArgumentException || ex is ArgumentException)
169 | {
170 | Console.WriteLine($"[error] {ex.Message}");
171 | Console.WriteLine($" {AppName.Name} -help to see usage info.");
172 | return 1;
173 | }
174 | catch (Exception ex)
175 | {
176 | Console.WriteLine($"[critical] {ex.Message}");
177 | Console.WriteLine("If this error persists, please report it at https://github.com/lowleveldesign/concerto/issues, " +
178 | "providing the below details.");
179 | Console.WriteLine("=== Details ===");
180 | Console.WriteLine($"{ex.GetType()}: {ex.Message}");
181 | Console.WriteLine();
182 | Console.WriteLine($"Command line: {Environment.CommandLine}");
183 | Console.WriteLine($"OS: {Environment.OSVersion}");
184 | Console.WriteLine($"x64 (OS): {Environment.Is64BitOperatingSystem}");
185 | Console.WriteLine($"x64 (Process): {Environment.Is64BitProcess}");
186 | return 1;
187 | }
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/Concerto/CertificateCreator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using Org.BouncyCastle.Asn1;
5 | using Org.BouncyCastle.Asn1.Sec;
6 | using Org.BouncyCastle.Asn1.X509;
7 | using Org.BouncyCastle.Crypto;
8 | using Org.BouncyCastle.Crypto.Generators;
9 | using Org.BouncyCastle.Crypto.Operators;
10 | using Org.BouncyCastle.Crypto.Parameters;
11 | using Org.BouncyCastle.Crypto.Prng;
12 | using Org.BouncyCastle.Math;
13 | using Org.BouncyCastle.Security;
14 | using Org.BouncyCastle.Utilities;
15 | using Org.BouncyCastle.X509;
16 | using Org.BouncyCastle.X509.Extension;
17 |
18 | namespace LowLevelDesign.Concerto
19 | {
20 | public sealed class CertificateChainWithPrivateKey
21 | {
22 | public CertificateChainWithPrivateKey(X509Certificate[] certificates, AsymmetricKeyParameter privateKey)
23 | {
24 | Certificates = certificates;
25 | PrivateKey = privateKey;
26 | }
27 |
28 | public X509Certificate PrimaryCertificate => Certificates[0];
29 |
30 | public X509Certificate[] Certificates { get; }
31 |
32 | public AsymmetricKeyParameter PrivateKey { get; }
33 | }
34 |
35 | public static class CertificateCreator
36 | {
37 | private static readonly string MachineName = Environment.MachineName;
38 | private static readonly string UserName = Environment.UserName;
39 |
40 | private static AsymmetricCipherKeyPair GenerateRsaKeyPair(SecureRandom secureRandom, int strength)
41 | {
42 | var keyParameters = new KeyGenerationParameters(secureRandom, strength);
43 | var keyPairGenerator = new RsaKeyPairGenerator();
44 | keyPairGenerator.Init(keyParameters);
45 | return keyPairGenerator.GenerateKeyPair();
46 | }
47 |
48 | private static AsymmetricCipherKeyPair GenerateEllipticCurveKeyPair(SecureRandom secureRandom)
49 | {
50 | var keyPairGenerator = new ECKeyPairGenerator();
51 | keyPairGenerator.Init(new ECKeyGenerationParameters(SecObjectIdentifiers.SecP256r1, secureRandom));
52 | return keyPairGenerator.GenerateKeyPair();
53 | }
54 |
55 | private static BigInteger GenerateRandomSerialNumber(SecureRandom secureRandom)
56 | {
57 | return BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.One.ShiftLeft(128), secureRandom);
58 | }
59 |
60 | private static X509Certificate[] BuildCertificateChain(X509Certificate primaryCertificate,
61 | X509Certificate[] issuerChain)
62 | {
63 | var certChain = new X509Certificate[issuerChain.Length + 1];
64 | certChain[0] = primaryCertificate;
65 | Array.Copy(issuerChain, 0, certChain, 1, issuerChain.Length);
66 | return certChain;
67 | }
68 |
69 | ///
70 | /// Creates a CA certificate.
71 | ///
72 | /// The name that should appear on the certificate in the subject field.
73 | /// If it's an intermediate CA, you should provide here the Root CA certificate. Otherwise, pass null.
74 | /// A CA certificate chain with a private key of the requested certificate.
75 | public static CertificateChainWithPrivateKey CreateCACertificate(
76 | string name = "Concerto",
77 | CertificateChainWithPrivateKey? issuer = null)
78 | {
79 | var randomGenerator = new CryptoApiRandomGenerator();
80 | var secureRandom = new SecureRandom(randomGenerator);
81 |
82 | // key
83 | var keyPair = GenerateRsaKeyPair(secureRandom, 3072);
84 |
85 | var certificateGenerator = new X509V3CertificateGenerator();
86 |
87 | // serial number
88 | certificateGenerator.SetSerialNumber(GenerateRandomSerialNumber(secureRandom));
89 |
90 | // set subject
91 | var subjectName = new X509Name($"O={name} CA,OU={UserName}@{MachineName},CN={name} {UserName}@{MachineName}");
92 | certificateGenerator.SetSubjectDN(subjectName);
93 |
94 | certificateGenerator.SetNotAfter(DateTime.UtcNow.AddYears(10));
95 | certificateGenerator.SetNotBefore(DateTime.UtcNow);
96 |
97 | certificateGenerator.SetPublicKey(keyPair.Public);
98 |
99 | // issuer information
100 | if (issuer != null) {
101 | certificateGenerator.SetIssuerDN(issuer.PrimaryCertificate.SubjectDN);
102 | certificateGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false,
103 | new AuthorityKeyIdentifier(
104 | SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuer.PrimaryCertificate.GetPublicKey())));
105 | } else {
106 | certificateGenerator.SetIssuerDN(subjectName);
107 | }
108 |
109 | // SKID
110 | certificateGenerator.AddExtension(X509Extensions.SubjectKeyIdentifier, false,
111 | new SubjectKeyIdentifierStructure(keyPair.Public));
112 |
113 | // CA constrains, we allow one intermediate certificate if root
114 | certificateGenerator.AddExtension(X509Extensions.BasicConstraints.Id, true,
115 | new BasicConstraints(issuer == null ? 1 : 0));
116 |
117 | // usage
118 | certificateGenerator.AddExtension(X509Extensions.KeyUsage, true,
119 | new KeyUsage(KeyUsage.KeyCertSign | KeyUsage.CrlSign));
120 |
121 | var signatureFactory = new Asn1SignatureFactory("SHA256WithRSA",
122 | issuer != null ? issuer.PrivateKey : keyPair.Private, secureRandom);
123 |
124 | var certificate = certificateGenerator.Generate(signatureFactory);
125 |
126 | return new CertificateChainWithPrivateKey(
127 | BuildCertificateChain(certificate, issuer?.Certificates ?? Array.Empty()),
128 | keyPair.Private);
129 | }
130 |
131 | ///
132 | /// Create a certificate for domains, IP addresses, or URIs.
133 | ///
134 | ///
135 | /// Host for which the certificate is created. Could be domains, IP addresses, or URIs.
136 | /// Wildcards are supported.
137 | ///
138 | /// The issuer certificate.
139 | /// Defines whether this certificate will be used for client authentication.
140 | /// Create a certificate with an ECDSA key.
141 | ///
142 | public static CertificateChainWithPrivateKey CreateCertificate(
143 | string[] hosts,
144 | CertificateChainWithPrivateKey issuer,
145 | bool client = false,
146 | bool ecdsa = false)
147 | {
148 | var randomGenerator = new CryptoApiRandomGenerator();
149 | var secureRandom = new SecureRandom(randomGenerator);
150 |
151 | // generate the key
152 | var keyPair = ecdsa ? GenerateEllipticCurveKeyPair(secureRandom) : GenerateRsaKeyPair(secureRandom, 2048);
153 | var certificateGenerator = new X509V3CertificateGenerator();
154 |
155 | // serial number
156 | certificateGenerator.SetSerialNumber(GenerateRandomSerialNumber(secureRandom));
157 |
158 | // set subject
159 | var subject = new X509Name($"O=concerto development,OU={UserName}@{MachineName},CN={hosts[0]}");
160 | certificateGenerator.SetSubjectDN(subject);
161 | certificateGenerator.SetNotAfter(DateTime.UtcNow.AddDays(820));
162 | certificateGenerator.SetNotBefore(DateTime.UtcNow);
163 | certificateGenerator.SetPublicKey(keyPair.Public);
164 |
165 | // not CA
166 | certificateGenerator.AddExtension(X509Extensions.BasicConstraints.Id, true,
167 | new BasicConstraints(false));
168 |
169 | // set issuer data
170 | certificateGenerator.SetIssuerDN(issuer.PrimaryCertificate.SubjectDN);
171 | certificateGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false,
172 | new AuthorityKeyIdentifier(
173 | SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuer.PrimaryCertificate.GetPublicKey())));
174 |
175 | // usage
176 | certificateGenerator.AddExtension(X509Extensions.KeyUsage, true,
177 | new KeyUsage(KeyUsage.KeyEncipherment | KeyUsage.DigitalSignature));
178 |
179 | var extendedKeyUsages = new List();
180 | if (client) {
181 | extendedKeyUsages.Add(KeyPurposeID.IdKPClientAuth);
182 | }
183 |
184 | extendedKeyUsages.Add(KeyPurposeID.IdKPServerAuth);
185 | certificateGenerator.AddExtension(X509Extensions.ExtendedKeyUsage.Id,
186 | false, new ExtendedKeyUsage(extendedKeyUsages));
187 | var subjectAlternativeNames = new List(hosts.Length);
188 | foreach (var host in hosts) {
189 | if (Uri.TryCreate(host, UriKind.Absolute, out _)) {
190 | subjectAlternativeNames.Add(new GeneralName(GeneralName.UniformResourceIdentifier, host));
191 | } else if (!string.IsNullOrEmpty(host)) {
192 | var h = host[0] == '*' ? "wildcard" + host[1..] : host;
193 | switch (Uri.CheckHostName(h)) {
194 | case UriHostNameType.IPv4:
195 | case UriHostNameType.IPv6:
196 | subjectAlternativeNames.Add(new GeneralName(GeneralName.IPAddress, host));
197 | break;
198 | case UriHostNameType.Dns:
199 | subjectAlternativeNames.Add(new GeneralName(GeneralName.DnsName, host));
200 | break;
201 | default:
202 | Trace.Write($"[warning] unrecognized host name type: {host}");
203 | break;
204 | }
205 | }
206 | }
207 |
208 | if (subjectAlternativeNames.Count > 0) {
209 | certificateGenerator.AddExtension(X509Extensions.SubjectAlternativeName.Id, false,
210 | new DerSequence(subjectAlternativeNames.ToArray()));
211 | }
212 |
213 | var signatureFactory = new Asn1SignatureFactory("SHA256WithRSA", issuer.PrivateKey, secureRandom);
214 | var certificate = certificateGenerator.Generate(signatureFactory);
215 |
216 | return new CertificateChainWithPrivateKey(
217 | BuildCertificateChain(certificate, issuer.Certificates), keyPair.Private);
218 | }
219 | }
220 | }
--------------------------------------------------------------------------------