diff --git a/Posh-ACME/Plugins/AddrTools.ps1 b/Posh-ACME/Plugins/AddrTools.ps1 new file mode 100644 index 00000000..040bf102 --- /dev/null +++ b/Posh-ACME/Plugins/AddrTools.ps1 @@ -0,0 +1,217 @@ +function Get-CurrentPluginType { 'dns-01' } + +function Add-DnsTxt { + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)] + [string]$RecordName, + [Parameter(Mandatory,Position=1)] + [string]$TxtValue, + [Parameter(Mandatory)] + [securestring]$AddrToolsSecret, + [string]$AddrToolsHost='challenges.addr.tools', + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + + $secPlain = [pscredential]::new('a',$AddrToolsSecret).GetNetworkCredential().Password + + $queryParams = @{ + Uri = 'https://{0}' -f $AddrToolsHost + Method = 'POST' + Body = @{ + secret = 'REDACTED' + txt = $TxtValue + } + Verbose = $false + ErrorAction = 'Stop' + } + # log with redacted secret + Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue" + Write-Debug "$($queryParams.Method) $($queryParams.Uri)`n$($queryParams.Body|ConvertTo-Json)" + + try { + $queryParams.Body.secret = $secPlain + $resp = Invoke-RestMethod @queryParams @script:UseBasic + if (-not $resp.Trim() -eq 'OK') { + Write-Warning "Addr.Tools returned: $($resp.Trim())" + } + } catch { throw } + + <# + .SYNOPSIS + Add a DNS TXT record to challenges.addr.tools + + .DESCRIPTION + Description for challenges.addr.tools + + .PARAMETER RecordName + The fully qualified name of the TXT record. + + .PARAMETER AddrToolsSecret + The secret associated with your challenges.addr.tools subdomain. + + .PARAMETER AddrToolsHost + If self-hosting, domain name of your challenges.addr.tools equivalent (e.g. challenges.example.com) + + .PARAMETER TxtValue + The value of the TXT record. + + .PARAMETER ExtraParams + This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. + + .EXAMPLE + Add-DnsTxt '_acme-challenge.example.com' 'txt-value' + + Adds a TXT record for the specified site with the specified value. + #> +} + +function Remove-DnsTxt { + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)] + [string]$RecordName, + [Parameter(Mandatory,Position=1)] + [string]$TxtValue, + [Parameter(Mandatory)] + [securestring]$AddrToolsSecret, + [string]$AddrToolsHost='challenges.addr.tools', + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + + $secPlain = [pscredential]::new('a',$AddrToolsSecret).GetNetworkCredential().Password + + $queryParams = @{ + Uri = 'https://{0}' -f $AddrToolsHost + Method = 'DELETE' + Body = @{ + secret = 'REDACTED' + txt = $TxtValue + } + Verbose = $false + ErrorAction = 'Stop' + } + # log with redacted secret + Write-Verbose "Deleting $RecordName with value $TxtValue" + Write-Debug "$($queryParams.Method) $($queryParams.Uri)`n$($queryParams.Body|ConvertTo-Json)" + + try { + $queryParams.Body.secret = $secPlain + $resp = Invoke-RestMethod @queryParams @script:UseBasic + if (-not $resp.Trim() -eq '') { + Write-Warning "Addr.Tools returned: $($resp.Trim())" + } + } catch { throw } + + <# + .SYNOPSIS + Remove a DNS TXT record from + + .DESCRIPTION + Description for + + .PARAMETER RecordName + The fully qualified name of the TXT record. + + .PARAMETER TxtValue + The value of the TXT record. + + .PARAMETER AddrToolsSecret + The secret associated with your challenges.addr.tools subdomain. + + .PARAMETER AddrToolsHost + If self-hosting, domain name of your challenges.addr.tools equivalent (e.g. challenges.example.com) + + .PARAMETER ExtraParams + This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. + + .EXAMPLE + Remove-DnsTxt '_acme-challenge.example.com' 'txt-value' + + Removes a TXT record for the specified site with the specified value. + #> +} + +function Save-DnsTxt { + [CmdletBinding()] + param( + [Parameter(ValueFromRemainingArguments)] + $ExtraParams + ) + <# + .SYNOPSIS + Not required. + + .DESCRIPTION + This provider does not require calling this function to commit changes to DNS records. + + .PARAMETER ExtraParams + This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports. + #> +} + +############################ +# Helper Functions +############################ + +# https://challenges.addr.tools/ + +function Get-AddrToolsCNAME { + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)] + [string[]]$Domain, + [Parameter(Mandatory,Position=1)] + [securestring]$AddrToolsSecret, + [string]$AddrToolsHost='challenges.addr.tools' + ) + + $challengeSub = Get-AddrToolsSubdomain $AddrToolsSecret -AddrToolsHost $AddrToolsHost + + # Create a unique list of domains after stripping wildcards + $Domain | Select-Object @{ + L='FQDN'; E={ '_acme-challenge.{0}' -f $_.TrimStart('*.') } + },@{ + L='Target';E={ $challengeSub } + } | Select-Object -Unique * +} + +function Get-AddrToolsSubdomain { + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)] + [securestring]$AddrToolsSecret, + [string]$AddrToolsHost='challenges.addr.tools' + ) + + if (-not $script:UseBasic) { + $script:UseBasic = @{UseBasicParsing=$true} + } + + # The subdomain for a give secret is the SHA-224 hash of the secret + # prepended to the challenges FQDN. So by default: + # .challenges.addr.tools + # + # Until we have a local SHA-224 hashing implementation, you can get this + # value by querying the endpoint with just the secret and no other arguments. + + $secPlain = [pscredential]::new('a',$AddrToolsSecret).GetNetworkCredential().Password + + $queryParams = @{ + Uri = 'https://{0}' -f $AddrToolsHost + Method = 'POST' + Body = @{ + secret = $secPlain + } + Verbose = $false + ErrorAction = 'Stop' + } + try { + Write-Debug "POST $($queryParams.Uri)" + $resp = Invoke-RestMethod @queryParams @script:UseBasic + } catch { throw } + + return $resp.Trim().TrimEnd('.') +} diff --git a/Posh-ACME/Private/Import-PluginDetail.ps1 b/Posh-ACME/Private/Import-PluginDetail.ps1 index 4ea5fa11..c39e2af4 100644 --- a/Posh-ACME/Private/Import-PluginDetail.ps1 +++ b/Posh-ACME/Private/Import-PluginDetail.ps1 @@ -13,6 +13,7 @@ function Import-PluginDetail { $script:Plugins = @{ 'AcmeDns' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'AcmeDns'} 'Active24' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'Active24'} + 'AddrTools' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'AddrTools'} 'Akamai' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'Akamai'} 'Aliyun' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'Aliyun'} 'All-Inkl' = [pscustomobject]@{PSTypeName = 'PoshACME.PAPluginDetail'; ChallengeType = 'dns-01'; Path = ''; Name = 'All-Inkl'} diff --git a/docs/Plugins/AddrTools.md b/docs/Plugins/AddrTools.md new file mode 100644 index 00000000..0e607158 --- /dev/null +++ b/docs/Plugins/AddrTools.md @@ -0,0 +1,52 @@ +title: AddrTools + +# How To Use the AddrTools DNS Plugin + +This plugin works against [challenges.addr.tools](https://challenges.addr.tools/) which is a specialized provider purpose built for validating dns-01 challenges using CNAME aliases. It is part of the larger [addr.tools](https://www.addr.tools/) open source project which means you can self-host your own instance. But the default configuration works against the author's public instance. + +## Setup + +It may help to read the [Using DNS Challenge Aliases](../Guides/Using-DNS-Challenge-Aliases.md) guide to better understand how CNAME records work with ACME challenges. Most importantly, there will be a one-time CNAME record creation for each name in the certificate you are requesting. + +There is a helper function in the plugin to help determine which CNAME records to create. It uses the same parameters that the AddrTools plugin uses. So use the following code to create an appropriate `$pArgs` variable and a `$domains` variable that contains the list of domains in your certificate. + +```powershell +$pArgs = @{ + AddrToolsSecret = Read-Host -Prompt "Enter Secret" -AsSecureString + AddrToolsHost = 'challenges.addr.tools' # optional unless self-hosting +} +$domains = 'example.com','www.example.com' +``` + +Now run the following to load and run the helper function that will tell you what CNAME records to create. + +```powershell +Import-Module Posh-ACME +. (Join-Path (Get-Module Posh-ACME).ModuleBase "Plugins\AddrTools.ps1") +Get-AddrToolsCNAME $domains @pArgs +``` + +The output will look similar to this depending on how many unique domains you have: + +``` +FQDN Target +---- ------ +_acme-challenge.example.com 7870508034f01f4a28d86812fa7bd10a03cb7e7e6ddda0ddb95ff771.challenges.addr.tools +_acme-challenge.www.example.com 7870508034f01f4a28d86812fa7bd10a03cb7e7e6ddda0ddb95ff771.challenges.addr.tools +``` + +## Using the Plugin + +Once all necessary CNAME records are created, you can use the same `$pArgs` value from setup with the plugin. `AddrToolsSecret` is always required and `AddrToolsHost` is only required if you're self-hosting your own instance at a different domain. + +You'll also need to use the `-DnsAlias` parameter from New-PACertificate with the Target value from the CNAME records. + +```powershell +# set this to the CNAME Target value from setup +$target = 'xxxxxxxxxxxxxxxxxx.challenges.addr.tools' + +$pArgs = @{ + AddrToolsSecret = (Read-Host 'Access Token' -AsSecureString) +} +New-PACertificate example.com -Plugin AddrTools -PluginArgs $pArgs -DnsAlias $target +``` diff --git a/docs/Plugins/index.md b/docs/Plugins/index.md index 185ba4b1..71c089f1 100644 --- a/docs/Plugins/index.md +++ b/docs/Plugins/index.md @@ -15,6 +15,7 @@ Plugin | Provider | Guide | PS Core Compatible ------ | -------- | ----- | :----------------: AcmeDns | [acme-dns](https://github.com/joohoi/acme-dns) | [Usage Guide](AcmeDns.md) | :white_check_mark: Active24 | [Active24](https://Active24.com) | [Usage Guide](Active24.md) | :white_check_mark: +AddrTools | [addr.tools](https://challenges.addr.tools) | [Usage Guide](AddrTools.md) | :white_check_mark: Akamai | [Akamai Edge DNS](https://www.akamai.com/products/edge-dns) | [Usage Guide](Akamai.md) | :white_check_mark: Aliyun | [Aliyun (Alibaba Cloud)](https://www.alibabacloud.com/product/dns) | [Usage Guide](Aliyun.md) | :white_check_mark: All-Inkl | [All-Inkl](https://all-inkl.com/) | [Usage Guide](All-Inkl.md) | :white_check_mark: