├── .editorconfig
├── .github
    ├── CODEOWNERS
    ├── ISSUE_TEMPLATE
    │   ├── Bug_report.md
    │   ├── Feature_request.md
    │   ├── Question.md
    │   └── config.yml
    └── workflows
    │   ├── ci.yml
    │   └── stale.yml
├── .gitignore
├── .vscode
    ├── launch.json
    └── tasks.json
├── CONTRIBUTING.md
├── Directory.Build.props
├── Directory.Build.targets
├── LICENSE.txt
├── LettuceEncrypt.sln
├── README.md
├── SECURITY.md
├── build.ps1
├── codecov.yml
├── samples
    ├── KeyVault
    │   ├── KeyVault.csproj
    │   ├── Program.cs
    │   ├── Properties
    │   │   └── launchSettings.json
    │   ├── Startup.cs
    │   ├── appsettings.Development.json
    │   ├── appsettings.Production.json
    │   ├── appsettings.Staging.json
    │   └── appsettings.json
    └── Web
    │   ├── Program.cs
    │   ├── Properties
    │       └── launchSettings.json
    │   ├── Startup.cs
    │   ├── Web.csproj
    │   ├── appsettings.Development.json
    │   ├── appsettings.Production.json
    │   ├── appsettings.Staging.json
    │   └── appsettings.json
├── src
    ├── Directory.Build.targets
    ├── Kestrel.Certificates
    │   ├── IServerCertificateSelector.cs
    │   ├── KestrelHttpsOptionsExtensions.cs
    │   ├── McMaster.AspNetCore.Kestrel.Certificates.csproj
    │   ├── PublicAPI.Shipped.txt
    │   ├── PublicAPI.Unshipped.txt
    │   └── releasenotes.props
    ├── LettuceEncrypt.Azure
    │   ├── AzureKeyVaultLettuceEncryptOptions.cs
    │   ├── AzureLettuceEncryptExtensions.cs
    │   ├── Internal
    │   │   ├── AzureKeyVaultAccountStore.cs
    │   │   ├── AzureKeyVaultCertificateRepository.cs
    │   │   ├── CertificateClientFactory.cs
    │   │   └── SecretClientFactory.cs
    │   ├── LettuceEncrypt.Azure.csproj
    │   ├── Properties
    │   │   └── AssemblyInfo.cs
    │   ├── PublicAPI.Shipped.txt
    │   ├── PublicAPI.Unshipped.txt
    │   └── releasenotes.props
    ├── LettuceEncrypt
    │   ├── Accounts
    │   │   ├── AccountModel.cs
    │   │   └── IAccountStore.cs
    │   ├── Acme
    │   │   ├── ChallengeType.cs
    │   │   ├── DnsTxtRecordContext.cs
    │   │   ├── EabCredentials.cs
    │   │   ├── ICertificateAuthorityConfiguration.cs
    │   │   └── IDnsChallengeProvider.cs
    │   ├── FileSystemPersistenceExtensions.cs
    │   ├── ICertificateRepository.cs
    │   ├── ICertificateSource.cs
    │   ├── ILettuceEncryptServiceBuilder.cs
    │   ├── Internal
    │   │   ├── AcmeCertificateFactory.cs
    │   │   ├── AcmeCertificateLoader.cs
    │   │   ├── AcmeClient.cs
    │   │   ├── AcmeClientFactory.cs
    │   │   ├── AcmeStates
    │   │   │   ├── AcmeState.cs
    │   │   │   ├── AcmeStateMachineContext.cs
    │   │   │   ├── BeginCertificateCreationState.cs
    │   │   │   ├── CheckForRenewalState.cs
    │   │   │   └── ServerStartupState.cs
    │   │   ├── CertificateSelector.cs
    │   │   ├── DefaultCertificateAuthorityConfiguration.cs
    │   │   ├── DeveloperCertLoader.cs
    │   │   ├── Dns01DomainValidator.cs
    │   │   ├── DomainOwnershipValidator.cs
    │   │   ├── FileSystemAccountStore.cs
    │   │   ├── FileSystemCertificateRepository.cs
    │   │   ├── Http01DomainValidator.cs
    │   │   ├── HttpChallengeResponseMiddleware.cs
    │   │   ├── HttpChallengeStartupFilter.cs
    │   │   ├── IHttpChallengeResponseStore.cs
    │   │   ├── IO
    │   │   │   ├── IClock.cs
    │   │   │   ├── IConsole.cs
    │   │   │   ├── PhysicalConsole.cs
    │   │   │   └── SystemClock.cs
    │   │   ├── InMemoryHttpChallengeStore.cs
    │   │   ├── KestrelOptionsSetup.cs
    │   │   ├── LettuceEncryptApplicationBuilderExtensions.cs
    │   │   ├── LettuceEncryptServiceBuilder.cs
    │   │   ├── LoggerExtensions.cs
    │   │   ├── NoOpDnsChallengeProvider.cs
    │   │   ├── OptionsValidation.cs
    │   │   ├── PfxBuilder
    │   │   │   ├── IPfxBuilder.cs
    │   │   │   ├── IPfxBuilderFactory.cs
    │   │   │   ├── PfxBuilderFactory.cs
    │   │   │   └── PfxBuilderWrapper.cs
    │   │   ├── StartupCertificateLoader.cs
    │   │   ├── TermsOfServiceChecker.cs
    │   │   ├── TlsAlpn01DomainValidator.cs
    │   │   ├── TlsAlpnChallengeResponder.cs
    │   │   ├── X509CertStore.cs
    │   │   └── X509CertificateHelpers.cs
    │   ├── KeyAlgorithm.cs
    │   ├── LettuceEncrypt.csproj
    │   ├── LettuceEncryptKestrelHttpsOptionsExtensions.cs
    │   ├── LettuceEncryptOptions.cs
    │   ├── LettuceEncryptServiceCollectionExtensions.cs
    │   ├── Properties
    │   │   └── AssemblyInfo.cs
    │   ├── PublicAPI.Shipped.txt
    │   ├── PublicAPI.Unshipped.txt
    │   └── releasenotes.props
    ├── StrongName.snk
    ├── common.psm1
    └── icon.png
└── test
    ├── Integration
        ├── README.md
        ├── ngrok.yml
        └── run_ngrok
    ├── LettuceEncrypt.Azure.UnitTests
        ├── AzureKeyVaultAccountStoreTests.cs
        ├── AzureKeyVaultTests.cs
        ├── ConfigurationBindingTests.cs
        └── LettuceEncrypt.Azure.UnitTests.csproj
    └── LettuceEncrypt.UnitTests
        ├── AcmeCertificateFactoryTest.cs
        ├── CertificateSelectorTests.cs
        ├── ChallengeTypeTests.cs
        ├── ConfigurationBindingTests.cs
        ├── DefaultCertificateAuthorityConfigurationTests.cs
        ├── DeveloperCertLoaderTests.cs
        ├── FileSystemAccountStoreTests.cs
        ├── FileSystemCertificateRepoTests.cs
        ├── HttpChallengeResponseMiddlewareTests.cs
        ├── KestrelHttpsOptionsExtensionsTests.cs
        ├── KestrelOptionsSetupTests.cs
        ├── LettuceEncrypt.UnitTests.csproj
        ├── SkipOnWindowsCIBuildAttribute.cs
        ├── StartupCertificateLoaderTests.cs
        ├── TermsOfServiceCheckerTests.cs
        ├── TestUtils.cs
        └── X509CertStoreTests.cs
/.editorconfig:
--------------------------------------------------------------------------------
  1 | root = true
  2 | 
  3 | [*]
  4 | indent_style             = space
  5 | trim_trailing_whitespace = true
  6 | insert_final_newline     = true
  7 | 
  8 | [*.{xml,csproj,props,targets,config}]
  9 | indent_size = 2
 10 | 
 11 | [*.json]
 12 | indent_size = 2
 13 | 
 14 | [*.cs]
 15 | indent_size                                                            = 4
 16 | 
 17 | # Style I care about
 18 | csharp_style_expression_bodied_constructors                            = false
 19 | csharp_prefer_braces                                                   = true
 20 | dotnet_sort_system_directives_first                                    = true
 21 | csharp_using_directive_placement                                       = outside_namespace
 22 | 
 23 | # Stuff that is usually best
 24 | csharp_style_inlined_variable_declaration                              = true
 25 | csharp_style_var_elsewhere                                             = true
 26 | csharp_space_after_cast                                                = false
 27 | csharp_style_pattern_matching_over_as_with_null_check                  = true
 28 | csharp_style_pattern_matching_over_is_with_cast_check                  = true
 29 | csharp_style_var_for_built_in_types                                    = true
 30 | csharp_style_var_when_type_is_apparent                                 = true
 31 | csharp_new_line_before_catch                                           = true
 32 | csharp_new_line_before_else                                            = true
 33 | csharp_new_line_before_finally                                         = true
 34 | csharp_indent_case_contents                                            = true
 35 | csharp_new_line_before_open_brace                                      = all
 36 | csharp_indent_switch_labels                                            = true
 37 | csharp_indent_labels                                                   = one_less_than_current
 38 | csharp_prefer_simple_default_expression                                = true
 39 | csharp_preserve_single_line_blocks                                     = true
 40 | csharp_preserve_single_line_statements                                 = true
 41 | 
 42 | # Good defaults, but not always
 43 | dotnet_style_object_initializer                                        = true
 44 | csharp_style_expression_bodied_indexers                                = true
 45 | csharp_style_expression_bodied_accessors                               = true
 46 | csharp_style_throw_expression                                          = true
 47 | csharp_style_namespace_declarations                                    = file_scoped:suggestion
 48 | 
 49 | # Default severity for analyzer diagnostics with category 'Style' (escalated to build warnings)
 50 | # dotnet_analyzer_diagnostic.category-Style.severity                     = suggestion
 51 | 
 52 | # Required naming style
 53 | dotnet_diagnostic.IDE0006.severity = error
 54 | 
 55 | # suppress warning aboud unused methods
 56 | dotnet_diagnostic.IDE0051.severity = none
 57 | 
 58 | # Missing required header
 59 | dotnet_diagnostic.IDE0040.severity = error
 60 | 
 61 | # Missing accessibility modifier
 62 | dotnet_diagnostic.IDE0073.severity = warning
 63 | 
 64 | # Remove unnecessary parenthesis
 65 | dotnet_diagnostic.IDE0047.severity = warning
 66 | 
 67 | # Parenthesis added for clarity
 68 | dotnet_diagnostic.IDE0048.severity = warning
 69 | 
 70 | # Suppress explicit type instead of var
 71 | dotnet_diagnostic.IDE0008.severity = none
 72 | 
 73 | # Suppress unused expression
 74 | dotnet_diagnostic.IDE0058.severity = none
 75 | 
 76 | 
 77 | 
 78 | # Naming styles
 79 | 
 80 | ## Constants are PascalCase
 81 | dotnet_naming_style.pascal_case.capitalization                         = pascal_case
 82 | 
 83 | dotnet_naming_symbols.constants.applicable_kinds                       = field, property
 84 | dotnet_naming_symbols.constants.applicable_accessibilities             = *
 85 | dotnet_naming_symbols.constants.required_modifiers                     = const
 86 | 
 87 | dotnet_naming_rule.constants_should_be_pascal_case.symbols             = constants
 88 | dotnet_naming_rule.constants_should_be_pascal_case.style               = pascal_case
 89 | dotnet_naming_rule.constants_should_be_pascal_case.severity            = error
 90 | 
 91 | ## Private static fields start with s_
 92 | dotnet_naming_style.s_underscore_camel_case.required_prefix            = s_
 93 | dotnet_naming_style.s_underscore_camel_case.capitalization             = camel_case
 94 | 
 95 | dotnet_naming_symbols.private_static_fields.applicable_kinds           = field
 96 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
 97 | dotnet_naming_symbols.private_static_fields.required_modifiers         = static
 98 | 
 99 | dotnet_naming_rule.private_static_fields_should_be_underscore.symbols  = private_static_fields
100 | dotnet_naming_rule.private_static_fields_should_be_underscore.style    = s_underscore_camel_case
101 | dotnet_naming_rule.private_static_fields_should_be_underscore.severity = suggestion
102 | 
103 | ## Private fields are _camelCase
104 | dotnet_naming_style.underscore_camel_case.required_prefix              = _
105 | dotnet_naming_style.underscore_camel_case.capitalization               = camel_case
106 | 
107 | dotnet_naming_symbols.private_fields.applicable_kinds                  = field
108 | dotnet_naming_symbols.private_fields.applicable_accessibilities        = private
109 | 
110 | dotnet_naming_rule.private_fields_should_be_underscore.symbols         = private_fields
111 | dotnet_naming_rule.private_fields_should_be_underscore.style           = underscore_camel_case
112 | dotnet_naming_rule.private_fields_should_be_underscore.severity        = error
113 | 
114 | # File header
115 | file_header_template = Copyright (c) Nate McMaster.\nLicensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
116 | 
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @natemcmaster
2 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug report
 3 | about: Create a report to help us improve
 4 | title: ''
 5 | labels: bug
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 | 
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Using this version of the library '...'
16 | 2. Run this code '....'
17 | 3. With these arguments '....'
18 | 4. See error
19 | 
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 | 
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 | 
26 | **Additional context**
27 | Add any other context about the problem here.
28 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature_request.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature request
 3 | about: Suggest an idea for this project
 4 | title: ''
 5 | labels: enhancement
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is.
12 | Example. I'm am trying to do [...] but [...]
13 | 
14 | **Describe the solution you'd like**
15 | A clear and concise description of what you want to happen.
16 | 
17 | **Describe alternatives you've considered**
18 | A description of any alternative solutions or features you've considered.
19 | 
20 | **Additional context**
21 | Add any other context or screenshots about the feature request here.
22 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Question.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Question
 3 | about: Ask a question
 4 | title: "[Question] "
 5 | labels: question
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | If you're not exactly sure what to say, here are some suggestions: https://stackoverflow.com/help/how-to-ask
11 | 
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | 
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
  1 | name: CI
  2 | 
  3 | on:
  4 |   push:
  5 |     branches-ignore:
  6 |       - dependabot/*
  7 |   pull_request:
  8 |   workflow_dispatch:
  9 |     inputs:
 10 |       is_stable_build:
 11 |         description: Use a version number indicating this is a stable release
 12 |         required: true
 13 |         default: "false"
 14 |       release:
 15 |         description: Create a release
 16 |         required: true
 17 |         default: "false"
 18 | 
 19 | env:
 20 |   IS_STABLE_BUILD: ${{ github.event.inputs.is_stable_build }}
 21 |   BUILD_NUMBER: ${{ github.run_number }}
 22 | 
 23 | jobs:
 24 |   build:
 25 |     if: ${{ !contains(github.event.head_commit.message, 'ci skip') || github.event_name == 'workflow_dispatch' }}
 26 |     strategy:
 27 |       fail-fast: false
 28 |       matrix:
 29 |         os: [windows-latest, ubuntu-latest, macos-latest]
 30 | 
 31 |     runs-on: ${{ matrix.os }}
 32 | 
 33 |     outputs:
 34 |       package_version: ${{ steps.build_script.outputs.package_version }}
 35 | 
 36 |     steps:
 37 |       - uses: actions/checkout@v4
 38 |       - name: Setup .NET
 39 |         uses: actions/setup-dotnet@v4
 40 |         with:
 41 |           dotnet-version: |
 42 |             6.0.x
 43 |             7.0.x
 44 |       - name: Run build script
 45 |         id: build_script
 46 |         run: ./build.ps1 -ci
 47 |       - uses: actions/upload-artifact@v4
 48 |         if: ${{ matrix.os == 'windows-latest' }}
 49 |         with:
 50 |           name: packages
 51 |           path: artifacts/
 52 |           if-no-files-found: error
 53 |       - uses: codecov/codecov-action@v4
 54 |         with:
 55 |           name: unittests-${{ matrix.os }}
 56 |           fail_ci_if_error: true
 57 |           token: ${{ secrets.CODECOV_TOKEN }}
 58 |   release:
 59 |     if: ${{ github.event.inputs.release }}
 60 |     needs: build
 61 |     runs-on: windows-latest
 62 |     env:
 63 |       PACKAGE_VERSION: ${{ needs.build.outputs.package_version }}
 64 |     steps:
 65 |       - run: echo "Releasing ${{ env.PACKAGE_VERSION }}"
 66 |       - name: Setup NuGet
 67 |         uses: NuGet/setup-nuget@v2
 68 |         with:
 69 |           nuget-version: latest
 70 |       - uses: actions/download-artifact@v4
 71 |         with:
 72 |           name: packages
 73 |           path: packages
 74 |       - name: Configure GitHub NuGet registry
 75 |         run: nuget sources add -name github -source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json -username ${{ github.repository_owner }} -password ${{ secrets.GITHUB_TOKEN }}
 76 |       - name: Push to GitHub package registry
 77 |         run: nuget push packages\*.nupkg -ApiKey ${{ secrets.GITHUB_TOKEN }} -Source github -SkipDuplicate
 78 |       - name: Push to NuGet.org
 79 |         run: nuget push packages\*.nupkg -ApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json -SkipDuplicate
 80 |       - name: Create GitHub release
 81 |         uses: softprops/action-gh-release@v2
 82 |         env:
 83 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 84 |         with:
 85 |           name: ${{ env.PACKAGE_VERSION }}
 86 |           tag_name: v${{ env.PACKAGE_VERSION }}
 87 |           generate_release_notes: true
 88 |           append_body: true
 89 |           body: |
 90 |             ### How to get this update
 91 | 
 92 |             Packages have been posted to these feeds:
 93 | 
 94 |             #### NuGet.org
 95 |             https://nuget.org/packages/LettuceEncrypt/${{ env.PACKAGE_VERSION }}
 96 |             https://nuget.org/packages/LettuceEncrypt.Azure/${{ env.PACKAGE_VERSION }}
 97 | 
 98 |             #### GitHub Package Registry
 99 |             https://github.com/natemcmaster?tab=packages&repo_name=LettuceEncrypt
100 | 
101 |           draft: true
102 |           prerelease: ${{ env.IS_STABLE_BUILD == 'false' }} # Example: v3.1.0-beta
103 |           files: packages/*
104 | 
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
 1 | name: 'Close stale issues and PRs'
 2 | on:
 3 |   workflow_dispatch:
 4 |     inputs:
 5 |       debug-only:
 6 |         description: Run in debug mode
 7 |         required: false
 8 |         default: 'false'
 9 |   schedule:
10 |     - cron: '30 1 * * *'
11 | 
12 | jobs:
13 |   stale:
14 |     runs-on: ubuntu-latest
15 |     steps:
16 |       - uses: actions/stale@v5
17 |         with:
18 |           debug-only: ${{ github.event.inputs.debug-only == 'true' }}
19 |           days-before-stale: 365
20 |           days-before-close: 14
21 |           stale-issue-label: stale
22 |           close-issue-label: closed-stale
23 |           close-issue-reason: not_planned
24 |           exempt-issue-labels: announcement,planning
25 |           exempt-all-milestones: true
26 |           exempt-all-assignees: true
27 |           stale-issue-message: >
28 |             This issue has been automatically marked as stale because it has no recent activity. 
29 |             It will be closed if no further activity occurs. Please comment if you believe this 
30 |             should remain open, otherwise it will be closed in 14 days.
31 |             Thank you for your contributions to this project.
32 |           close-issue-message: >
33 |             Closing due to inactivity.
34 | 
35 |             If you are looking at this issue in the future and think it should be reopened, 
36 |             please make a commented here and mention natemcmaster so he sees the notification.
37 |           stale-pr-message: >
38 |             This pull request appears to be stale. Please comment if you believe this should remain 
39 |             open and reviewed. If there are no updates, it will be closed in 14 days.
40 |           close-pr-message: >
41 |              Thank you for your contributions to this project. This pull request has been closed due to inactivity.
42 | 
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | obj/
 3 | bin/
 4 | .vs/
 5 | .build/
 6 | artifacts/
 7 | *.user
 8 | TestResults/
 9 | .idea/
10 | *.iml
11 | 
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "version": "0.2.0",
 3 |   "configurations": [
 4 |     {
 5 |       "name": ".NET Core Launch (web)",
 6 |       "type": "coreclr",
 7 |       "request": "launch",
 8 |       "preLaunchTask": "build",
 9 |       "program": "${workspaceFolder}/.build/Web/bin/Debug/net7.0/Web.dll",
10 |       "args": [],
11 |       "cwd": "${workspaceFolder}/samples/Web",
12 |       "stopAtEntry": false,
13 |       "internalConsoleOptions": "openOnSessionStart",
14 |       "launchBrowser": {
15 |         "enabled": false,
16 |         "args": "${auto-detect-url}",
17 |         "windows": {
18 |           "command": "cmd.exe",
19 |           "args": "/C start ${auto-detect-url}"
20 |         },
21 |         "osx": {
22 |           "command": "open"
23 |         },
24 |         "linux": {
25 |           "command": "xdg-open"
26 |         }
27 |       },
28 |       "env": {
29 |         "ASPNETCORE_ENVIRONMENT": "Staging",
30 |         "ASPNETCORE_URLS": "http://localhost:5001;https://+:5002"
31 |       },
32 |       "sourceFileMap": {
33 |         "/Views": "${workspaceFolder}/Views"
34 |       }
35 |     },
36 |     {
37 |       "name": ".NET Core Attach",
38 |       "type": "coreclr",
39 |       "request": "attach",
40 |       "processId": "${command:pickProcess}"
41 |     }
42 |   ]
43 | }
44 | 
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "version": "2.0.0",
 3 |   "tasks": [
 4 |     {
 5 |       "label": "build",
 6 |       "command": "dotnet",
 7 |       "type": "process",
 8 |       "args": [
 9 |         "build",
10 |         "--no-restore"
11 |       ],
12 |       "options": {
13 |         "cwd": "${workspaceFolder}"
14 |       },
15 |       "problemMatcher": "$msCompile",
16 |       "presentation": {
17 |         "echo": true,
18 |         "reveal": "always",
19 |         "focus": false,
20 |         "panel": "shared",
21 |         "showReuseMessage": true,
22 |         "clear": true
23 |       },
24 |       "group": {
25 |         "kind": "build",
26 |         "isDefault": true
27 |       }
28 |     }
29 |   ]
30 | }
31 | 
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | Contributing Guide
 2 | ==================
 3 | 
 4 | Contributions are welcome! If you would like to help out, here are some suggestions for how to get involved.
 5 | 
 6 | ## Get involved
 7 | [Watch][watchers] this repository to get notifications about all conversations. GitHub issues and pull requests are the authoritative
 8 | source of truth for design reviews, release schedules, and bug fixes.
 9 | 
10 | ## You don't have to contribute code
11 | 
12 | There are more ways to help that don't involve writing code.
13 | 
14 | * Respond to new issues. Users often open an issue to ask a question. You are welcome to offer your answer on the thread.
15 | * :+1: Up vote features that you think are important.
16 | * Look through issues labeled [closed-stale][closed-stale] to see if there are feature requests worth reviving.
17 | * Review pull requests.
18 | 
19 | ## Contributing code
20 | 
21 | * Open issues labeled ["help wanted"][help-wanted] are issues that I think are worth doing, but no one has volunteered to do the work yet.
22 |   Make a comment on issues you want assigned to yourself.
23 | * Pull requests are more likely to be accepted if I have first agreed to accept a feature or bug fix. Open an issue first if you aren't sure.
24 | 
25 | ## Questions?
26 | 
27 | Open a GitHub issue if you'd like to help and don't know where to begin.
28 | 
29 | [watchers]: https://github.com/natemcmaster/LettuceEncrypt/watchers
30 | [closed-stale]: https://github.com/natemcmaster/LettuceEncrypt/labels/closed-stale
31 | [help-wanted]: https://github.com/natemcmaster/LettuceEncrypt/labels/help%20wanted
32 | 
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
 1 | 
 2 |   
 3 |     lets-encrypt;aspnetcore;https;certificates
 4 |     Nate McMaster
 5 |     Copyright © Nate McMaster
 6 |     en-US
 7 |     false
 8 |     Apache-2.0
 9 |     https://github.com/natemcmaster/LettuceEncrypt
10 |     https://github.com/natemcmaster/LettuceEncrypt
11 |     git
12 |     snupkg
13 |     portable
14 |     false
15 |     false
16 |     true
17 |     true
18 |     10.0
19 |     enable
20 |     true
21 |     $(MSBuildThisFileDirectory)src\StrongName.snk
22 |     true
23 |     $(NoWarn);NU5105
24 |     $(WarningsNotAsErrors);CS1591
25 |     false
26 | 
27 |     true
28 |     true
29 |     true
30 |     $(MSBuildThisFileDirectory).build\$(MSBuildProjectName)\bin\
31 |     $(MSBuildThisFileDirectory).build\$(MSBuildProjectName)\obj\
32 |     
33 |     $(DefaultItemExcludes);TestResults\**
34 |   
35 | 
36 |   
37 |     1.3.3
38 |     beta
39 |     true
40 |     $([MSBuild]::ValueOrDefault($(BUILD_NUMBER), 0))
41 |     $(VersionSuffix).$(BuildNumber)
42 |     $(BUILD_SOURCEVERSION)
43 |     $(VersionPrefix)
44 |     $(PackageVersion)-$(VersionSuffix)
45 | 
46 |     
47 |       See release notes at https://github.com/natemcmaster/LettuceEncrypt/releases/tag/v$(PackageVersion).
48 |     
49 |   
50 | 
51 |   
52 |     icon.png
53 |     https://raw.githubusercontent.com/wiki/natemcmaster/LettuceEncrypt/logo.png
54 |   
55 | 
56 |   
57 |     
58 |       true
59 |       /$(PackageIcon)
60 |       false
61 |     
62 |   
63 | 
64 |   
66 | 
67 | 
--------------------------------------------------------------------------------
/Directory.Build.targets:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |   
 6 | 
 7 |   
 8 |     $(PackageDescription)
 9 | 
10 | This package was build from source code at $(RepositoryUrl)/tree/$(SourceRevisionId)
11 |     
12 |   
13 | 
14 | 
15 | 
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security Policy
 2 | 
 3 | ## Supported Versions
 4 | 
 5 | It is highly recommended that you use the newest versions of this library as they are released.
 6 | Only the latest version of this library will be supported with security updates.
 7 | 
 8 | ## Reporting a Vulnerability
 9 | 
10 | If you have found an issue that you believe is a security vulnerability, DO NOT OPEN an issue on GitHub. Instead,
11 | email your concerns to open-source@natemcmaster.com. Security concerns will be reviewed as soon as possible and
12 | you can expect a reply within two weeks.
13 | 
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env pwsh
 2 | [CmdletBinding(PositionalBinding = $false)]
 3 | param(
 4 |     [ValidateSet('Debug', 'Release')]
 5 |     $Configuration = $null,
 6 |     [switch]
 7 |     $ci,
 8 |     [Parameter(ValueFromRemainingArguments = $true)]
 9 |     [string[]]$MSBuildArgs
10 | )
11 | 
12 | Set-StrictMode -Version 1
13 | $ErrorActionPreference = 'Stop'
14 | 
15 | Import-Module -Force -Scope Local "$PSScriptRoot/src/common.psm1"
16 | 
17 | #
18 | # Main
19 | #
20 | 
21 | if ($env:CI -eq 'true') {
22 |     $ci = $true
23 | }
24 | 
25 | if (!$Configuration) {
26 |     $Configuration = if ($ci) { 'Release' } else { 'Debug' }
27 | }
28 | 
29 | if ($ci) {
30 |     $MSBuildArgs += '-p:CI=true'
31 | 
32 |     & dotnet --info
33 | }
34 | 
35 | if (-not (Test-Path variable:\IsCoreCLR)) {
36 |     $IsWindows = $true
37 | }
38 | 
39 | $artifacts = "$PSScriptRoot/artifacts/"
40 | 
41 | Remove-Item -Recurse $artifacts -ErrorAction Ignore
42 | 
43 | [string[]] $formatArgs=@()
44 | if ($ci) {
45 |     $formatArgs += '--verify-no-changes'
46 | }
47 | 
48 | exec dotnet format -v detailed @formatArgs
49 | exec dotnet build --configuration $Configuration '-warnaserror:CS1591' @MSBuildArgs
50 | exec dotnet pack --no-restore --no-build --configuration $Configuration -o $artifacts @MSBuildArgs
51 | 
52 | [string[]] $testArgs=@()
53 | if ($env:TF_BUILD) {
54 |     $testArgs += '--logger', 'trx'
55 | }
56 | 
57 | exec dotnet test --no-restore --no-build --configuration $Configuration '-clp:Summary' `
58 |     --collect:"XPlat Code Coverage" `
59 |     @testArgs `
60 |     @MSBuildArgs
61 | 
62 | write-host -f green 'BUILD SUCCEEDED'
63 | 
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 |   - samples
3 |   - test
4 | 
--------------------------------------------------------------------------------
/samples/KeyVault/KeyVault.csproj:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     net6.0
 5 |   
 6 | 
 7 |   
 8 |     
 9 |     
10 |   
11 | 
12 | 
--------------------------------------------------------------------------------
/samples/KeyVault/Program.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace Web;
 5 | 
 6 | public class Program
 7 | {
 8 |     public static void Main(string[] args)
 9 |     {
10 |         CreateHostBuilder(args).Build().Run();
11 |     }
12 | 
13 |     public static IHostBuilder CreateHostBuilder(string[] args) =>
14 |         Host.CreateDefaultBuilder(args)
15 |             .ConfigureWebHostDefaults(webBuilder =>
16 |             {
17 |                 webBuilder.UseStartup();
18 |             });
19 | }
20 | 
--------------------------------------------------------------------------------
/samples/KeyVault/Properties/launchSettings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "profiles": {
 3 |     "Development": {
 4 |       "commandName": "Project",
 5 |       "launchBrowser": true,
 6 |       "environmentVariables": {
 7 |         "ASPNETCORE_ENVIRONMENT": "Development"
 8 |       },
 9 |       "applicationUrl": "http://localhost:5001;https://localhost:5002"
10 |     },
11 |     "Staging": {
12 |       "commandName": "Project",
13 |       "launchBrowser": true,
14 |       "environmentVariables": {
15 |         "ASPNETCORE_ENVIRONMENT": "Staging"
16 |       },
17 |       "applicationUrl": "http://localhost:5001;https://localhost:5002"
18 |     }
19 |   }
20 | }
21 | 
--------------------------------------------------------------------------------
/samples/KeyVault/Startup.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace Web;
 5 | 
 6 | public class Startup
 7 | {
 8 |     public void ConfigureServices(IServiceCollection services)
 9 |     {
10 |         services
11 |             .AddLettuceEncrypt()
12 |             .PersistCertificatesToAzureKeyVault();
13 |     }
14 | 
15 |     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
16 |     {
17 |         if (env.IsDevelopment())
18 |         {
19 |             app.UseDeveloperExceptionPage();
20 |         }
21 | 
22 |         app.UseRouting();
23 | 
24 |         app.UseEndpoints(endpoints =>
25 |         {
26 |             endpoints.MapGet("/", async context =>
27 |             {
28 |                 await context.Response.WriteAsync("Hello World!");
29 |             });
30 |         });
31 |     }
32 | }
33 | 
--------------------------------------------------------------------------------
/samples/KeyVault/appsettings.Development.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "LettuceEncrypt": {
 3 |     // Uncomment to test in the dev environment
 4 |     // See CONTRIBUTING.md for instructions
 5 | 
 6 |     //     "DomainNames": [
 7 |     //       "XYZ.ngrok.io"
 8 |     //     ],
 9 |     //
10 |     //     "AzureKeyVault": {
11 |     //        "AzureKeyVaultEndpoint": "https://XYZ.vault.azure.net"
12 |     //     }
13 |   }
14 | }
15 | 
--------------------------------------------------------------------------------
/samples/KeyVault/appsettings.Production.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "LettuceEncrypt": {
 3 |     "DomainNames": [
 4 |       "example.com",
 5 |       "www.example.com",
 6 |       "www2.example.com"
 7 |     ],
 8 |     "AzureKeyVault": {
 9 |       "AzureKeyVaultEndpoint": "https://my-production-secrets.vault.azure.net"
10 |     }
11 |   }
12 | }
13 | 
--------------------------------------------------------------------------------
/samples/KeyVault/appsettings.Staging.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "LettuceEncrypt": {
 3 |     "DomainNames": [
 4 |       "staging.example.com"
 5 |     ],
 6 |     "AzureKeyVault": {
 7 |       "AzureKeyVaultEndpoint": "https://my-staging-secrets.vault.azure.net"
 8 |     }
 9 |   }
10 | }
11 | 
--------------------------------------------------------------------------------
/samples/KeyVault/appsettings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Logging": {
 3 |     "LogLevel": {
 4 |       "Default": "Information",
 5 |       "LettuceEncrypt": "Trace",
 6 |       "Microsoft": "Warning",
 7 |       "Microsoft.Hosting.Lifetime": "Information"
 8 |     }
 9 |   },
10 |   "AllowedHosts": "*",
11 |   "LettuceEncrypt": {
12 |     "AcceptTermsOfService": true,
13 |     "EmailAddress": "admin@example.com"
14 |   }
15 | }
16 | 
--------------------------------------------------------------------------------
/samples/Web/Program.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.Net;
 5 | using Microsoft.AspNetCore.Server.Kestrel.Https;
 6 | 
 7 | namespace Web;
 8 | 
 9 | public class Program
10 | {
11 |     public static void Main(string[] args)
12 |     {
13 |         CreateHostBuilder(args).Build().Run();
14 |     }
15 | 
16 |     public static IHostBuilder CreateHostBuilder(string[] args) =>
17 |         Host.CreateDefaultBuilder(args)
18 |             .ConfigureWebHostDefaults(webBuilder =>
19 |             {
20 |                 webBuilder.UseStartup();
21 | 
22 |                 // This example shows how to configure Kestrel's client certificate requirements along with
23 |                 // enabling Lettuce Encrypt's certificate automation.
24 |                 if (Environment.GetEnvironmentVariable("REQUIRE_CLIENT_CERT") == "true")
25 |                 {
26 |                     webBuilder.UseKestrel(k =>
27 |                     {
28 |                         var appServices = k.ApplicationServices;
29 |                         k.ConfigureHttpsDefaults(h =>
30 |                         {
31 |                             h.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
32 |                             h.UseLettuceEncrypt(appServices);
33 |                         });
34 |                     });
35 |                 }
36 | 
37 |                 // This example shows how to configure Kestrel's address/port binding along with
38 |                 // enabling Lettuce Encrypt's certificate automation.
39 |                 if (Environment.GetEnvironmentVariable("CONFIG_KESTREL_VIA_CODE") == "true")
40 |                 {
41 |                     webBuilder.PreferHostingUrls(false);
42 |                     webBuilder.UseKestrel(k =>
43 |                     {
44 |                         var appServices = k.ApplicationServices;
45 |                         k.Listen(IPAddress.Any, 443,
46 |                             o =>
47 |                                 o.UseHttps(h => h.UseLettuceEncrypt(appServices)));
48 |                     });
49 |                 }
50 |             });
51 | }
52 | 
--------------------------------------------------------------------------------
/samples/Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "profiles": {
 3 |     "Development": {
 4 |       "commandName": "Project",
 5 |       "launchBrowser": true,
 6 |       "environmentVariables": {
 7 |         "ASPNETCORE_ENVIRONMENT": "Development"
 8 |       },
 9 |       "applicationUrl": "http://localhost:5001"
10 |     },
11 |     "Staging": {
12 |       "commandName": "Project",
13 |       "launchBrowser": true,
14 |       "environmentVariables": {
15 |         "ASPNETCORE_ENVIRONMENT": "Staging"
16 |       },
17 |       "applicationUrl": "http://localhost:5001;https://localhost:5002"
18 |     }
19 |   }
20 | }
21 | 
--------------------------------------------------------------------------------
/samples/Web/Startup.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace Web;
 5 | 
 6 | public class Startup
 7 | {
 8 |     public void ConfigureServices(IServiceCollection services)
 9 |     {
10 |         services.AddLettuceEncrypt();
11 |     }
12 | 
13 |     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
14 |     {
15 |         if (env.IsDevelopment())
16 |         {
17 |             app.UseDeveloperExceptionPage();
18 |         }
19 | 
20 |         app.UseHttpsRedirection();
21 | 
22 |         app.UseRouting();
23 | 
24 |         app.UseEndpoints(endpoints =>
25 |         {
26 |             endpoints.MapGet("/", async context =>
27 |             {
28 |                 await context.Response.WriteAsync("Hello World!");
29 |             });
30 |         });
31 |     }
32 | }
33 | 
--------------------------------------------------------------------------------
/samples/Web/Web.csproj:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     net6.0
 5 |   
 6 | 
 7 |   
 8 |     
 9 |   
10 | 
11 | 
--------------------------------------------------------------------------------
/samples/Web/appsettings.Development.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "LettuceEncrypt": {
 3 |     // Uncomment to test in the dev environment
 4 |     // See CONTRIBUTING.md for instructions
 5 | 
 6 |     // "DomainNames": [
 7 |     //   "56638cf5.ngrok.io"
 8 |     // ]
 9 |   }
10 | }
11 | 
--------------------------------------------------------------------------------
/samples/Web/appsettings.Production.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "LettuceEncrypt": {
 3 |     "DomainNames": [
 4 |       "example.com",
 5 |       "www.example.com",
 6 |       "www2.example.com"
 7 |     ]
 8 |   }
 9 | }
10 | 
--------------------------------------------------------------------------------
/samples/Web/appsettings.Staging.json:
--------------------------------------------------------------------------------
1 | {
2 |   "LettuceEncrypt": {
3 |     "DomainNames": [
4 |       "staging.example.com"
5 |     ],
6 |     "UseStagingServer": true
7 |   }
8 | }
9 | 
--------------------------------------------------------------------------------
/samples/Web/appsettings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Logging": {
 3 |     "LogLevel": {
 4 |       "Default": "Information",
 5 |       "LettuceEncrypt": "Trace",
 6 |       "Microsoft": "Warning",
 7 |       "Microsoft.Hosting.Lifetime": "Information"
 8 |     }
 9 |   },
10 |   "AllowedHosts": "*",
11 |   "LettuceEncrypt": {
12 |     "AcceptTermsOfService": true,
13 |     "EmailAddress": "admin@example.com"
14 |   }
15 | }
16 | 
--------------------------------------------------------------------------------
/src/Directory.Build.targets:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 | 
 6 |     
 7 |     
 8 |     
 9 |   
10 | 
11 |   
12 | 
13 | 
14 | 
--------------------------------------------------------------------------------
/src/Kestrel.Certificates/IServerCertificateSelector.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.Security.Cryptography.X509Certificates;
 5 | using Microsoft.AspNetCore.Connections;
 6 | 
 7 | namespace McMaster.AspNetCore.Kestrel.Certificates;
 8 | 
 9 | /// 
10 | /// Selects a certificate for incoming TLS connections.
11 | /// 
12 | public interface IServerCertificateSelector
13 | {
14 |     /// 
15 |     /// 
16 |     /// A callback that will be invoked to dynamically select a server certificate.
17 |     /// If SNI is not available, then the domainName parameter will be null.
18 |     /// 
19 |     /// 
20 |     /// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
21 |     /// 
22 |     /// 
23 |     X509Certificate2? Select(ConnectionContext context, string? domainName);
24 | }
25 | 
--------------------------------------------------------------------------------
/src/Kestrel.Certificates/KestrelHttpsOptionsExtensions.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using McMaster.AspNetCore.Kestrel.Certificates;
 5 | using Microsoft.AspNetCore.Server.Kestrel.Https;
 6 | 
 7 | // ReSharper disable once CheckNamespace
 8 | namespace Microsoft.AspNetCore.Hosting;
 9 | 
10 | /// 
11 | /// API for configuring Kestrel certificate options
12 | /// 
13 | public static class KestrelHttpsOptionsExtensions
14 | {
15 |     /// 
16 |     /// Configure HTTPS certificates dynamically with an implementation of .
17 |     /// 
18 |     /// The HTTPS configuration
19 |     /// The server certificate selector.
20 |     /// The HTTPS configuration
21 |     public static HttpsConnectionAdapterOptions UseServerCertificateSelector(
22 |         this HttpsConnectionAdapterOptions httpsOptions,
23 |         IServerCertificateSelector certificateSelector)
24 |     {
25 |         var fallbackSelector = httpsOptions.ServerCertificateSelector;
26 |         httpsOptions.ServerCertificateSelector = (connectionContext, domainName) =>
27 |         {
28 |             var primaryCert = certificateSelector.Select(connectionContext!, domainName);
29 |             // fallback to the original selector if the injected selector fails to find a certificate.
30 |             return primaryCert ?? fallbackSelector?.Invoke(connectionContext, domainName);
31 |         };
32 | 
33 |         return httpsOptions;
34 |     }
35 | }
36 | 
--------------------------------------------------------------------------------
/src/Kestrel.Certificates/McMaster.AspNetCore.Kestrel.Certificates.csproj:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     net6.0
 5 |     true
 6 |     enable
 7 |     true
 8 |     A class library for managing HTTPS certificates with ASP.NET Core.
 9 | 
10 | This library includes API for dynamically selecting which HTTPS certificate to use in Kestrel.
11 |     1.0.0
12 |     $(VersionPrefix)
13 |     $(PackageVersion)-$(VersionSuffix)
14 |   
15 | 
16 |   
17 |     
18 |   
19 | 
20 | 
21 | 
22 | 
--------------------------------------------------------------------------------
/src/Kestrel.Certificates/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | McMaster.AspNetCore.Kestrel.Certificates.IServerCertificateSelector
3 | McMaster.AspNetCore.Kestrel.Certificates.IServerCertificateSelector.Select(Microsoft.AspNetCore.Connections.ConnectionContext! context, string? domainName) -> System.Security.Cryptography.X509Certificates.X509Certificate2?
4 | Microsoft.AspNetCore.Hosting.KestrelHttpsOptionsExtensions
5 | static Microsoft.AspNetCore.Hosting.KestrelHttpsOptionsExtensions.UseServerCertificateSelector(this Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions! httpsOptions, McMaster.AspNetCore.Kestrel.Certificates.IServerCertificateSelector! certificateSelector) -> Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions!
6 | 
--------------------------------------------------------------------------------
/src/Kestrel.Certificates/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | 
--------------------------------------------------------------------------------
/src/Kestrel.Certificates/releasenotes.props:
--------------------------------------------------------------------------------
 1 | 
 2 |   
 3 |     
 4 | First stable release!
 5 |     
 6 | 
 7 |     $(PackageReleaseNotes.Trim())
 8 |   
 9 | 
10 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/AzureKeyVaultLettuceEncryptOptions.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.ComponentModel.DataAnnotations;
 5 | using Azure.Core;
 6 | using Azure.Identity;
 7 | using LettuceEncrypt.Accounts;
 8 | 
 9 | namespace LettuceEncrypt.Azure;
10 | 
11 | /// 
12 | /// Options to connect to an Azure KeyVault
13 | /// 
14 | public class AzureKeyVaultLettuceEncryptOptions
15 | {
16 |     /// 
17 |     /// Gets or sets the Url for the KeyVault instance.
18 |     /// 
19 |     [Url]
20 |     [Required]
21 |     public string AzureKeyVaultEndpoint { get; set; } = null!;
22 | 
23 |     /// 
24 |     /// Gets or sets the credentials used for connecting to the key vault. If null, will use .
25 |     /// 
26 |     public TokenCredential? Credentials { get; set; }
27 | 
28 |     /// 
29 |     /// Gets or sets the name the secret used to store the account information for accessing the certificate authority.
30 |     /// This is a JSON string which encodes the information in .
31 |     /// If not set, the name defaults to the name of the "le-account-${ACME server hostname}".
32 |     /// 
33 |     [MaxLength(127)]
34 |     public string? AccountKeySecretName { get; set; }
35 | }
36 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/AzureLettuceEncryptExtensions.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using LettuceEncrypt;
 5 | using LettuceEncrypt.Accounts;
 6 | using LettuceEncrypt.Azure;
 7 | using LettuceEncrypt.Azure.Internal;
 8 | using Microsoft.Extensions.Configuration;
 9 | using Microsoft.Extensions.DependencyInjection.Extensions;
10 | using Microsoft.Extensions.Options;
11 | 
12 | // ReSharper disable once CheckNamespace
13 | namespace Microsoft.Extensions.DependencyInjection;
14 | 
15 | /// 
16 | /// Extensions to integrate Azure with LettuceEncrypt.
17 | /// 
18 | public static class AzureLettuceEncryptExtensions
19 | {
20 |     /// 
21 |     /// Persists certificates to configured key vault.
22 |     /// 
23 |     /// A LettuceEncrypt service builder.
24 |     /// The original LettuceEncrypt service builder.
25 |     public static ILettuceEncryptServiceBuilder PersistCertificatesToAzureKeyVault(
26 |         this ILettuceEncryptServiceBuilder builder)
27 |         => builder.PersistCertificatesToAzureKeyVault(_ => { });
28 | 
29 |     /// 
30 |     /// Persists certificates to configured key vault.
31 |     /// 
32 |     /// A LettuceEncrypt service builder.
33 |     /// Configuration for KeyVault connections.
34 |     /// The original LettuceEncrypt service builder.
35 |     public static ILettuceEncryptServiceBuilder PersistCertificatesToAzureKeyVault(
36 |         this ILettuceEncryptServiceBuilder builder,
37 |         Action configure)
38 |     {
39 |         var services = builder.Services;
40 |         services
41 |             .AddSingleton()
42 |             .AddSingleton();
43 | 
44 |         services.TryAddSingleton();
45 |         services.TryAddSingleton();
46 |         services.TryAddEnumerable(
47 |             ServiceDescriptor.Singleton(x =>
48 |                 x.GetRequiredService()));
49 |         services.TryAddEnumerable(
50 |             ServiceDescriptor.Singleton(x =>
51 |                 x.GetRequiredService()));
52 | 
53 |         services.AddSingleton>(s =>
54 |         {
55 |             var config = s.GetService();
56 |             return new ConfigureOptions(o =>
57 |                 config?.Bind("LettuceEncrypt:AzureKeyVault", o));
58 |         });
59 | 
60 |         services
61 |             .AddOptions()
62 |             .Configure(configure)
63 |             .ValidateDataAnnotations();
64 | 
65 |         return builder;
66 |     }
67 | }
68 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/Internal/AzureKeyVaultAccountStore.cs:
--------------------------------------------------------------------------------
  1 | // Copyright (c) Nate McMaster.
  2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3 | 
  4 | using System.Text.Json;
  5 | using Azure;
  6 | using LettuceEncrypt.Accounts;
  7 | using LettuceEncrypt.Acme;
  8 | using Microsoft.Extensions.Logging;
  9 | using Microsoft.Extensions.Options;
 10 | 
 11 | namespace LettuceEncrypt.Azure.Internal;
 12 | 
 13 | internal class AzureKeyVaultAccountStore : IAccountStore
 14 | {
 15 |     private readonly ILogger _logger;
 16 |     private readonly IOptions _options;
 17 |     private readonly ICertificateAuthorityConfiguration _certificateAuthority;
 18 |     private readonly ISecretClientFactory _secretClientFactory;
 19 | 
 20 |     public AzureKeyVaultAccountStore(
 21 |         ILogger logger,
 22 |         IOptions options,
 23 |         ISecretClientFactory secretClientFactory,
 24 |         ICertificateAuthorityConfiguration certificateAuthority)
 25 |     {
 26 |         _logger = logger;
 27 |         _options = options;
 28 |         _secretClientFactory = secretClientFactory;
 29 |         _certificateAuthority = certificateAuthority;
 30 |     }
 31 | 
 32 |     public async Task SaveAccountAsync(AccountModel account, CancellationToken cancellationToken)
 33 |     {
 34 |         var secretName = GetSecretName();
 35 |         _logger.LogTrace("Saving account information to Azure Key Vault as {secretName}", secretName);
 36 |         var secretValue = JsonSerializer.Serialize(account);
 37 |         try
 38 |         {
 39 |             var secretClient = _secretClientFactory.Create();
 40 | 
 41 |             await secretClient.SetSecretAsync(secretName, secretValue, cancellationToken);
 42 |             _logger.LogInformation("Saved account information to Azure Key Vault as {secretName}", secretName);
 43 |         }
 44 |         catch (Exception ex)
 45 |         {
 46 |             _logger.LogError(ex, "Failed to save account information to Azure Key Vault as {secretName}",
 47 |                 secretName);
 48 |             throw;
 49 |         }
 50 |     }
 51 | 
 52 |     public async Task GetAccountAsync(CancellationToken cancellationToken)
 53 |     {
 54 |         var secretName = GetSecretName();
 55 | 
 56 |         _logger.LogTrace("Searching account information to Azure Key Vault in secret {secretName}", secretName);
 57 | 
 58 |         try
 59 |         {
 60 |             var secretClient = _secretClientFactory.Create();
 61 | 
 62 |             var secret = await secretClient.GetSecretAsync(secretName, version: null, cancellationToken);
 63 | 
 64 |             _logger.LogInformation("Found account key in {secretName}, version {version}",
 65 |                 secret.Value.Name,
 66 |                 secret.Value.Properties.Version);
 67 | 
 68 |             return JsonSerializer.Deserialize(secret.Value.Value);
 69 |         }
 70 |         catch (RequestFailedException ex) when (ex.Status == 404)
 71 |         {
 72 |             _logger.LogInformation("Could not find account information in secret '{secretName}' in Azure Key Vault",
 73 |                 secretName);
 74 |             return null;
 75 |         }
 76 |         catch (Exception ex)
 77 |         {
 78 |             _logger.LogError(ex, "Failed to fetch secret '{secretName}' from Azure Key Vault", secretName);
 79 |             throw;
 80 |         }
 81 |     }
 82 | 
 83 |     private string GetSecretName()
 84 |     {
 85 |         const int MaxLength = 127;
 86 |         string name;
 87 | 
 88 |         var options = _options.Value;
 89 |         if (!string.IsNullOrEmpty(options.AccountKeySecretName))
 90 |         {
 91 |             name = options.AccountKeySecretName!;
 92 |         }
 93 |         else
 94 |         {
 95 |             name = _certificateAuthority.AcmeDirectoryUri.Host;
 96 |         }
 97 | 
 98 |         name = "le-account-" + name.Replace(".", "-");
 99 |         return name.Length > MaxLength
100 |             ? name.Substring(0, MaxLength)
101 |             : name;
102 |     }
103 | }
104 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/Internal/CertificateClientFactory.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Azure.Identity;
 5 | using Azure.Security.KeyVault.Certificates;
 6 | using Microsoft.Extensions.Options;
 7 | 
 8 | namespace LettuceEncrypt.Azure.Internal;
 9 | 
10 | internal interface ICertificateClientFactory
11 | {
12 |     CertificateClient Create();
13 | }
14 | 
15 | internal class CertificateClientFactory : ICertificateClientFactory
16 | {
17 |     private readonly IOptions _options;
18 | 
19 |     public CertificateClientFactory(IOptions options)
20 |     {
21 |         _options = options ?? throw new ArgumentNullException(nameof(options));
22 |     }
23 | 
24 |     public CertificateClient Create()
25 |     {
26 |         var value = _options.Value;
27 | 
28 |         if (string.IsNullOrEmpty(value.AzureKeyVaultEndpoint))
29 |         {
30 |             throw new ArgumentException("Missing required option: AzureKeyVaultEndpoint");
31 |         }
32 | 
33 |         var vaultUri = new Uri(value.AzureKeyVaultEndpoint);
34 |         var credentials = value.Credentials ?? new DefaultAzureCredential();
35 | 
36 |         return new CertificateClient(vaultUri, credentials);
37 |     }
38 | }
39 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/Internal/SecretClientFactory.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Azure.Identity;
 5 | using Azure.Security.KeyVault.Secrets;
 6 | using Microsoft.Extensions.Options;
 7 | 
 8 | namespace LettuceEncrypt.Azure.Internal;
 9 | 
10 | internal interface ISecretClientFactory
11 | {
12 |     SecretClient Create();
13 | }
14 | 
15 | internal class SecretClientFactory : ISecretClientFactory
16 | {
17 |     private readonly IOptions _options;
18 | 
19 |     public SecretClientFactory(IOptions options)
20 |     {
21 |         _options = options ?? throw new ArgumentNullException(nameof(options));
22 |     }
23 | 
24 |     public SecretClient Create()
25 |     {
26 |         var value = _options.Value;
27 | 
28 |         if (string.IsNullOrEmpty(value.AzureKeyVaultEndpoint))
29 |         {
30 |             throw new ArgumentException("Missing required option: AzureKeyVaultEndpoint");
31 |         }
32 | 
33 |         var vaultUri = new Uri(value.AzureKeyVaultEndpoint);
34 |         var credentials = value.Credentials ?? new DefaultAzureCredential();
35 | 
36 |         return new SecretClient(vaultUri, credentials);
37 |     }
38 | }
39 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/LettuceEncrypt.Azure.csproj:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     net6.0
 5 |     true
 6 |     enable
 7 |     true
 8 |     Provides API for configuring ASP.NET Core to automatically generate HTTPS certificates and store them in Azure Key Vault.
 9 |     $(Description)
10 | 
11 | See https://nuget.org/packages/LettuceEncrypt for more details.
12 |     
13 |   
14 | 
15 |   
16 |     
17 |     
18 |     
19 |   
20 | 
21 |   
22 |     
23 |   
24 | 
25 | 
26 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Nate McMaster.
2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3 | 
4 | using System.Runtime.CompilerServices;
5 | 
6 | [assembly: InternalsVisibleTo("LettuceEncrypt.Azure.UnitTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001df0eba4297c8ffdf114a13714ad787744619dfb18e29191703f6f782d6a09e4a4cac35b8c768cbbd9ade8197bc0f66ec66fabc9071a206c8060af8b7a332236968d3ee44b90bd2f30d0edcb6150555c6f8d988e48234debaf2d427a08d7c06ba1343411142dc8ac996f7f7dbe0e93d13f17a7624db5400510e6144b0fd683b9")]
7 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
8 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
 1 | #nullable enable
 2 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions
 3 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions.AzureKeyVaultLettuceEncryptOptions() -> void
 4 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions.Credentials.get -> Azure.Core.TokenCredential?
 5 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions.Credentials.set -> void
 6 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions.AzureKeyVaultEndpoint.get -> string!
 7 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions.AzureKeyVaultEndpoint.set -> void
 8 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions.AccountKeySecretName.get -> string?
 9 | LettuceEncrypt.Azure.AzureKeyVaultLettuceEncryptOptions.AccountKeySecretName.set -> void
10 | Microsoft.Extensions.DependencyInjection.AzureLettuceEncryptExtensions
11 | static Microsoft.Extensions.DependencyInjection.AzureLettuceEncryptExtensions.PersistCertificatesToAzureKeyVault(this LettuceEncrypt.ILettuceEncryptServiceBuilder! builder) -> LettuceEncrypt.ILettuceEncryptServiceBuilder!
12 | static Microsoft.Extensions.DependencyInjection.AzureLettuceEncryptExtensions.PersistCertificatesToAzureKeyVault(this LettuceEncrypt.ILettuceEncryptServiceBuilder! builder, System.Action! configure) -> LettuceEncrypt.ILettuceEncryptServiceBuilder!
13 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt.Azure/releasenotes.props:
--------------------------------------------------------------------------------
 1 | 
 2 |   
 3 |     
 4 | Update to latest Azure dependencies. See also the release notes for the main LettuceEncrypt package.
 5 |     
 6 |     
 7 |     
 8 |     
 9 | First release! This is basically the same as McMaster.AspNetCore.LetsEncrypt.Azure 0.5.0, but has been renamed.
10 | 
11 | See https://github.com/natemcmaster/LettuceEncrypt/#Usage for instructions.
12 | 
13 | Changes since 0.5.0:
14 | 
15 | * Support configuring Azure Key Vault settings from appsettings.json under the 'LettuceEncrypt:AzureKeyVault' section.
16 | * The private key for accessing the account is now stored in the Azure Key Vault as well under a secret
17 |   named 'le-account-${acmeserver}'. This ensures certificates can be renewed later using the same account.
18 |     
19 | 
20 |     $(PackageReleaseNotes.Trim())
21 |   
22 | 
23 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Accounts/AccountModel.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.Text.Json.Serialization;
 5 | using Certes;
 6 | 
 7 | namespace LettuceEncrypt.Accounts;
 8 | 
 9 | /// 
10 | /// Represents an account with the certificate authority.
11 | /// 
12 | public class AccountModel
13 | {
14 |     private byte[] _privateKey = Array.Empty();
15 | 
16 |     /// 
17 |     /// A unique identifier.
18 |     /// 
19 |     public int Id { get; set; }
20 | 
21 |     /// 
22 |     /// A list of email addresses associated with the account.
23 |     /// At least one should be specified.
24 |     /// 
25 |     public string[] EmailAddresses { get; set; } = Array.Empty();
26 | 
27 |     /// 
28 |     /// The private key for the account.
29 |     /// This should be DER encoded key content.
30 |     /// 
31 |     public byte[] PrivateKey
32 |     {
33 |         get => _privateKey;
34 |         set
35 |         {
36 |             _privateKey = value;
37 |             Key = KeyFactory.FromDer(value);
38 |         }
39 |     }
40 | 
41 |     [JsonIgnore] internal IKey? Key { get; private set; }
42 | }
43 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Accounts/IAccountStore.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace LettuceEncrypt.Accounts;
 5 | 
 6 | /// 
 7 | /// Manages persistence and retrieval of account information.
 8 | /// 
 9 | public interface IAccountStore
10 | {
11 |     /// 
12 |     /// Save account information for reuse later after a server restart.
13 |     /// 
14 |     /// All information in this model should round trip with .
15 |     /// 
16 |     /// 
17 |     Task SaveAccountAsync(AccountModel account, CancellationToken cancellationToken);
18 | 
19 |     /// 
20 |     /// Fetch account information.
21 |     /// 
22 |     /// 
23 |     /// Should return null if no account could be found.
24 |     Task GetAccountAsync(CancellationToken cancellationToken);
25 | }
26 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Acme/ChallengeType.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace LettuceEncrypt.Acme;
 5 | 
 6 | /// 
 7 | /// An enumeration that represents the various kinds of challenges in the ACME protocol.
 8 | /// See https://letsencrypt.org/docs/challenge-types/.
 9 | /// 
10 | [Flags]
11 | public enum ChallengeType
12 | {
13 |     /// 
14 |     /// The HTTP-01 challenge, which uses a well-known URL on the server and a HTTP request/response.
15 |     /// See https://letsencrypt.org/docs/challenge-types/#http-01-challenge
16 |     /// 
17 |     Http01 = 1 << 0,
18 | 
19 |     /// 
20 |     /// The TLS-ALPN-01 challenge, which uses an auto-generated, ephemeral certificate in the TLS handshake.
21 |     /// See https://letsencrypt.org/docs/challenge-types/#tls-alpn-01
22 |     /// 
23 |     TlsAlpn01 = 1 << 1,
24 | 
25 |     /// 
26 |     /// The DNS-01 challenge, which uses TXT record under that domain name.
27 |     /// See https://letsencrypt.org/docs/challenge-types/#dns-01-challenge
28 |     /// 
29 |     Dns01 = 1 << 2,
30 | 
31 |     /// 
32 |     /// A special flag which represents all known challenge types.
33 |     /// 
34 |     Any = 0xFFFF,
35 | }
36 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Acme/DnsTxtRecordContext.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace LettuceEncrypt.Acme;
 5 | 
 6 | /// 
 7 | /// Default txt record context
 8 | /// 
 9 | public class DnsTxtRecordContext
10 | {
11 |     /// 
12 |     /// default constructor
13 |     /// 
14 |     /// Domain name for the txt record
15 |     /// TXT record Value
16 |     public DnsTxtRecordContext(string domainName, string txt)
17 |     {
18 |         DomainName = domainName;
19 |         Txt = txt;
20 |     }
21 | 
22 |     /// 
23 |     /// Domain name for the txt record
24 |     /// 
25 |     public string DomainName { get; }
26 |     /// 
27 |     /// TXT record Value
28 |     /// 
29 |     public string Txt { get; }
30 | }
31 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Acme/EabCredentials.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace LettuceEncrypt.Acme;
 5 | 
 6 | /// 
 7 | /// External Account Binding (EAB) account credentials
 8 | /// 
 9 | public class EabCredentials
10 | {
11 |     /// 
12 |     /// Optional key identifier for external account binding
13 |     /// 
14 |     public string? EabKeyId { get; set; }
15 | 
16 |     /// 
17 |     /// Optional key for use with external account binding
18 |     /// 
19 |     public string? EabKey { get; set; }
20 | 
21 |     /// 
22 |     /// Optional key algorithm e.g HS256, for external account binding
23 |     /// 
24 |     public string? EabKeyAlg { get; set; }
25 | }
26 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Acme/ICertificateAuthorityConfiguration.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace LettuceEncrypt.Acme;
 5 | 
 6 | /// 
 7 | /// Provides configuration for the certificate authority which implements the ACME protocol.
 8 | /// 
 9 | public interface ICertificateAuthorityConfiguration
10 | {
11 |     /// 
12 |     /// The base uri of the ACME protocol.
13 |     /// 
14 |     Uri AcmeDirectoryUri { get; }
15 | 
16 |     /// 
17 |     /// Certificates passed to certes before building the successfully downloaded certificate,
18 |     /// used internally by certes to verify the issuer for authenticity.
19 |     /// 
20 |     /// 
21 |     /// Lettuce encrypt uses certes internally, while certes depends on BouncyCastle.Cryptography to parse
22 |     /// certificates. See https://github.com/bcgit/bc-csharp/blob/830d9b8c7bdfcec511bff0a6cf4a0e8ed568e7c1/crypto/src/x509/X509CertificateParser.cs#L20
23 |     /// if you're wondering what certificate formats are supported.
24 |     /// 
25 |     /// 
26 |     string[] IssuerCertificates => Array.Empty();
27 | }
28 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Acme/IDnsChallengeProvider.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace LettuceEncrypt.Acme;
 5 | 
 6 | /// 
 7 | /// External Dns provider to update for DNS-01 challenge
 8 | /// 
 9 | public interface IDnsChallengeProvider
10 | {
11 |     /// 
12 |     /// call to add record in advance of the validation
13 |     /// 
14 |     /// domain name including _acme-challenge.	<YOUR_DOMAIN>
15 |     /// TXT value for DNS-01 Challenge
16 |     /// A cancellation token.
17 |     /// context of added txt record
18 |     Task AddTxtRecordAsync(string domainName, string txt, CancellationToken ct = default);
19 | 
20 |     /// 
21 |     /// callback to remove dns record after validations
22 |     /// 
23 |     /// context from previous added txt record
24 |     /// A cancellation token.
25 |     /// 
26 |     Task RemoveTxtRecordAsync(DnsTxtRecordContext context, CancellationToken ct = default);
27 | }
28 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/FileSystemPersistenceExtensions.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using LettuceEncrypt.Accounts;
 5 | using LettuceEncrypt.Acme;
 6 | using LettuceEncrypt.Internal;
 7 | using Microsoft.Extensions.DependencyInjection;
 8 | using Microsoft.Extensions.DependencyInjection.Extensions;
 9 | using Microsoft.Extensions.Logging;
10 | 
11 | namespace LettuceEncrypt;
12 | 
13 | /// 
14 | /// Extensions for configuring certificate persistence
15 | /// 
16 | public static class FileSystemStorageExtensions
17 | {
18 |     /// 
19 |     /// Save certificates and account data to a directory.
20 |     /// Certificates are stored in the .pfx (PKCS #12) format in a subdirectory of .
21 |     /// Account key information is stored in a JSON format in a different subdirectory of .
22 |     /// 
23 |     /// 
24 |     /// The root directory for storing information. Information may be stored in subdirectories.
25 |     /// Set to null or empty for password-less .pfx files.
26 |     /// 
27 |     public static ILettuceEncryptServiceBuilder PersistDataToDirectory(
28 |         this ILettuceEncryptServiceBuilder builder,
29 |         DirectoryInfo directory,
30 |         string? pfxPassword)
31 |     {
32 |         if (builder is null)
33 |         {
34 |             throw new ArgumentNullException(nameof(builder));
35 |         }
36 | 
37 |         if (directory is null)
38 |         {
39 |             throw new ArgumentNullException(nameof(directory));
40 |         }
41 | 
42 |         var otherFileSystemRepoServices = builder
43 |             .Services
44 |             .Where(d => d.ServiceType == typeof(ICertificateRepository)
45 |             && d.ImplementationInstance != null
46 |             && d.ImplementationInstance.GetType() == typeof(FileSystemCertificateRepository));
47 | 
48 |         foreach (var serviceDescriptor in otherFileSystemRepoServices)
49 |         {
50 |             var otherRepo = (FileSystemCertificateRepository)serviceDescriptor.ImplementationInstance!;
51 |             if (otherRepo.RootDir.Equals(directory))
52 |             {
53 |                 if (otherRepo.PfxPassword != pfxPassword)
54 |                 {
55 |                     throw new ArgumentException($"Another file system repo has been configured for {directory}, but with a different password.");
56 |                 }
57 |                 return builder;
58 |             }
59 |         }
60 | 
61 |         var implementationInstance = new FileSystemCertificateRepository(directory, pfxPassword);
62 |         builder.Services
63 |             .AddSingleton(implementationInstance)
64 |             .AddSingleton(implementationInstance);
65 | 
66 |         builder.Services.TryAddSingleton(services => new FileSystemAccountStore(directory,
67 |                 services.GetRequiredService>(),
68 |                 services.GetRequiredService()));
69 | 
70 |         return builder;
71 |     }
72 | }
73 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/ICertificateRepository.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.Security.Cryptography.X509Certificates;
 5 | 
 6 | namespace LettuceEncrypt;
 7 | 
 8 | /// 
 9 | /// Manages certificate persistence after it is generated.
10 | /// 
11 | public interface ICertificateRepository
12 | {
13 |     /// 
14 |     /// Save the certificate.
15 |     /// 
16 |     /// The certificate, including its private keys.
17 |     /// A token which, when canceled, should stop any async operations.
18 |     /// A task which completes once the certificate is done saving.
19 |     Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellationToken);
20 | }
21 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/ICertificateSource.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.Security.Cryptography.X509Certificates;
 5 | 
 6 | namespace LettuceEncrypt;
 7 | 
 8 | /// 
 9 | /// Defines a source for certificates.
10 | /// 
11 | public interface ICertificateSource
12 | {
13 |     /// 
14 |     /// Gets available certificates from the source.
15 |     /// 
16 |     /// A cancellation token.
17 |     /// A collection of certificates.
18 |     Task> GetCertificatesAsync(CancellationToken cancellationToken);
19 | }
20 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/ILettuceEncryptServiceBuilder.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Microsoft.Extensions.DependencyInjection;
 5 | 
 6 | namespace LettuceEncrypt;
 7 | 
 8 | /// 
 9 | /// An interface for building extension methods to extend LettuceEncrypt configuration.
10 | /// 
11 | public interface ILettuceEncryptServiceBuilder
12 | {
13 |     /// 
14 |     /// The service collection.
15 |     /// 
16 |     IServiceCollection Services { get; }
17 | }
18 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeCertificateLoader.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using LettuceEncrypt.Internal.AcmeStates;
 5 | using Microsoft.AspNetCore.Hosting.Server;
 6 | using Microsoft.AspNetCore.Server.Kestrel.Core;
 7 | using Microsoft.Extensions.Configuration;
 8 | using Microsoft.Extensions.DependencyInjection;
 9 | using Microsoft.Extensions.Hosting;
10 | using Microsoft.Extensions.Logging;
11 | using Microsoft.Extensions.Options;
12 | 
13 | namespace LettuceEncrypt.Internal;
14 | 
15 | /// 
16 | /// This starts the ACME state machine, which handles certificate generation and renewal
17 | /// 
18 | internal class AcmeCertificateLoader : BackgroundService
19 | {
20 |     private readonly IServiceScopeFactory _serviceScopeFactory;
21 |     private readonly IOptions _options;
22 |     private readonly ILogger _logger;
23 | 
24 |     private readonly IServer _server;
25 |     private readonly IConfiguration _config;
26 | 
27 |     public AcmeCertificateLoader(
28 |         IServiceScopeFactory serviceScopeFactory,
29 |         IOptions options,
30 |         ILogger logger,
31 |         IServer server,
32 |         IConfiguration config)
33 |     {
34 |         _serviceScopeFactory = serviceScopeFactory;
35 |         _options = options;
36 |         _logger = logger;
37 |         _server = server;
38 |         _config = config;
39 |     }
40 | 
41 |     protected override async Task ExecuteAsync(CancellationToken stoppingToken)
42 |     {
43 |         if (!_server.GetType().Name.StartsWith(nameof(KestrelServer)))
44 |         {
45 |             var serverType = _server.GetType().FullName;
46 |             _logger.LogWarning(
47 |                 "LettuceEncrypt can only be used with Kestrel and is not supported on {serverType} servers. Skipping certificate provisioning.",
48 |                 serverType);
49 |             return;
50 |         }
51 | 
52 |         if (_config.GetValue("UseIISIntegration"))
53 |         {
54 |             _logger.LogWarning(
55 |                 "LettuceEncrypt does not work with apps hosting in IIS. IIS does not allow for dynamic HTTPS certificate binding." +
56 |                 "Skipping certificate provisioning.");
57 |             return;
58 |         }
59 | 
60 |         // load certificates in the background
61 |         if (!LettuceEncryptDomainNamesWereConfigured())
62 |         {
63 |             _logger.LogInformation("No domain names were configured");
64 |             return;
65 |         }
66 | 
67 |         using var acmeStateMachineScope = _serviceScopeFactory.CreateScope();
68 | 
69 |         try
70 |         {
71 |             IAcmeState state = acmeStateMachineScope.ServiceProvider.GetRequiredService();
72 | 
73 |             while (!stoppingToken.IsCancellationRequested)
74 |             {
75 |                 _logger.LogTrace("ACME state transition: moving to {stateName}", state.GetType().Name);
76 |                 state = await state.MoveNextAsync(stoppingToken);
77 |             }
78 |         }
79 |         catch (OperationCanceledException)
80 |         {
81 |             _logger.LogDebug("State machine cancellation requested. Exiting...");
82 |         }
83 |         catch (AggregateException ex) when (ex.InnerException != null)
84 |         {
85 |             _logger.LogError(0, ex.InnerException, "ACME state machine encountered unhandled error");
86 |         }
87 |         catch (Exception ex)
88 |         {
89 |             _logger.LogError(0, ex, "ACME state machine encountered unhandled error");
90 |         }
91 |     }
92 | 
93 |     private bool LettuceEncryptDomainNamesWereConfigured()
94 |         => _options.Value.DomainNames
95 |             .Any(w => !string.Equals("localhost", w, StringComparison.OrdinalIgnoreCase));
96 | }
97 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeClient.cs:
--------------------------------------------------------------------------------
  1 | // Copyright (c) Nate McMaster.
  2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3 | 
  4 | using Certes;
  5 | using Certes.Acme;
  6 | using Certes.Acme.Resource;
  7 | using Microsoft.Extensions.Logging;
  8 | using Microsoft.Extensions.Options;
  9 | 
 10 | namespace LettuceEncrypt.Internal;
 11 | 
 12 | internal class AcmeClient
 13 | {
 14 |     private readonly AcmeContext _context;
 15 |     private readonly ILogger _logger;
 16 |     private readonly IOptions _options;
 17 |     private readonly IKey _acmeAccountKey;
 18 |     private IAccountContext? _accountContext;
 19 | 
 20 |     public AcmeClient(ILogger logger, IOptions options, Uri directoryUri, IKey acmeAccountKey)
 21 |     {
 22 |         _logger = logger;
 23 |         _options = options;
 24 |         _acmeAccountKey = acmeAccountKey;
 25 |         _logger.LogInformation("Using certificate authority {directoryUri}", directoryUri);
 26 |         _context = new AcmeContext(directoryUri, acmeAccountKey);
 27 |     }
 28 | 
 29 |     public async Task GetAccountAsync()
 30 |     {
 31 |         _logger.LogAcmeAction("FetchAccount");
 32 |         _accountContext = await _context.Account();
 33 |         _logger.LogAcmeAction("FetchAccountDetails", _accountContext);
 34 |         return await _accountContext.Resource();
 35 |     }
 36 | 
 37 |     public IKey? GetAccountKey()
 38 |     {
 39 |         _logger.LogAcmeAction("GetAccountKey");
 40 |         return _acmeAccountKey;
 41 |     }
 42 | 
 43 |     public async Task CreateAccountAsync(string emailAddress)
 44 |     {
 45 |         _logger.LogAcmeAction("NewAccount");
 46 |         var eabCredentials = _options.Value.EabCredentials;
 47 |         _accountContext = await _context.NewAccount(emailAddress, termsOfServiceAgreed: true, eabKeyId: eabCredentials.EabKeyId, eabKey: eabCredentials.EabKey, eabKeyAlg: eabCredentials.EabKeyAlg);
 48 | 
 49 |         return int.TryParse(_accountContext.Location.Segments.Last(), out var accountId)
 50 |             ? accountId
 51 |             : 0;
 52 |     }
 53 | 
 54 |     public async Task GetTermsOfServiceAsync()
 55 |     {
 56 |         _logger.LogAcmeAction("FetchTOS");
 57 |         return await _context.TermsOfService();
 58 |     }
 59 | 
 60 |     public async Task AgreeToTermsOfServiceAsync()
 61 |     {
 62 |         if (_accountContext == null)
 63 |         {
 64 |             throw MissingAccountContext();
 65 |         }
 66 |         _logger.LogAcmeAction("UpdateTOS");
 67 |         await _accountContext.Update(agreeTermsOfService: true);
 68 |     }
 69 | 
 70 |     public async Task> GetOrdersAsync()
 71 |     {
 72 |         if (_accountContext == null)
 73 |         {
 74 |             throw MissingAccountContext();
 75 |         }
 76 | 
 77 |         _logger.LogAcmeAction("FetchOrderList");
 78 |         var orderListContext = await _accountContext.Orders();
 79 | 
 80 |         if (orderListContext == null)
 81 |         {
 82 |             return Enumerable.Empty();
 83 |         }
 84 |         _logger.LogAcmeAction("FetchOrderDetails", orderListContext);
 85 |         return await orderListContext.Orders();
 86 |     }
 87 | 
 88 |     public async Task CreateOrderAsync(string[] domainNames)
 89 |     {
 90 |         _logger.LogAcmeAction("NewOrder");
 91 |         return await _context.NewOrder(domainNames);
 92 |     }
 93 | 
 94 |     public async Task GetOrderDetailsAsync(IOrderContext order)
 95 |     {
 96 |         _logger.LogAcmeAction("FetchOrderDetails", order);
 97 |         return await order.Resource();
 98 |     }
 99 | 
100 |     public async Task> GetOrderAuthorizations(IOrderContext orderContext)
101 |     {
102 |         _logger.LogAcmeAction("FetchAuthorizations", orderContext);
103 |         return await orderContext.Authorizations();
104 |     }
105 | 
106 |     public async Task GetAuthorizationAsync(IAuthorizationContext authorizationContext)
107 |     {
108 |         _logger.LogAcmeAction("FetchAuthorizationDetails", authorizationContext);
109 |         return await authorizationContext.Resource();
110 |     }
111 | 
112 |     public async Task CreateChallengeAsync(IAuthorizationContext authorizationContext, string challengeType)
113 |     {
114 |         _logger.LogAcmeAction("CreateChallenge", authorizationContext);
115 |         return await authorizationContext.Challenge(challengeType);
116 |     }
117 | 
118 |     public async Task ValidateChallengeAsync(IChallengeContext httpChallenge)
119 |     {
120 |         _logger.LogAcmeAction("ValidateChallenge", httpChallenge);
121 |         return await httpChallenge.Validate();
122 |     }
123 | 
124 |     public async Task GetCertificateAsync(CsrInfo csrInfo, IKey privateKey, IOrderContext order)
125 |     {
126 |         _logger.LogAcmeAction("GenerateCertificate", order);
127 |         return await order.Generate(csrInfo, privateKey);
128 |     }
129 | 
130 |     private static Exception MissingAccountContext() => new InvalidOperationException("Account wasn't initialized yet");
131 | }
132 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeClientFactory.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Certes;
 5 | using LettuceEncrypt.Acme;
 6 | using Microsoft.Extensions.Logging;
 7 | using Microsoft.Extensions.Options;
 8 | 
 9 | namespace LettuceEncrypt.Internal;
10 | 
11 | internal class AcmeClientFactory
12 | {
13 |     private readonly ICertificateAuthorityConfiguration _certificateAuthority;
14 |     private readonly ILogger _logger;
15 |     private readonly IOptions _options;
16 | 
17 |     public AcmeClientFactory(
18 |         ICertificateAuthorityConfiguration certificateAuthority,
19 |         ILogger logger,
20 |         IOptions options)
21 |     {
22 |         _certificateAuthority = certificateAuthority;
23 |         _logger = logger;
24 |         _options = options;
25 |     }
26 | 
27 |     public AcmeClient Create(IKey acmeAccountKey)
28 |     {
29 |         var directoryUri = _certificateAuthority.AcmeDirectoryUri;
30 | 
31 |         return new AcmeClient(_logger, _options, directoryUri, acmeAccountKey);
32 |     }
33 | }
34 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeStates/AcmeState.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Microsoft.Extensions.DependencyInjection;
 5 | 
 6 | namespace LettuceEncrypt.Internal.AcmeStates;
 7 | 
 8 | internal interface IAcmeState
 9 | {
10 |     Task MoveNextAsync(CancellationToken cancellationToken);
11 | }
12 | 
13 | internal class TerminalState : IAcmeState
14 | {
15 |     public static TerminalState Singleton { get; } = new();
16 | 
17 |     private TerminalState() { }
18 | 
19 |     public Task MoveNextAsync(CancellationToken cancellationToken)
20 |     {
21 |         throw new OperationCanceledException();
22 |     }
23 | }
24 | 
25 | internal abstract class AcmeState : IAcmeState
26 | {
27 |     private readonly AcmeStateMachineContext _context;
28 | 
29 |     public AcmeState(AcmeStateMachineContext context)
30 |     {
31 |         _context = context;
32 |     }
33 | 
34 |     public abstract Task MoveNextAsync(CancellationToken cancellationToken);
35 | 
36 |     protected T MoveTo() where T : IAcmeState
37 |     {
38 |         return _context.Services.GetRequiredService();
39 |     }
40 | }
41 | 
42 | internal abstract class SyncAcmeState : AcmeState
43 | {
44 |     protected SyncAcmeState(AcmeStateMachineContext context) : base(context)
45 |     {
46 |     }
47 | 
48 |     public override Task MoveNextAsync(CancellationToken cancellationToken)
49 |     {
50 |         cancellationToken.ThrowIfCancellationRequested();
51 | 
52 |         var next = MoveNext();
53 | 
54 |         return Task.FromResult(next);
55 |     }
56 | 
57 |     public abstract IAcmeState MoveNext();
58 | }
59 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeStates/AcmeStateMachineContext.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | namespace LettuceEncrypt.Internal.AcmeStates;
 5 | 
 6 | internal class AcmeStateMachineContext
 7 | {
 8 |     public IServiceProvider Services { get; }
 9 | 
10 |     public AcmeStateMachineContext(IServiceProvider services)
11 |     {
12 |         Services = services;
13 |     }
14 | }
15 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeStates/BeginCertificateCreationState.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.Security.Cryptography.X509Certificates;
 5 | using Microsoft.Extensions.Logging;
 6 | using Microsoft.Extensions.Options;
 7 | 
 8 | namespace LettuceEncrypt.Internal.AcmeStates;
 9 | 
10 | internal class BeginCertificateCreationState : AcmeState
11 | {
12 |     private readonly ILogger _logger;
13 |     private readonly IOptions _options;
14 |     private readonly AcmeCertificateFactory _acmeCertificateFactory;
15 |     private readonly CertificateSelector _selector;
16 |     private readonly IEnumerable _certificateRepositories;
17 | 
18 |     public BeginCertificateCreationState(
19 |         AcmeStateMachineContext context, ILogger logger,
20 |         IOptions options, AcmeCertificateFactory acmeCertificateFactory,
21 |         CertificateSelector selector, IEnumerable certificateRepositories)
22 |         : base(context)
23 |     {
24 |         _logger = logger;
25 |         _options = options;
26 |         _acmeCertificateFactory = acmeCertificateFactory;
27 |         _selector = selector;
28 |         _certificateRepositories = certificateRepositories;
29 |     }
30 | 
31 |     public override async Task MoveNextAsync(CancellationToken cancellationToken)
32 |     {
33 |         var domainNames = _options.Value.DomainNames;
34 | 
35 |         try
36 |         {
37 |             var account = await _acmeCertificateFactory.GetOrCreateAccountAsync(cancellationToken);
38 |             _logger.LogInformation("Using account {accountId}", account.Id);
39 | 
40 |             _logger.LogInformation("Creating certificate for {hostname}",
41 |                 string.Join(",", domainNames));
42 | 
43 |             var cert = await _acmeCertificateFactory.CreateCertificateAsync(cancellationToken);
44 | 
45 |             _logger.LogInformation("Created certificate {subjectName} ({thumbprint})",
46 |                 cert.Subject,
47 |                 cert.Thumbprint);
48 | 
49 |             await SaveCertificateAsync(cert, cancellationToken);
50 |         }
51 |         catch (Exception ex)
52 |         {
53 |             _logger.LogError(0, ex, "Failed to automatically create a certificate for {hostname}", domainNames);
54 |             throw;
55 |         }
56 | 
57 |         return MoveTo();
58 |     }
59 | 
60 |     private async Task SaveCertificateAsync(X509Certificate2 cert, CancellationToken cancellationToken)
61 |     {
62 |         _selector.Add(cert);
63 | 
64 |         var saveTasks = new List
65 |         {
66 |             Task.Delay(TimeSpan.FromMinutes(5), cancellationToken)
67 |         };
68 | 
69 |         var errors = new List();
70 |         foreach (var repo in _certificateRepositories)
71 |         {
72 |             try
73 |             {
74 |                 saveTasks.Add(repo.SaveAsync(cert, cancellationToken));
75 |             }
76 |             catch (Exception ex)
77 |             {
78 |                 // synchronous saves may fail immediately
79 |                 errors.Add(ex);
80 |             }
81 |         }
82 | 
83 |         await Task.WhenAll(saveTasks);
84 | 
85 |         if (errors.Count > 0)
86 |         {
87 |             throw new AggregateException("Failed to save cert to repositories", errors);
88 |         }
89 |     }
90 | }
91 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeStates/CheckForRenewalState.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using LettuceEncrypt.Internal.IO;
 5 | using Microsoft.Extensions.Logging;
 6 | using Microsoft.Extensions.Options;
 7 | 
 8 | namespace LettuceEncrypt.Internal.AcmeStates;
 9 | 
10 | internal class CheckForRenewalState : AcmeState
11 | {
12 |     private readonly ILogger _logger;
13 |     private readonly IOptions _options;
14 |     private readonly CertificateSelector _selector;
15 |     private readonly IClock _clock;
16 | 
17 |     public CheckForRenewalState(
18 |         AcmeStateMachineContext context,
19 |         ILogger logger,
20 |         IOptions options,
21 |         CertificateSelector selector,
22 |         IClock clock) : base(context)
23 |     {
24 |         _logger = logger;
25 |         _options = options;
26 |         _selector = selector;
27 |         _clock = clock;
28 |     }
29 | 
30 |     public override async Task MoveNextAsync(CancellationToken cancellationToken)
31 |     {
32 |         while (!cancellationToken.IsCancellationRequested)
33 |         {
34 |             var checkPeriod = _options.Value.RenewalCheckPeriod;
35 |             var daysInAdvance = _options.Value.RenewDaysInAdvance;
36 |             if (!checkPeriod.HasValue || !daysInAdvance.HasValue)
37 |             {
38 |                 _logger.LogInformation("Automatic certificate renewal is not configured. Stopping {service}",
39 |                     nameof(AcmeCertificateLoader));
40 |                 return MoveTo();
41 |             }
42 | 
43 |             var domainNames = _options.Value.DomainNames;
44 |             if (_logger.IsEnabled(LogLevel.Debug))
45 |             {
46 |                 _logger.LogDebug("Checking certificates' renewals for {hostname}",
47 |                     string.Join(", ", domainNames));
48 |             }
49 | 
50 |             foreach (var domainName in domainNames)
51 |             {
52 |                 if (!_selector.TryGet(domainName, out var cert)
53 |                     || cert == null
54 |                     || cert.NotAfter <= _clock.Now.DateTime + daysInAdvance.Value)
55 |                 {
56 |                     return MoveTo();
57 |                 }
58 |             }
59 | 
60 |             await Task.Delay(checkPeriod.Value, cancellationToken);
61 |         }
62 | 
63 |         return MoveTo();
64 |     }
65 | }
66 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/AcmeStates/ServerStartupState.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Microsoft.Extensions.Logging;
 5 | using Microsoft.Extensions.Options;
 6 | 
 7 | namespace LettuceEncrypt.Internal.AcmeStates;
 8 | 
 9 | internal class ServerStartupState : SyncAcmeState
10 | {
11 |     private readonly IOptions _options;
12 |     private readonly CertificateSelector _selector;
13 |     private readonly ILogger _logger;
14 | 
15 |     public ServerStartupState(
16 |         AcmeStateMachineContext context,
17 |         IOptions options,
18 |         CertificateSelector selector,
19 |         ILogger logger) :
20 |         base(context)
21 |     {
22 |         _options = options;
23 |         _selector = selector;
24 |         _logger = logger;
25 |     }
26 | 
27 |     public override IAcmeState MoveNext()
28 |     {
29 |         var domainNames = _options.Value.DomainNames;
30 |         var hasCertForAllDomains = domainNames.All(_selector.HasCertForDomain);
31 |         if (hasCertForAllDomains)
32 |         {
33 |             _logger.LogDebug("Certificate for {domainNames} already found.", domainNames);
34 |             return MoveTo();
35 |         }
36 | 
37 |         return MoveTo();
38 |     }
39 | }
40 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/DefaultCertificateAuthorityConfiguration.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Certes.Acme;
 5 | using LettuceEncrypt.Acme;
 6 | using Microsoft.Extensions.Hosting;
 7 | using Microsoft.Extensions.Options;
 8 | 
 9 | namespace LettuceEncrypt.Internal;
10 | 
11 | internal class DefaultCertificateAuthorityConfiguration : ICertificateAuthorityConfiguration
12 | {
13 |     private readonly IHostEnvironment _env;
14 |     private readonly IOptions _options;
15 | 
16 |     public DefaultCertificateAuthorityConfiguration(IHostEnvironment env, IOptions options)
17 |     {
18 |         _env = env ?? throw new ArgumentNullException(nameof(env));
19 |         _options = options ?? throw new ArgumentNullException(nameof(options));
20 |     }
21 | 
22 |     public Uri AcmeDirectoryUri
23 |     {
24 |         get
25 |         {
26 |             var options = _options.Value;
27 |             var useStaging = options.UseStagingServerExplicitlySet
28 |                 ? options.UseStagingServer
29 |                 : _env.IsDevelopment();
30 | 
31 |             return useStaging
32 |                 ? WellKnownServers.LetsEncryptStagingV2
33 |                 : WellKnownServers.LetsEncryptV2;
34 |         }
35 |     }
36 | }
37 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/DeveloperCertLoader.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using System.Security.Cryptography.X509Certificates;
 5 | using Microsoft.Extensions.Hosting;
 6 | using Microsoft.Extensions.Logging;
 7 | 
 8 | namespace LettuceEncrypt.Internal;
 9 | 
10 | /// 
11 | /// Loads the ASP.NET Developer certificate
12 | /// /// 
13 | internal class DeveloperCertLoader : ICertificateSource
14 | {
15 |     // see https://github.com/aspnet/Common/blob/61320f4ecc1a7b60e76ca8fe05cd86c98778f92c/shared/Microsoft.AspNetCore.Certificates.Generation.Sources/CertificateManager.cs#L19-L20
16 |     // This is the unique OID for the developer cert generated by VS and the .NET Core CLI
17 |     private const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
18 |     private const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";
19 |     private readonly IHostEnvironment _environment;
20 |     private readonly ILogger _logger;
21 | 
22 |     public DeveloperCertLoader(
23 |         IHostEnvironment environment,
24 |         ILogger logger)
25 |     {
26 |         _environment = environment;
27 |         _logger = logger;
28 |     }
29 | 
30 |     public Task> GetCertificatesAsync(CancellationToken cancellationToken)
31 |     {
32 |         if (!_environment.IsDevelopment())
33 |         {
34 |             return Task.FromResult(Enumerable.Empty());
35 |         }
36 | 
37 |         var certs = FindDeveloperCert();
38 | 
39 |         return Task.FromResult(certs);
40 |     }
41 | 
42 |     private IEnumerable FindDeveloperCert()
43 |     {
44 |         using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
45 |         store.Open(OpenFlags.ReadOnly);
46 |         var certs = store.Certificates.Find(X509FindType.FindByExtension, AspNetHttpsOid, validOnly: false);
47 |         if (certs.Count == 0)
48 |         {
49 |             _logger.LogDebug("Could not find the " + AspNetHttpsOidFriendlyName);
50 |         }
51 |         else
52 |         {
53 |             _logger.LogDebug("Using the " + AspNetHttpsOidFriendlyName + " for 'localhost' requests");
54 | 
55 |             foreach (var cert in certs)
56 |             {
57 |                 yield return cert;
58 |             }
59 |         }
60 |     }
61 | }
62 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/Dns01DomainValidator.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Certes;
 5 | using Certes.Acme;
 6 | using Certes.Acme.Resource;
 7 | using LettuceEncrypt.Acme;
 8 | using Microsoft.Extensions.Hosting;
 9 | using Microsoft.Extensions.Logging;
10 | 
11 | namespace LettuceEncrypt.Internal;
12 | 
13 | internal class Dns01DomainValidator : DomainOwnershipValidator
14 | {
15 |     private readonly IDnsChallengeProvider _dnsChallengeProvider;
16 | 
17 |     public Dns01DomainValidator(
18 |         IDnsChallengeProvider dnsChallengeProvider,
19 |         IHostApplicationLifetime appLifetime,
20 |         AcmeClient client,
21 |         ILogger logger,
22 |         string domainName
23 |     ) : base(appLifetime, client, logger, domainName)
24 |     {
25 |         _dnsChallengeProvider = dnsChallengeProvider;
26 |     }
27 | 
28 |     public override async Task ValidateOwnershipAsync(
29 |         IAuthorizationContext authzContext,
30 |         CancellationToken cancellationToken
31 |     )
32 |     {
33 |         var context = new DnsTxtRecordContext(_domainName, string.Empty);
34 |         try
35 |         {
36 |             context = await PrepareDns01ChallengeResponseAsync(authzContext, _domainName, cancellationToken);
37 |             await WaitForChallengeResultAsync(authzContext, cancellationToken);
38 |         }
39 |         finally
40 |         {
41 |             // Cleanup
42 |             await _dnsChallengeProvider.RemoveTxtRecordAsync(context, cancellationToken);
43 |         }
44 |     }
45 | 
46 |     private async Task PrepareDns01ChallengeResponseAsync(
47 |         IAuthorizationContext authorizationContext,
48 |         string domainName,
49 |         CancellationToken cancellationToken
50 |     )
51 |     {
52 |         cancellationToken.ThrowIfCancellationRequested();
53 | 
54 |         var account = _client.GetAccountKey();
55 |         var dnsChallenge = await _client.CreateChallengeAsync(authorizationContext, ChallengeTypes.Dns01);
56 | 
57 |         var dnsTxt = account.DnsTxt(dnsChallenge.Token);
58 | 
59 |         var acmeDomain = GetAcmeDnsDomain(domainName);
60 | 
61 |         var context = await _dnsChallengeProvider.AddTxtRecordAsync(acmeDomain, dnsTxt, cancellationToken);
62 | 
63 |         _logger.LogTrace("Requesting server to validate DNS challenge");
64 |         await _client.ValidateChallengeAsync(dnsChallenge);
65 | 
66 |         return context;
67 |     }
68 | 
69 |     private const string DnsAcmePrefix = "_acme-challenge";
70 | 
71 |     private string GetAcmeDnsDomain(string domainName) =>
72 |         $"{DnsAcmePrefix}.{domainName.TrimStart('*')}";
73 | }
74 | 
--------------------------------------------------------------------------------
/src/LettuceEncrypt/Internal/DomainOwnershipValidator.cs:
--------------------------------------------------------------------------------
 1 | // Copyright (c) Nate McMaster.
 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 3 | 
 4 | using Certes.Acme;
 5 | using Certes.Acme.Resource;
 6 | using Microsoft.Extensions.Hosting;
 7 | using Microsoft.Extensions.Logging;
 8 | 
 9 | namespace LettuceEncrypt.Internal;
10 | 
11 | internal abstract class DomainOwnershipValidator
12 | {
13 |     protected readonly AcmeClient _client;
14 |     protected readonly ILogger _logger;
15 |     protected readonly string _domainName;
16 |     protected readonly TaskCompletionSource