Public/Get-DnsPersistAccountUri.ps1
|
function Get-DnsPersistAccountUri { [CmdletBinding(DefaultParameterSetName='NativeAccount')] param( [Parameter(Mandatory,Position=0,ValueFromPipeline)] [string]$Domain, [Parameter(Position=1,ParameterSetName='NativeAccount')] [PSTypeName('PoshACME.PAAccount')]$Account, [Parameter(Mandatory,Position=1,ParameterSetName='ExplicitAccountDetails')] [string]$AccountUri, [Parameter(Mandatory,Position=2,ParameterSetName='ExplicitAccountDetails')] [string]$KeyThumbprint, [string]$AccountHashPrefix, [ValidateSet('sha-256')] [string]$HashAlgorithm='sha-256', [switch]$NoDomainCorrelationMitigation ) # https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-02.html#section-4.1 Begin { trap { $PSCmdlet.ThrowTerminatingError($_) } # prep the SHA-256 hasher for later use $sha256 = [Security.Cryptography.SHA256]::Create() if ('NativeAccount' -eq $PSCmdlet.ParameterSetName) { # make sure any account passed in is actually associated with the current server # or if no account was specified, that there's a current account. if (-not $Account) { if (-not ($Account = Get-PAAccount)) { throw "No Account parameter specified and no current account selected. Try running Set-PAAccount first." } } elseif ($Account.id -notin (Get-PAAccount -List).id) { throw "Specified account id $($Account.id) was not found in the current server's account list." } # make sure it's valid if ($Account.status -ne 'valid') { throw "Account status is $($Account.status)." } # Use the account thumbprint and location values $thumb = $Account.thumbprint $accountLocation = $Account.location } else { # use the provided account uri and key thumbprint $thumb = $KeyThumbprint $accountLocation = $AccountUri } # If AccountHashPrefix wasn't provided, grab it from the ACME server's directory object if (-not $AccountHashPrefix) { if (-not ($server = Get-PAServer)) { throw "AccountHashPrefix not provided and no ACME server configured. Run Set-PAServer first or provide the AccountHashPrefix parameter." } if (-not ($server.meta.accountHashPrefix)) { throw "AccountHashPrefix not provided and the ACME server has not published one. Unable to continue without AccountHashPrefix." } $AccountHashPrefix = $server.meta.accountHashPrefix } # Prepare the IDN mapping object for later use in domain normalization $idn = [Globalization.IdnMapping]::new() } Process { trap { $PSCmdlet.ThrowTerminatingError($_) } # Normalize the domain name according to draft suggestions # https://www.ietf.org/archive/id/draft-ietf-acme-dns-persist-02.html#section-9.2 $origDomain = $Domain $Domain = $Domain.Trim().ToLowerInvariant().Normalize([Text.NormalizationForm]::FormC) # Remove accidentally included wildcard prefix and any trailing dots $isWildcard = $Domain.StartsWith('*.', [StringComparison]::Ordinal) if ($isWildcard) { $Domain = $Domain.Substring(2) } $Domain = $Domain.TrimEnd('.') # double check the domain isn't empty now if (-not $Domain) { throw 'Domain is empty after removing whitespace and wildcard prefixes.' } # convert the domain to its ASCII-compatible encoding (A-label) form $Domain = $idn.GetAscii($Domain).ToLowerInvariant() Write-Debug "Domain '$origDomain' normalized to '$Domain'" if ($Domain.Length -gt 253) { throw 'Domain exceeds the maximum length of 253 octets.' } foreach ($label in $Domain.Split('.')) { if ($label.Length -gt 63 -or $label -cnotmatch '^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$') { throw "Domain contains an invalid DNS label: $label" } } $recordDomain = if ($isWildcard) { "*.$Domain" } else { $Domain } $hashDomain = if ($NoDomainCorrelationMitigation) { '*' } else { $Domain } # Build the octet sequence: length_of_domain || domain_name || key || account_URL $hashInputBytes = [Collections.Generic.List[byte]]::new() $hashInputBytes.Add([byte]$hashDomain.Length) $hashInputBytes.AddRange([Text.Encoding]::ASCII.GetBytes($hashDomain)) $hashInputBytes.AddRange([Text.Encoding]::ASCII.GetBytes($thumb)) $hashInputBytes.AddRange([Text.Encoding]::ASCII.GetBytes($accountLocation)) $hashInputBytes = $hashInputBytes.ToArray() Write-Debug ("Hash input: length=0x{0:X2}, data={1}" -f $hashInputBytes[0], [Text.Encoding]::ASCII.GetString($hashInputBytes, 1, $hashInputBytes.Length - 1)) $hashB64 = ConvertTo-Base64Url $sha256.ComputeHash($hashInputBytes) $accountUri = '{0}{1}/{2}' -f $AccountHashPrefix, $HashAlgorithm, $hashB64 [pscustomobject]@{ Domain = $recordDomain HashedAccountUri = $accountUri } } } |