diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml new file mode 100644 index 0000000..0e4d87b --- /dev/null +++ b/.github/workflows/build-validation.yml @@ -0,0 +1,82 @@ +name: Build Validation +on: + push: + branches: + - feature/* + - bugfix/* + paths: + - '**.cs' + - '**.csproj' + + pull_request: + branches: + - main + paths: + - '**.cs' + - '**.csproj' + + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + include: + - os: ubuntu-22.04 + runtime: linux-x64 + - os: ubuntu-22.04 + runtime: linux-arm64 + - os: macos-14 + runtime: osx-x64 + - os: macos-14 + runtime: osx-arm64 + - os: windows-2022 + runtime: win-x64 + - os: windows-2022 + runtime: win-arm64 + runs-on: ${{ matrix.os }} + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + cache: true + config-file: ./nuget.config + cache-dependency-path: '**/packages.lock.json' + + - name: Install Dependencies + shell: bash + run: dotnet restore src --locked-mode + + - name: Build + shell: bash + run: dotnet build src/CertGen --configuration Release --runtime ${{ matrix.runtime }} --no-restore + + # See: https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/cross-compile#linux + - name: Install Packages for linux-arm64 + if: ${{ matrix.runtime == 'linux-arm64' }} + shell: bash + run: | + sudo dpkg --add-architecture arm64 + sudo bash -c 'cat > /etc/apt/sources.list.d/arm64.list < /etc/apt/sources.list.d/arm64.list <> $GITHUB_ENV + dotnet publish src/CertGen --configuration Release --runtime ${{ matrix.runtime }} --no-restore --no-build --output $ARTIFACTS_DIR + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACTS_DIR }} + path: ${{ env.ARTIFACTS_DIR }} + if-no-files-found: error + include-hidden-files: true + retention-days: 90 + + release: + runs-on: ubuntu-22.04 + needs: + - build + permissions: + contents: write + steps: + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release create v${{ needs.build.outputs.SemVer2 }} \ + --title v${{ needs.build.outputs.SemVer2 }} \ + --target ${{ github.sha }} \ + --latest=false \ + --prerelease \ + --repo ${{ github.repository }} \ + --generate-notes + + - name: Download Release Artifact + uses: actions/download-artifact@v4 + with: + path: downloaded + pattern: cert-gent_v${{ needs.build.outputs.SemVer2 }}_* + + - name: Zip Release Artifact + shell: bash + run: | + mkdir artifacts + cd downloaded + for artifact in */; do + artifact_dir=$(basename "$artifact") + tar -czvf "../artifacts/${artifact%/}.tar.gz" -C "${artifact_dir}" . + done + + - name: Publish Release Artifact to GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release upload v${{ needs.build.outputs.SemVer2 }} artifacts/* \ + --clobber \ + --repo ${{ github.repository }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a9d9818 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,182 @@ +name: Release +on: + workflow_dispatch: + +jobs: + preparing: + runs-on: ubuntu-22.04 + permissions: + contents: write + outputs: + ref: release/v${{ steps.versioning.outputs.SimpleVersion }} + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git user for CI + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Setup Nerdbank.GitVersioning + uses: dotnet/nbgv@master + id: versioning + with: + path: src/CertGen + + - name: Prepare Release + shell: bash + run: nbgv prepare-release + working-directory: src/CertGen + + - name: Push Release Branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + git push origin ${{ github.ref }} + git checkout release/v${{ steps.versioning.outputs.SimpleVersion }} + git push --set-upstream origin release/v${{ steps.versioning.outputs.SimpleVersion }} + + build: + strategy: + matrix: + include: + - os: ubuntu-22.04 + runtime: linux-x64 + - os: ubuntu-22.04 + runtime: linux-arm64 + - os: macos-14 + runtime: osx-x64 + - os: macos-14 + runtime: osx-arm64 + - os: windows-2022 + runtime: win-x64 + - os: windows-2022 + runtime: win-arm64 + runs-on: ${{ matrix.os }} + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + ARTIFACTS_DIR: '' + outputs: + SimpleVersion: ${{ steps.versioning.outputs.SimpleVersion }} + needs: + - preparing + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.preparing.outputs.ref }} + + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + cache: true + config-file: nuget.config + cache-dependency-path: '**/packages.lock.json' + + - name: Setup Nerdbank.GitVersioning + uses: dotnet/nbgv@master + id: versioning + with: + path: src/CertGen + + - name: Install Dependencies + shell: bash + run: dotnet restore src --locked-mode + + - name: Build + shell: bash + run: | + dotnet build src/CertGen \ + --configuration Release \ + --runtime ${{ matrix.runtime }} \ + --no-restore \ + -p:PublicRelease=true \ + -p:Version=${{ steps.versioning.outputs.CloudBuildNumber }} \ + -p:AssemblyVersion=${{ steps.versioning.outputs.AssemblyVersion }} \ + -p:FileVersion=${{ steps.versioning.outputs.AssemblyFileVersion }} \ + -p:AssemblyInformationalVersion=${{ steps.versioning.outputs.AssemblyInformationalVersion }} + + # See: https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/cross-compile#linux + - name: Install Packages for linux-arm64 + if: ${{ matrix.runtime == 'linux-arm64' }} + shell: bash + run: | + sudo dpkg --add-architecture arm64 + sudo bash -c 'cat > /etc/apt/sources.list.d/arm64.list <> $GITHUB_ENV + dotnet publish src/CertGen --configuration Release --runtime ${{ matrix.runtime }} --no-restore --no-build --output $ARTIFACTS_DIR + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACTS_DIR }} + path: ${{ env.ARTIFACTS_DIR }} + if-no-files-found: error + include-hidden-files: true + retention-days: 90 + + release: + runs-on: ubuntu-22.04 + needs: + - build + permissions: + contents: write + steps: + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release create v${{ needs.build.outputs.SimpleVersion }} \ + --title v${{ needs.build.outputs.SimpleVersion }} \ + --target ${{ github.sha }} \ + --latest=true \ + --repo ${{ github.repository }} \ + --generate-notes + + - name: Download Release Artifact + uses: actions/download-artifact@v4 + with: + path: downloaded + pattern: cert-gent_v${{ needs.build.outputs.SimpleVersion }}_* + + - name: Zip Release Artifact + shell: bash + run: | + mkdir artifacts + cd downloaded + for artifact in */; do + artifact_dir=$(basename "$artifact") + tar -czvf "../artifacts/${artifact%/}.tar.gz" -C "${artifact_dir}" . + done + + - name: Publish Release Artifact to GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release upload v${{ needs.build.outputs.SimpleVersion }} artifacts/* \ + --clobber \ + --repo ${{ github.repository }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e57f18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..6dbc024 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,364 @@ +root = true + +# All files +[*] +indent_style = space + +# Xml files +[*.xml] +indent_size = 2 + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = true + +#### .NET Coding Conventions #### +[*.{cs,vb}] + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### +[*.cs] + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### +[*.{cs,vb}] + +# Naming rules + +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case + diff --git a/src/CertGen.sln b/src/CertGen.sln new file mode 100644 index 0000000..754a014 --- /dev/null +++ b/src/CertGen.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CertGen", "CertGen\CertGen.csproj", "{BD281212-C737-41FA-B440-E3CA326DC7AE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BD281212-C737-41FA-B440-E3CA326DC7AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD281212-C737-41FA-B440-E3CA326DC7AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD281212-C737-41FA-B440-E3CA326DC7AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD281212-C737-41FA-B440-E3CA326DC7AE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/CertGen/CertGen.csproj b/src/CertGen/CertGen.csproj new file mode 100644 index 0000000..781198b --- /dev/null +++ b/src/CertGen/CertGen.csproj @@ -0,0 +1,16 @@ + + + + cert-gen + Sisa.Security + Generates a self-signed certificate for development purposes. + Exe + true + win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 + + + + + + + diff --git a/src/CertGen/CommandOptions/Algorithm.cs b/src/CertGen/CommandOptions/Algorithm.cs new file mode 100644 index 0000000..535ea2a --- /dev/null +++ b/src/CertGen/CommandOptions/Algorithm.cs @@ -0,0 +1,9 @@ +namespace Sisa.Security; + +public enum Algorithm +{ + SHA256, + SHA384, + SHA512 +} + diff --git a/src/CertGen/CommandOptions/CreateEcdsaCommandOptions.cs b/src/CertGen/CommandOptions/CreateEcdsaCommandOptions.cs new file mode 100644 index 0000000..52b81d6 --- /dev/null +++ b/src/CertGen/CommandOptions/CreateEcdsaCommandOptions.cs @@ -0,0 +1,35 @@ +using System.CommandLine; +using System.CommandLine.Binding; + +namespace Sisa.Security; + +public record CreateEcdsaCommandOptions : GlobalOptions +{ + public NamedCurve NamedCurve { get; set; } = NamedCurve.nistP256; +} + +public sealed class CreateEcdsaCommandOptionsBinder( + Option certName, + Option algorithm, + Option namedCurve, + Option dnsNames, + Option pfxPassword, + Option organizationName, + Option organizationUnitName, + Option commonName +) : GlobalOptionsBinder( + certName, + algorithm, + dnsNames, + pfxPassword, + organizationName, + organizationUnitName, + commonName +) +{ + protected override CreateEcdsaCommandOptions GetBoundValue(BindingContext bindingContext) => + base.GetBoundValue(bindingContext) with + { + NamedCurve = bindingContext.ParseResult.GetValueForOption(namedCurve) + }; +} diff --git a/src/CertGen/CommandOptions/CreateRsaCommandOptions.cs b/src/CertGen/CommandOptions/CreateRsaCommandOptions.cs new file mode 100644 index 0000000..0834751 --- /dev/null +++ b/src/CertGen/CommandOptions/CreateRsaCommandOptions.cs @@ -0,0 +1,35 @@ +using System.CommandLine; +using System.CommandLine.Binding; + +namespace Sisa.Security; + +public sealed record CreateRsaCommandOptions : GlobalOptions +{ + public int KeySize { get; set; } = 2048; +} + +public sealed class CreateRsaCommandOptionsBinder( + Option certName, + Option algorithm, + Option keySize, + Option dnsNames, + Option pfxPassword, + Option organizationName, + Option organizationUnitName, + Option commonName +) : GlobalOptionsBinder( + certName, + algorithm, + dnsNames, + pfxPassword, + organizationName, + organizationUnitName, + commonName +) +{ + protected override CreateRsaCommandOptions GetBoundValue(BindingContext bindingContext) => + base.GetBoundValue(bindingContext) with + { + KeySize = bindingContext.ParseResult.GetValueForOption(keySize) + }; +} diff --git a/src/CertGen/CommandOptions/GlobalOptions.cs b/src/CertGen/CommandOptions/GlobalOptions.cs new file mode 100644 index 0000000..1a01c9f --- /dev/null +++ b/src/CertGen/CommandOptions/GlobalOptions.cs @@ -0,0 +1,45 @@ +using System.CommandLine; +using System.CommandLine.Binding; + +namespace Sisa.Security; + +public record GlobalOptions +{ + public string CertName { get; set; } = null!; + + public Algorithm Algorithm { get; set; } + + public string[] DnsNames { get; set; } = []; + public string? PfxPassword { get; set; } + + public string? OrganizationName { get; set; } + + public string? OrganizationUnitName { get; set; } + + public string? CommonName { get; set; } + +} + +public abstract class GlobalOptionsBinder( + Option certName, + Option algorithm, + Option dnsNames, + Option pfxPassword, + Option organizationName, + Option organizationUnitName, + Option commonName +) : BinderBase + where TOptions : GlobalOptions, new() +{ + protected override TOptions GetBoundValue(BindingContext bindingContext) => + new() + { + CertName = bindingContext.ParseResult.GetValueForOption(certName)!, + Algorithm = bindingContext.ParseResult.GetValueForOption(algorithm), + DnsNames = bindingContext.ParseResult.GetValueForOption(dnsNames) ?? [], + PfxPassword = bindingContext.ParseResult.GetValueForOption(pfxPassword), + OrganizationName = bindingContext.ParseResult.GetValueForOption(organizationName), + OrganizationUnitName = bindingContext.ParseResult.GetValueForOption(organizationUnitName), + CommonName = bindingContext.ParseResult.GetValueForOption(commonName) + }; +} diff --git a/src/CertGen/CommandOptions/NamedCurve.cs b/src/CertGen/CommandOptions/NamedCurve.cs new file mode 100644 index 0000000..fede0b1 --- /dev/null +++ b/src/CertGen/CommandOptions/NamedCurve.cs @@ -0,0 +1,8 @@ +namespace Sisa.Security; + +public enum NamedCurve +{ + nistP256, + nistP384, + nistP521 +} diff --git a/src/CertGen/Handlers.cs b/src/CertGen/Handlers.cs new file mode 100644 index 0000000..e708c05 --- /dev/null +++ b/src/CertGen/Handlers.cs @@ -0,0 +1,335 @@ +using System.CommandLine; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +using Sisa.Security.Helpers; + +namespace Sisa.Security; + +public static class CommandHandler +{ + private static readonly string RootCAName = "root-ca"; + private static readonly string RootCACommonName = "Sisa Development Root CA"; + + public static Command Initialize() + { + #region Global options + + var certNameOption = new Option( + name: "--name", + description: "Certificate name, the output files name with have the format {name}-{ecdsa|rsa}-key.pem, {name}-{ecdsa|rsa}-cert.pem, {name}-{ecdsa|rsa}-cert.pfx" + ) + { + IsRequired = true, + }; + certNameOption.AddAlias("-n"); + + var algorithmOption = new Option( + name: "--algorithm", + description: "Hash algorithm", + getDefaultValue: () => Algorithm.SHA256 + ) + { + IsRequired = false, + }; + + algorithmOption.AddAlias("-a"); + + var dnsNamesOption = new Option( + name: "--dns-names", + description: "DNS names for the certificate", + getDefaultValue: () => ["localhost"] + ) + { + IsRequired = false, + }; + dnsNamesOption.AddAlias("-d"); + + var pfxPasswordOption = new Option( + name: "--pfx-password", + description: "Password for PFX export, if not provided, PFX export will be skipped" + ) + { + IsRequired = false, + }; + pfxPasswordOption.AddAlias("-p"); + + var organizationNameOption = new Option( + name: "--organization-name", + description: "Organization name", + getDefaultValue: () => "Sisa Solutions" + ) + { + IsRequired = false, + }; + + organizationNameOption.AddAlias("-o"); + + var organizationUnitNameOption = new Option( + name: "--organization-unit-name", + description: "Organization unit name", + getDefaultValue: () => $"{Environment.UserName}@{Environment.UserDomainName}" + ) + { + IsRequired = false, + }; + + organizationUnitNameOption.AddAlias("-ou"); + + var commonNameOption = new Option( + name: "--common-name", + description: "Common name", + getDefaultValue: () => "Sisa Development" + ) + { + IsRequired = false, + }; + + commonNameOption.AddAlias("-cn"); + + #endregion + + #region Root Command + + var rootCommand = new RootCommand( + description: "Sisa Development Certificate Generator" + ); + + rootCommand.AddGlobalOption(certNameOption); + rootCommand.AddGlobalOption(algorithmOption); + rootCommand.AddGlobalOption(dnsNamesOption); + rootCommand.AddGlobalOption(pfxPasswordOption); + rootCommand.AddGlobalOption(organizationNameOption); + rootCommand.AddGlobalOption(organizationUnitNameOption); + rootCommand.AddGlobalOption(commonNameOption); + + #endregion + + # region Create ECDSA subcommand + + var ecdsaCurveOption = new Option( + name: "--curve", + description: "Named curve for the ECDSA key pair", + getDefaultValue: () => NamedCurve.nistP256 + ) + { + IsRequired = false, + }; + + ecdsaCurveOption.AddAlias("-c"); + + var createEcdsaSubCommand = new Command( + name: "ecdsa", + description: "Generate ECDSA key pair" + ) { + ecdsaCurveOption + }; + + createEcdsaSubCommand.SetHandler( + CreateEcdsaCommandHandleAsync, + new CreateEcdsaCommandOptionsBinder( + certNameOption, + algorithmOption, + ecdsaCurveOption, + dnsNamesOption, + pfxPasswordOption, + organizationNameOption, + organizationUnitNameOption, + commonNameOption + ) + ); + + rootCommand.Add(createEcdsaSubCommand); + + #endregion + + #region Create RSA subcommand + + var rsaKeySizeOption = new Option( + name: "--key-size", + description: "RSA key size in bits", + getDefaultValue: () => 2048 + ) + { + IsRequired = false, + }; + rsaKeySizeOption.AddAlias("-s"); + + var createRsaSubCommand = new Command( + name: "rsa", + description: "Generate RSA key pair" + ) { + rsaKeySizeOption, + }; + + createRsaSubCommand.SetHandler( + CreateRsaCommandHandleAsync, + new CreateRsaCommandOptionsBinder( + certNameOption, + algorithmOption, + rsaKeySizeOption, + dnsNamesOption, + pfxPasswordOption, + organizationNameOption, + organizationUnitNameOption, + commonNameOption + ) + ); + + rootCommand.Add(createRsaSubCommand); + + #endregion + + return rootCommand; + } + + private static async Task CreateEcdsaCommandHandleAsync(CreateEcdsaCommandOptions options) + { + string rootCaFilePath = ExportCertificateHelper.GetExistingFilePath($"{RootCAName}-ecdsa-cert.pem"); + string rootCaKeyFilePath = ExportCertificateHelper.GetExistingFilePath($"{RootCAName}-ecdsa-key.pem"); + + bool isRootCaExists = File.Exists(rootCaFilePath) && File.Exists(rootCaKeyFilePath); + + X509Certificate2? rootCa; + + if (isRootCaExists) + { + Console.WriteLine("Root CA exists"); + Console.WriteLine("Loading root CA certificate"); + + rootCa = GenerateSslHelper.LoadRootCACertificate(rootCaFilePath, rootCaKeyFilePath); + + if (rootCa == null) + { + await Console.Error.WriteLineAsync("Failed to load root CA certificate"); + + return; + } + } + else + { + var caSubjectName = GenerateSslHelper.BuildSubjectName(options.OrganizationName!, options.OrganizationUnitName!, RootCACommonName); + rootCa = GenerateSslHelper.CreateEcdsaRootCACertificate(caSubjectName); + } + + HashAlgorithmName hashAlgorithm = GenerateSslHelper.GetHashAlgorithm(options.Algorithm); + + var certSubjectName = GenerateSslHelper.BuildSubjectName(options.OrganizationName!, options.OrganizationUnitName!, options.CommonName!); + var cert = GenerateSslHelper.CreateEcdsaSelfSignCertificate( + rootCa, + certSubjectName, + hashAlgorithm, + options.NamedCurve, + options.DnsNames + ); + + ExportCertificateHelper.EnsureOutFolderExists(); + + List tasks = []; + + if (!isRootCaExists) + { + tasks.AddRange([ + ExportCertificateHelper.ExportEcdsaPrivateKeyPemAsync(rootCa, rootCaKeyFilePath), + ExportCertificateHelper.ExportCertificatePemAsync(rootCa, rootCaFilePath) + ]); + } + + string keyFilePath = ExportCertificateHelper.BuildExportFilePath($"{options.CertName}-ecdsa-key.pem"); + string certFilePath = ExportCertificateHelper.BuildExportFilePath($"{options.CertName}-ecdsa-cert.pem"); + + tasks.AddRange([ + ExportCertificateHelper.ExportEcdsaPrivateKeyPemAsync(cert, keyFilePath), + ExportCertificateHelper.ExportCertificatePemAsync(cert, certFilePath), + ]); + + if (!string.IsNullOrWhiteSpace(options.PfxPassword)) + { + string pfxFilePath = $"{options.CertName}-ecdsa-cert.pfx"; + + tasks.Add(ExportCertificateHelper.ExportPfxAsync(cert, pfxFilePath, options.PfxPassword)); + } + + await Task.WhenAll(tasks); + } + + private static async Task CreateRsaCommandHandleAsync(CreateRsaCommandOptions options) + { + string rootCaFilePath = ExportCertificateHelper.GetExistingFilePath($"{RootCAName}-rsa-cert.pem"); + string rootCaKeyFilePath = ExportCertificateHelper.GetExistingFilePath($"{RootCAName}-rsa-key.pem"); + + bool isRootCaExists = File.Exists(rootCaFilePath) && File.Exists(rootCaKeyFilePath); + + X509Certificate2? rootCa; + + if (isRootCaExists) + { + Console.WriteLine("Root CA exists"); + Console.WriteLine("Loading root CA certificate"); + + rootCa = GenerateSslHelper.LoadRootCACertificate(rootCaFilePath, rootCaKeyFilePath); + + if (rootCa == null) + { + await Console.Error.WriteLineAsync("Failed to load root CA certificate"); + + return; + } + } + else + { + Console.WriteLine("Root CA does not exist"); + Console.WriteLine("Creating root CA certificate"); + + var caSubjectName = GenerateSslHelper.BuildSubjectName(options.OrganizationName!, options.OrganizationUnitName!, RootCACommonName); + rootCa = GenerateSslHelper.CreateRsaRootCACertificate(caSubjectName); + } + + HashAlgorithmName hashAlgorithm = GenerateSslHelper.GetHashAlgorithm(options.Algorithm); + var certSubjectName = GenerateSslHelper.BuildSubjectName(options.OrganizationName!, options.OrganizationUnitName!, options.CommonName!); + + Console.WriteLine("Creating self-signed certificate"); + var cert = GenerateSslHelper.CreateRsaSelfSignCertificate( + rootCa, + certSubjectName, + hashAlgorithm, + options.KeySize, + options.DnsNames + ); + + ExportCertificateHelper.EnsureOutFolderExists(); + + var keyFileName = $"{options.CertName}-rsa-key.pem"; + var certFileName = $"{options.CertName}-rsa-key.pem"; + + List tasks = []; + + if (!isRootCaExists) + { + tasks.AddRange([ + ExportCertificateHelper.ExportRsaPrivateKeyPemAsync(rootCa, rootCaKeyFilePath), + ExportCertificateHelper.ExportCertificatePemAsync(rootCa, rootCaFilePath) + ]); + } + + string keyFilePath = ExportCertificateHelper.BuildExportFilePath(keyFileName); + string certFilePath = ExportCertificateHelper.BuildExportFilePath(certFileName); + + Console.WriteLine("Exporting certificate and private key"); + + tasks.AddRange([ + ExportCertificateHelper.ExportRsaPrivateKeyPemAsync(cert, keyFilePath), + ExportCertificateHelper.ExportCertificatePemAsync(cert, certFilePath), + ]); + + if (!string.IsNullOrWhiteSpace(options.PfxPassword)) + { + string pfxFilePath = $"{options.CertName}-rsa-cert.pfx"; + pfxFilePath = ExportCertificateHelper.BuildExportFilePath(pfxFilePath); + + tasks.Add(ExportCertificateHelper.ExportPfxAsync(cert, pfxFilePath, options.PfxPassword)); + } + + await Task.WhenAll(tasks); + } +} diff --git a/src/CertGen/Helpers/ExportCertificateHelper.cs b/src/CertGen/Helpers/ExportCertificateHelper.cs new file mode 100644 index 0000000..e8a3fef --- /dev/null +++ b/src/CertGen/Helpers/ExportCertificateHelper.cs @@ -0,0 +1,85 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Sisa.Security.Helpers; + +public static class ExportCertificateHelper +{ + private static readonly string OutFolder = "gen"; + + public static async Task ExportPfxAsync(X509Certificate2 certificate, string filePath, string password) + { + byte[] pfxBytes = certificate.Export(X509ContentType.Pfx, password); + + await File.WriteAllBytesAsync(filePath, pfxBytes); + + Console.WriteLine("Certificate exported to {0}.", filePath); + } + + public static async Task ExportCertificatePemAsync(X509Certificate2 certificate, string filePath) + { + await File.WriteAllTextAsync(filePath, certificate.ExportCertificatePem()); + + Console.WriteLine("Certificate exported to {0}.", filePath); + } + + public static async Task ExportRsaPrivateKeyPemAsync(X509Certificate2 certificate, string filePath) + { + await File.WriteAllTextAsync(filePath, certificate.GetRSAPrivateKey()!.ExportRSAPrivateKeyPem()); + + Console.WriteLine("RSA private key exported to {0}.", filePath); + } + + public static async Task ExportRsaPublicKeyPemAsync(X509Certificate2 certificate, string filePath) + { + await File.WriteAllTextAsync(filePath, certificate.GetRSAPublicKey()!.ExportRSAPublicKeyPem()); + + Console.WriteLine("RSA public key exported to {0}.", filePath); + } + + public static async Task ExportEcdsaPrivateKeyPemAsync(X509Certificate2 certificate, string filePath) + { + await File.WriteAllTextAsync(filePath, certificate.GetECDsaPrivateKey()!.ExportECPrivateKeyPem()); + + Console.WriteLine("ECDSA private key exported to {0}.", filePath); + } + + public static string GetExistingFilePath(string fileName) + { + string currentDirectory = Environment.CurrentDirectory; + + string filePath = Path.Combine(currentDirectory, OutFolder, fileName); + + return filePath; + } + + public static void EnsureOutFolderExists() + { + string currentDirectory = Environment.CurrentDirectory; + + string outFolderPath = Path.Combine(currentDirectory, OutFolder); + + if (!Directory.Exists(outFolderPath)) + { + Console.WriteLine("Creating output folder {0}.", outFolderPath); + + Directory.CreateDirectory(outFolderPath); + } + } + + public static string BuildExportFilePath(string fileName) + { + string currentDirectory = Environment.CurrentDirectory; + + string filePath = Path.Combine(currentDirectory, OutFolder, fileName); + + int i = 1; + + while (File.Exists(filePath)) + { + filePath = Path.Combine(currentDirectory, OutFolder, fileName.Replace(".", $"-{i}.")); + i++; + } + + return filePath; + } +} diff --git a/src/CertGen/Helpers/GenerateSslHelper.cs b/src/CertGen/Helpers/GenerateSslHelper.cs new file mode 100644 index 0000000..47003df --- /dev/null +++ b/src/CertGen/Helpers/GenerateSslHelper.cs @@ -0,0 +1,170 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Sisa.Security.Helpers; + +public static class GenerateSslHelper +{ + public static X509Certificate2? LoadRootCACertificate(string certPath, string keyPath) + { + try + { + var cert = X509Certificate2.CreateFromPemFile(certPath, keyPath); + + return cert; + } + catch + { + return null; + } + } + + public static X509Certificate2 CreateEcdsaRootCACertificate(string subjectName) + { + using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP521); + CertificateRequest certificateRequest = new(subjectName, key, HashAlgorithmName.SHA512); + + return CreateRootCACertificate(certificateRequest); + } + + public static X509Certificate2 CreateRsaRootCACertificate(string subjectName) + { + using RSA key = RSA.Create(4096); + CertificateRequest certificateRequest = new(subjectName, key, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); + + return CreateRootCACertificate(certificateRequest); + } + + public static X509Certificate2 CreateEcdsaSelfSignCertificate(X509Certificate2 issuerCertificate, string subjectName, HashAlgorithmName hashAlgorithm, NamedCurve namedCurve, string[] dnsNames) + { + var curve = namedCurve switch + { + NamedCurve.nistP384 => ECCurve.NamedCurves.nistP384, + NamedCurve.nistP521 => ECCurve.NamedCurves.nistP521, + _ => ECCurve.NamedCurves.nistP256, + }; + + using ECDsa key = ECDsa.Create(curve); + CertificateRequest certificateRequest = new(subjectName, key, hashAlgorithm); + + var signedCert = CreateSelfSignCertificate(issuerCertificate, certificateRequest, dnsNames); + + return signedCert.CopyWithPrivateKey(key); + } + + public static X509Certificate2 CreateRsaSelfSignCertificate(X509Certificate2 issuerCertificate, string subjectName, HashAlgorithmName hashAlgorithm, int keySize, string[] dnsNames) + { + using RSA key = RSA.Create(keySize); + CertificateRequest certificateRequest = new(subjectName, key, hashAlgorithm, RSASignaturePadding.Pkcs1); + + var signedCert = CreateSelfSignCertificate(issuerCertificate, certificateRequest, dnsNames); + + return signedCert.CopyWithPrivateKey(key); + } + + # region Private Methods + + private static X509Certificate2 CreateRootCACertificate(CertificateRequest certificateRequest) + { + DateTimeOffset currentDate = DateTimeOffset.UtcNow; + + // Add Key Usage for CA (KeyCertSign) + certificateRequest.CertificateExtensions.Add( + new X509KeyUsageExtension( + keyUsages: X509KeyUsageFlags.KeyCertSign, + critical: true + ) + ); + + // Add Basic Constraints for CA + certificateRequest.CertificateExtensions.Add( + new X509BasicConstraintsExtension( + certificateAuthority: true, + hasPathLengthConstraint: true, + pathLengthConstraint: 0, + critical: true + ) + ); + + // Add Subject Key Identifier + certificateRequest.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension(certificateRequest.PublicKey, critical: false)); + + var certificate = certificateRequest.CreateSelfSigned(currentDate.AddDays(-1), currentDate.AddYears(10)); + + return certificate; + } + + private static X509Certificate2 CreateSelfSignCertificate(X509Certificate2 issuerCertificate, CertificateRequest certificateRequest, string[] dnsNames) + { + DateTimeOffset currentDate = DateTimeOffset.UtcNow; + + certificateRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension( + certificateAuthority: false, + hasPathLengthConstraint: true, + pathLengthConstraint: 0, + critical: false + )); + + certificateRequest.CertificateExtensions.Add(new X509KeyUsageExtension( + keyUsages: X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, + critical: true + )); + + // 1.3.6.1.5.5.7.3.1 OID for Server Authentication + // 1.3.6.1.5.5.7.3.2 OID for Client Authentication + // 1.3.6.1.5.5.7.3.3 Code Signing + // 1.3.6.1.5.5.7.3.4 Email Protection + // 1.3.6.1.5.5.7.3.8 Time Stamping + + var enhancedKeyUsage = new OidCollection + { + new Oid("1.3.6.1.5.5.7.3.1"), // Server Authentication + }; + certificateRequest.CertificateExtensions.Add( + new X509EnhancedKeyUsageExtension(enhancedKeyUsage, critical: false)); + + var authorityKeyIdentifierExtension = X509AuthorityKeyIdentifierExtension.CreateFromCertificate( + issuerCertificate, includeKeyIdentifier: true, includeIssuerAndSerial: false); + + certificateRequest.CertificateExtensions.Add(authorityKeyIdentifierExtension); + + var sanBuilder = new SubjectAlternativeNameBuilder(); + + foreach (var dnsName in dnsNames) + { + sanBuilder.AddDnsName(dnsName); + } + + certificateRequest.CertificateExtensions.Add(sanBuilder.Build(critical: false)); + // Sign the certificate using the CA's private key + Span serialNumber = stackalloc byte[8]; + RandomNumberGenerator.Fill(serialNumber); + + X509Certificate2 signedCert = certificateRequest.Create( + issuerCertificate, + currentDate.AddDays(-1), + currentDate.AddYears(3), + serialNumber + ); + + return signedCert; + } + + #endregion + + public static string BuildSubjectName(string organizationName, string organizationUnitName, string commonName) + { + return $"O={organizationName}, OU={organizationUnitName}, CN={commonName}"; + } + + public static HashAlgorithmName GetHashAlgorithm(Algorithm algorithm) + { + return algorithm switch + { + Algorithm.SHA384 => HashAlgorithmName.SHA384, + Algorithm.SHA512 => HashAlgorithmName.SHA512, + _ => HashAlgorithmName.SHA256, + }; + } +} diff --git a/src/CertGen/Program.cs b/src/CertGen/Program.cs new file mode 100644 index 0000000..5c2eb04 --- /dev/null +++ b/src/CertGen/Program.cs @@ -0,0 +1,25 @@ +using System.CommandLine; + +using Sisa.Security; + +await CommandHandler.Initialize() + .InvokeAsync(args); + +// For Linux, run the following commands to install the root CA certificate +// sudo cp root-ca-cert.pem /usr/local/share/ca-certificates/root-ca-cert.crt +// sudo update-ca-certificates --fresh + +// For Windows, double-click on root-ca-cert.pem and install to Trusted Root Certification Authorities + +// For macOS, double-click on root-ca-cert.pem and install to System keychain + +// For Chrome, go to chrome://settings/certificates +// Import root-ca-cert.pem to Authorities + +// For Edge, go to edge://settings/certificates +// Import root-ca-cert.pem to Trusted Root Certification Authorities + +// For Firefox, go to about:preferences#privacy +// View Certificates -> Authorities -> Import + +// For Safari, go to Preferences -> Privacy -> Manage Website Data diff --git a/src/CertGen/packages.lock.json b/src/CertGen/packages.lock.json new file mode 100644 index 0000000..f76bd9c --- /dev/null +++ b/src/CertGen/packages.lock.json @@ -0,0 +1,121 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "iThS5LNCcLlFkUlnxCJMgMSQ6s1BgbZo9ri11DDgNxDn8StwgFVUQJ8O5N/7S037jcS1md8tWp7Zu2YconIlag==" + }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "xT8jYjlroY7SLbGtoV9vUTVW/TPgodL4Egc31a444Xe0TMytLZ3UlKQ0kxMZsy/CrWsFB6wtKnSG1SsXcWreew==" + }, + "System.CommandLine": { + "type": "Direct", + "requested": "[2.0.0-beta4.22272.1, )", + "resolved": "2.0.0-beta4.22272.1", + "contentHash": "1uqED/q2H0kKoLJ4+hI2iPSBSEdTuhfCYADeJrAqERmiGQ2NNacYKRNEQ+gFbU4glgVyK8rxI+ZOe1onEtr/Pg==" + } + }, + "net8.0/linux-arm64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "iThS5LNCcLlFkUlnxCJMgMSQ6s1BgbZo9ri11DDgNxDn8StwgFVUQJ8O5N/7S037jcS1md8tWp7Zu2YconIlag==", + "dependencies": { + "runtime.linux-arm64.Microsoft.DotNet.ILCompiler": "8.0.10" + } + }, + "runtime.linux-arm64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "8.0.10", + "contentHash": "YS61Q0I8QHpUOhIOqe+Oa1dHYAQGWkioAuMzEZWUiBnXjs8yi32KFan2ISDTR3vGVU3HGdMsb7gS/EM5X6kBVg==" + } + }, + "net8.0/linux-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "iThS5LNCcLlFkUlnxCJMgMSQ6s1BgbZo9ri11DDgNxDn8StwgFVUQJ8O5N/7S037jcS1md8tWp7Zu2YconIlag==", + "dependencies": { + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": "8.0.10" + } + }, + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "8.0.10", + "contentHash": "gH4EOwMHeOgfuEVui/dPWlLEnIipUaU64GGNZO9gUx34pjSTg2ztefOWLuARReWuwGJnZx3zbgKaiLOt4X67sA==" + } + }, + "net8.0/osx-arm64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "iThS5LNCcLlFkUlnxCJMgMSQ6s1BgbZo9ri11DDgNxDn8StwgFVUQJ8O5N/7S037jcS1md8tWp7Zu2YconIlag==", + "dependencies": { + "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": "8.0.10" + } + }, + "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "8.0.10", + "contentHash": "Dc3UsgOGeR2ZQCK5ij6oMXSDp7dDgwz8JIxHVhIzArugQkiPalUBNX3p72VcqhnutCPhUFbAsxR2B6dk5f2LGQ==" + } + }, + "net8.0/osx-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "iThS5LNCcLlFkUlnxCJMgMSQ6s1BgbZo9ri11DDgNxDn8StwgFVUQJ8O5N/7S037jcS1md8tWp7Zu2YconIlag==", + "dependencies": { + "runtime.osx-x64.Microsoft.DotNet.ILCompiler": "8.0.10" + } + }, + "runtime.osx-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "8.0.10", + "contentHash": "r7MLJ6pjKc9uyUTd6AZL9OpYdpsBNIDEIYFiN+oj00daZn7gYMudqrjEfkzE1ZtFDG9pq7cFkRxgCXXceqQtGA==" + } + }, + "net8.0/win-arm64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "iThS5LNCcLlFkUlnxCJMgMSQ6s1BgbZo9ri11DDgNxDn8StwgFVUQJ8O5N/7S037jcS1md8tWp7Zu2YconIlag==", + "dependencies": { + "runtime.win-arm64.Microsoft.DotNet.ILCompiler": "8.0.10" + } + }, + "runtime.win-arm64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "8.0.10", + "contentHash": "cfkLy8gvy4N/RwCW1UbEJBEIUfXQlg3yCwpiRmdku38pwxZl6YRE6Ai3divN9TFGGXghMBmmKA5k1ndUerHU+A==" + } + }, + "net8.0/win-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.10, )", + "resolved": "8.0.10", + "contentHash": "iThS5LNCcLlFkUlnxCJMgMSQ6s1BgbZo9ri11DDgNxDn8StwgFVUQJ8O5N/7S037jcS1md8tWp7Zu2YconIlag==", + "dependencies": { + "runtime.win-x64.Microsoft.DotNet.ILCompiler": "8.0.10" + } + }, + "runtime.win-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "8.0.10", + "contentHash": "0FC09AZCyw9ka7uvI2nGNMph9ouaCuVSvg3geu6C/U5HC9e3x5BaTn1qm7clu1Z6dit9QhJoA3HPkti9zgxQWg==" + } + } + } +} \ No newline at end of file diff --git a/src/CertGen/version.json b/src/CertGen/version.json new file mode 100644 index 0000000..d4bf741 --- /dev/null +++ b/src/CertGen/version.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.0.0-next.{height}", + "publicReleaseRefSpec": [ + "^refs/heads/release/v\\d+\\.\\d+\\.\\d+$", + "^refs/heads/hotfix/v\\d+\\.\\d+\\.\\d+$", + "^refs/tags/v\\d+\\.\\d+\\.\\d+$" + ], + "cloudBuild": { + "buildNumber": { + "enabled": true, + "includeCommitId": { + "when": "never" + } + } + }, + "release": { + "branchName": "release/v{version}", + "firstUnstableTag": "next" + } +} \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..bd542e1 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);NU5104 + true + true + $(MSBuildThisFileDirectory)artifacts + + \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 0000000..4f4e88c --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,9 @@ + + + true + + + + + + \ No newline at end of file diff --git a/src/nuget.config b/src/nuget.config new file mode 100644 index 0000000..bb42c33 --- /dev/null +++ b/src/nuget.config @@ -0,0 +1,8 @@ + + + + + + + +