internal/AllClasses.ps1
<# abstract #> class KeyExport { KeyExport () { if ($this.GetType() -eq [KeyExport]) { throw [System.InvalidOperationException]::new("Class must be inherited"); } } <# abstract #> [string] TypeName() { throw [System.NotImplementedException]::new(); } } class RSAKeyExport : KeyExport { hidden [string] $TypeName = "RSAKeyExport"; [int] $HashSize; [byte[]] $Modulus; [byte[]] $Exponent; [byte[]] $P; [byte[]] $Q; [byte[]] $DP; [byte[]] $DQ; [byte[]] $InverseQ; [byte[]] $D; } class ECDsaKeyExport : KeyExport { hidden [string] $TypeName = "ECDsaKeyExport"; [int] $HashSize; [byte[]] $D; [byte[]] $X; [byte[]] $Y; } <# abstract #> class KeyBase { [ValidateSet(256,384,512)] [int] hidden $HashSize; [System.Security.Cryptography.HashAlgorithmName] hidden $HashName; KeyBase([int] $hashSize) { if ($this.GetType() -eq [KeyBase]) { throw [System.InvalidOperationException]::new("Class must be inherited"); } $this.HashSize = $hashSize; switch ($hashSize) { 256 { $this.HashName = "SHA256"; } 384 { $this.HashName = "SHA384"; } 512 { $this.HashName = "SHA512"; } default { throw [System.ArgumentOutOfRangeException]::new("Cannot set hash size"); } } } <# abstract #> [KeyExport] ExportKey() { throw [System.NotImplementedException]::new(); } } class Certificate { static [byte[]] ExportPfx([byte[]] $acmeCertificate, [System.Security.Cryptography.AsymmetricAlgorithm] $algorithm, [securestring] $password) { $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($acmeCertificate); if($algorithm -is [System.Security.Cryptography.RSA]) { $certifiate = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($certificate, $algorithm); } elseif($algorithm -is [System.Security.Cryptography.ECDsa]) { $certifiate = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::CopyWithPrivateKey($certificate, $algorithm); } else { throw [System.InvalidOperationException]::new("Cannot use $($algorithm.GetType().Name) to export pfx."); } if($password) { return $certifiate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $password); } else { return $certifiate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx); } } static [byte[]] GenerateCsr([string[]] $dnsNames, [string]$distinguishedName, [System.Security.Cryptography.AsymmetricAlgorithm] $algorithm, [System.Security.Cryptography.HashAlgorithmName] $hashName) { if(-not $dnsNames) { throw [System.ArgumentException]::new("You need to provide at least one DNSName", "dnsNames"); } if(-not $distinguishedName) { thtow [System.ArgumentException]::new("Provide a distinguishedName for the Certificate") } $sanBuilder = [System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder]::new(); foreach ($dnsName in $dnsNames) { $sanBuilder.AddDnsName($dnsName); } $certDN = [X500DistinguishedName]::new($distinguishedName); [System.Security.Cryptography.X509Certificates.CertificateRequest]$certRequest = $null; if($algorithm -is [System.Security.Cryptography.RSA]) { $certRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( $certDN, $algorithm, $hashName, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1); } elseif($algorithm -is [System.Security.Cryptography.ECDsa]) { $certRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( $certDN, $algorithm, $hashName); } else { throw [System.InvalidOperationException]::new("Cannot use $($algorithm.GetType().Name) to create CSR."); } $certRequest.CertificateExtensions.Add($sanBuilder.Build()); return $certRequest.CreateSigningRequest(); } } <# abstract #> class RSAKeyBase : KeyBase { hidden [System.Security.Cryptography.RSA] $RSA; RSAKeyBase([int] $hashSize, [int] $keySize) : base($hashSize) { if ($this.GetType() -eq [RSAKeyBase]) { throw [System.InvalidOperationException]::new("Class must be inherited"); } $this.RSA = [System.Security.Cryptography.RSA]::Create($keySize); } RSAKeyBase([int] $hashSize, [System.Security.Cryptography.RSAParameters] $keyParameters) :base($hashSize) { if ($this.GetType() -eq [RSAKeyBase]) { throw [System.InvalidOperationException]::new("Class must be inherited"); } $this.RSA = [System.Security.Cryptography.RSA]::Create($keyParameters); } [object] ExportKey() { $rsaParams = $this.RSA.ExportParameters($true); $keyExport = [RSAKeyExport]::new(); $keyExport.D = $rsaParams.D; $keyExport.DP = $rsaParams.DP; $keyExport.DQ = $rsaParams.DQ; $keyExport.Exponent = $rsaParams.Exponent; $keyExport.InverseQ = $rsaParams.InverseQ; $keyExport.Modulus = $rsaParams.Modulus; $keyExport.P = $rsaParams.P; $keyExport.Q = $rsaParams.Q; $keyExport.HashSize = $this.HashSize; return $keyExport; } } class RSAAccountKey : RSAKeyBase, IAccountKey { RSAAccountKey([int] $hashSize, [int] $keySize) : base($hashSize, $keySize) { } RSAAccountKey([int] $hashSize, [System.Security.Cryptography.RSAParameters] $keyParameters) : base($hashSize, $keyParameters) { } [string] JwsAlgorithmName() { return "RS$($this.HashSize)" } [System.Collections.Specialized.OrderedDictionary] ExportPublicJwk() { $keyParams = $this.RSA.ExportParameters($false); <# As per RFC 7638 Section 3, these are the *required* elements of the JWK and are sorted in lexicographic order to produce a canonical form #> $publicJwk = [ordered]@{ "e" = ConvertTo-UrlBase64 -InputBytes $keyParams.Exponent; "kty" = "RSA"; # https://tools.ietf.org/html/rfc7518#section-6.3 "n" = ConvertTo-UrlBase64 -InputBytes $keyParams.Modulus; } return $publicJwk; } [byte[]] Sign([byte[]] $inputBytes) { return $this.RSA.SignData($inputBytes, $this.HashName, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1); } [byte[]] Sign([string] $inputString) { return $this.Sign([System.Text.Encoding]::UTF8.GetBytes($inputString)); } static [IAccountKey] Create([RSAKeyExport] $keyExport) { $keyParameters = [System.Security.Cryptography.RSAParameters]::new(); $keyParameters.D = $keyExport.D; $keyParameters.DP = $keyExport.DP; $keyParameters.DQ = $keyExport.DQ; $keyParameters.Exponent = $keyExport.Exponent; $keyParameters.InverseQ = $keyExport.InverseQ; $keyParameters.Modulus = $keyExport.Modulus; $keyParameters.P = $keyExport.P; $keyParameters.Q = $keyExport.Q; return [RSAAccountKey]::new($keyExport.HashSize, $keyParameters); } } class RSACertificateKey : RSAAccountKey, ICertificateKey { RSACertificateKey([int] $hashSize, [int] $keySize) : base($hashSize, $keySize) { } RSACertificateKey([int] $hashSize, [System.Security.Cryptography.RSAParameters] $keyParameters) : base($hashSize, $keyParameters) { } [byte[]] ExportPfx([byte[]] $acmeCertificate, [SecureString] $password) { return [Certificate]::ExportPfx($acmeCertificate, $this.RSA, $password); } [byte[]] GenerateCsr([string[]] $dnsNames, [string] $distinguishedName) { return [Certificate]::GenerateCsr($dnsNames, $distinguishedName, $this.RSA, $this.HashName); } static [ICertificateKey] Create([RSAKeyExport] $keyExport) { $keyParameters = [System.Security.Cryptography.RSAParameters]::new(); $keyParameters.D = $keyExport.D; $keyParameters.DP = $keyExport.DP; $keyParameters.DQ = $keyExport.DQ; $keyParameters.Exponent = $keyExport.Exponent; $keyParameters.InverseQ = $keyExport.InverseQ; $keyParameters.Modulus = $keyExport.Modulus; $keyParameters.P = $keyExport.P; $keyParameters.Q = $keyExport.Q; return [RSACertificateKey]::new($keyExport.HashSize, $keyParameters); } } <# abstract #> class ECDsaKeyBase : KeyBase { hidden [System.Security.Cryptography.ECDsa] $ECDsa; hidden [string] $CurveName ECDsaKeyBase([int] $hashSize) : base($hashSize) { if ($this.GetType() -eq [ECDsaKeyBase]) { throw [System.InvalidOperationException]::new("Class must be inherited"); } $this.CurveName = "P-$hashSize"; $curve = [ECDsaKeyBase]::GetCurve($hashSize); $this.ECDsa = [System.Security.Cryptography.ECDsa]::Create($curve); } ECDsaKeyBase([int] $hashSize,[System.Security.Cryptography.ECParameters] $keyParameters) :base($hashSize) { if ($this.GetType() -eq [ECDsaKeyBase]) { throw [System.InvalidOperationException]::new("Class must be inherited"); } $this.CurveName = "P-$hashSize"; $this.ECDsa = [System.Security.Cryptography.ECDsa]::Create($keyParameters); } static [System.Security.Cryptography.ECCurve] GetCurve($hashSize) { switch ($hashSize) { 256 { return [System.Security.Cryptography.ECCurve+NamedCurves]::nistP256; } 384 { return [System.Security.Cryptography.ECCurve+NamedCurves]::nistP384; } 512 { return [System.Security.Cryptography.ECCurve+NamedCurves]::nistP521; } Default { throw [System.ArgumentOutOfRangeException]::new("Cannot use hash size to create curve."); } } return $null; } [object] ExportKey() { $ecParams = $this.ECDsa.ExportParameters($true); $keyExport = [ECDsaKeyExport]::new(); $keyExport.D = $ecParams.D; $keyExport.X = $ecParams.Q.X; $keyExport.Y = $ecParams.Q.Y; $keyExport.HashSize = $this.HashSize; return $keyExport; } } class ECDsaAccountKey : ECDsaKeyBase, IAccountKey { ECDsaAccountKey([int] $hashSize) : base($hashSize) { } ECDsaAccountKey([int] $hashSize, [System.Security.Cryptography.ECParameters] $keyParameters) : base($hashSize, $keyParameters) { } [string] JwsAlgorithmName() { return "ES$($this.HashSize)" } [System.Collections.Specialized.OrderedDictionary] ExportPublicJwk() { $keyParams = $this.ECDsa.ExportParameters($false); <# As per RFC 7638 Section 3, these are the *required* elements of the JWK and are sorted in lexicographic order to produce a canonical form #> $publicJwk = [ordered]@{ "crv" = $this.CurveName; "kty" = "EC"; # https://tools.ietf.org/html/rfc7518#section-6.2 "x" = ConvertTo-UrlBase64 -InputBytes $keyParams.Q.X; "y" = ConvertTo-UrlBase64 -InputBytes $keyParams.Q.Y; } return $publicJwk; } [byte[]] Sign([byte[]] $inputBytes) { return $this.ECDsa.SignData($inputBytes, $this.HashName); } [byte[]] Sign([string] $inputString) { return $this.Sign([System.Text.Encoding]::UTF8.GetBytes($inputString)); } static [IAccountKey] Create([ECDsaKeyExport] $keyExport) { $keyParameters = [System.Security.Cryptography.ECParameters]::new(); $keyParameters.Curve = [ECDsaKeyBase]::GetCurve($keyExport.HashSize); $keyParameters.D = $keyExport.D; $keyParameters.Q.X = $keyExport.QX; $keyParameters.Q.Y = $keyExport.QY; return [ECDsaAccountKey]::new($keyExport.HashSize, $keyParameters); } } class ECDsaCertificateKey : ECDsaAccountKey, ICertificateKey { ECDsaCertificateKey([int] $hashSize) : base($hashSize) { } ECDsaCertificateKey([int] $hashSize, [System.Security.Cryptography.ECParameters] $keyParameters) : base($hashSize, $keyParameters) { } [byte[]] ExportPfx([byte[]] $acmeCertificate, [SecureString] $password) { return [Certificate]::ExportPfx($acmeCertificate, $this.ECDsa, $password); } [byte[]] GenerateCsr([string[]] $dnsNames, [string] $distinguishedName) { return [Certificate]::GenerateCsr($dnsNames, $distinguishedName, $this.ECDsa, $this.HashName); } static [ICertificateKey] Create([ECDsaKeyExport] $keyExport) { $keyParameters = [System.Security.Cryptography.ECParameters]::new(); $keyParameters.Curve = [ECDsaKeyBase]::GetCurve($keyExport.HashSize); $keyParameters.D = $keyExport.D; $keyParameters.Q.X = $keyExport.QX; $keyParameters.Q.Y = $keyExport.QY; return [ECDsaCertificateKey]::new($keyExport.HashSize, $keyParameters); } } class KeyAuthorization { static hidden [byte[]] ComputeThumbprint([IAccountKey] $accountKey, [System.Security.Cryptography.HashAlgorithm] $hashAlgorithm) { $jwkJson = $accountKey.ExportPublicJwk() | ConvertTo-Json -Compress; $jwkBytes = [System.Text.Encoding]::UTF8.GetBytes($jwkJson); $jwkHash = $hashAlgorithm.ComputeHash($jwkBytes); return $jwkHash; } static [string] Compute([IAccountKey] $accountKey, [string] $token) { $sha256 = [System.Security.Cryptography.SHA256]::Create(); try { $thumbprintBytes = [KeyAuthorization]::ComputeThumbprint($accountKey, $sha256); $thumbprint = ConvertTo-UrlBase64 -InputBytes $thumbprintBytes; return "$token.$thumbprint"; } finally { $sha256.Dispose(); } } static [string] ComputeDigest([IAccountKey] $accountKey, [string] $token) { $sha256 = [System.Security.Cryptography.SHA256]::Create(); try { $thumbprintBytes = [KeyAuthorization]::ComputeThumbprint($accountKey, $sha256); $thumbprint = ConvertTo-UrlBase64 -InputBytes $thumbprintBytes; $keyAuthZBytes = [System.Text.Encoding]::UTF8.GetBytes("$token.$thumbprint"); $digest = $sha256.ComputeHash($keyAuthZBytes); return ConvertTo-UrlBase64 -InputBytes $digest; } finally { $sha256.Dispose(); } } } class Creator { [Type] $TargetType; [Type] $KeyType; [Func[[KeyExport], [KeyBase]]] $Create; Creator([Type] $targetType, [Type] $keyType, [Func[[KeyExport], [KeyBase]]] $creatorFunction) { $this.TargetType = $targetType; $this.KeyType = $keyType; $this.Create = $creatorFunction; } } class KeyFactory { static [System.Collections.ArrayList] $Factories = @( [Creator]::new("IAccountKey", "RSAKeyExport", { param($k) return [RSAAccountKey]::Create($k) }), [Creator]::new("ICertificateKey", "RSAKeyExport", { param($k) return [RSACertificateKey]::Create($k) }), [Creator]::new("IAccountKey", "ECDsaKeyExport", { param($k) return [ECDsaAccountKey]::Create($k) }), [Creator]::new("ICertificateKey", "ECDsaKeyExport", { param($k) return [ECDsaCertificateKey]::Create($k) }) ); hidden static [KeyBase] Create([Type] $targetType, [KeyExport] $keyParameters) { $keyType = $keyParameters.GetType(); $factory = [KeyFactory]::Factories | Where-Object { $_.TargetType -eq $targetType -and $_.KeyType -eq $keyType } | Select-Object -First 1 if ($null -eq $factory) { throw [InvalidOperationException]::new("Unknown KeyParameters-Type."); } return $factory.Create.Invoke($keyParameters); } static [IAccountKey] CreateAccountKey([KeyExport] $keyParameters) { return [KeyFactory]::Create("IAccountKey", $keyParameters); } static [ICertificateKey] CreateCertificateKey([KeyExport] $keyParameters) { return [KeyFactory]::Create("ICertificateKey", $keyParameters); } } class AcmeHttpResponse { AcmeHttpResponse() {} AcmeHttpResponse([System.Net.Http.HttpResponseMessage] $responseMessage, [string] $stringContent) { $this.RequestUri = $responseMessage.RequestMessage.RequestUri; $this.StatusCode = $responseMessage.StatusCode; $this.IsError = $this.StatusCode -ge 400; if($stringContent) { $this.Content = $stringContent | ConvertFrom-Json; } $this.Headers = @{}; foreach($h in $responseMessage.Headers) { $this.Headers.Add($h.Key, $h.Value); if($h.Key -eq "Replay-Nonce") { $this.NextNonce = $h.Value[0]; } } } [string] $RequestUri; [int] $StatusCode; [bool] $IsError; [PSCustomObject] $Content; [hashtable] $Headers; [string] $NextNonce; } class AcmeDirectory { AcmeDirectory([PSCustomObject] $obj) { $this.ResourceUrl = $obj.ResourceUrl $this.NewAccount = $obj.NewAccount; $this.NewAuthz = $obj.NewAuthz; $this.NewNonce = $obj.NewNonce; $this.NewOrder = $obj.NewOrder; $this.KeyChange = $obj.KeyChange; $this.RevokeCert = $obj.RevokeCert; $this.Meta = [AcmeDirectoryMeta]::new($obj.Meta); } [string] $ResourceUrl; [string] $NewAccount; [string] $NewAuthz; [string] $NewNonce; [string] $NewOrder; [string] $KeyChange; [string] $RevokeCert; [AcmeDirectoryMeta] $Meta; } class AcmeDirectoryMeta { AcmeDirectoryMeta([PSCustomObject] $obj) { $this.CaaIdentites = $obj.CaaIdentities; $this.TermsOfService = $obj.TermsOfService; $this.Website = $obj.Website; } [string[]] $CaaIdentites; [string] $TermsOfService; [string] $Website; } class AcmeAccount { AcmeAccount() {} AcmeAccount([AcmeHttpResponse] $httpResponse, [string] $KeyId) { $this.KeyId = $KeyId; $this.Status = $httpResponse.Content.Status; $this.Id = $httpResponse.Content.Id; $this.Contact = $httpResponse.Content.Contact; $this.InitialIp = $httpResponse.Content.InitialIp; $this.CreatedAt = $httpResponse.Content.CreatedAt; $this.OrderListUrl = $httpResponse.Content.Orders; $this.ResourceUrl = $KeyId; } [string] $ResourceUrl; [string] $KeyId; [string] $Status; [string] $Id; [string[]] $Contact; [string] $InitialIp; [string] $CreatedAt; [string] $OrderListUrl; } class AcmeIdentifier { AcmeIdentifier([string] $type, [string] $value) { $this.Type = $type; $this.Value = $value; } AcmeIdentifier([PsCustomObject] $obj) { $this.type = $obj.type; $this.value = $obj.Value; } [string] $type; [string] $value; [string] ToString() { return "$($this.Type.ToLower()):$($this.Value.ToLower())"; } } class AcmeChallenge { AcmeChallenge([PSCustomObject] $obj, [AcmeIdentifier] $identifier) { $this.Type = $obj.type; $this.Url = $obj.url; $this.Token = $obj.token; $this.Identifier = $identifier; } [string] $Type; [string] $Url; [string] $Token; [AcmeIdentifier] $Identifier; [PSCustomObject] $Data; } class AcmeCsrOptions { AcmeCsrOptions() { } AcmeCsrOptions([PsCustomObject] $obj) { $this.DistinguishedName = $obj.DistinguishedName } [string]$DistinguishedName; } class AcmeOrder { AcmeOrder([PsCustomObject] $obj) { $this.Status = $obj.Status; $this.Expires = $obj.Expires; $this.NotBefore = $obj.NotBefore; $this.NotAfter = $obj.NotAfter; $this.Identifiers = $obj.Identifiers | ForEach-Object { [AcmeIdentifier]::new($_) }; $this.AuthorizationUrls = $obj.AuthorizationUrls; $this.FinalizeUrl = $obj.FinalizeUrl; $this.CertificateUrl = $obj.CertificateUrl; $this.ResourceUrl = $obj.ResourceUrl; if($obj.CSROptions) { $this.CSROptions = [AcmeCsrOptions]::new($obj.CSROptions) } else { $this.CSROptions = [AcmeCsrOptions]::new() } } AcmeOrder([AcmeHttpResponse] $httpResponse) { $this.UpdateOrder($httpResponse) $this.CSROptions = [AcmeCsrOptions]::new(); } AcmeOrder([AcmeHttpResponse] $httpResponse, [AcmeCsrOptions] $csrOptions) { $this.UpdateOrder($httpResponse) if($csrOptions) { $this.CSROptions = $csrOptions; } else { $this.CSROptions = [AcmeCSROptions]::new() } } [void] UpdateOrder([AcmeHttpResponse] $httpResponse) { $this.Status = $httpResponse.Content.Status; $this.Expires = $httpResponse.Content.Expires; $this.NotBefore = $httpResponse.Content.NotBefore; $this.NotAfter = $httpResponse.Content.NotAfter; $this.Identifiers = $httpResponse.Content.Identifiers | ForEach-Object { [AcmeIdentifier]::new($_) }; $this.AuthorizationUrls = $httpResponse.Content.Authorizations; $this.FinalizeUrl = $httpResponse.Content.Finalize; $this.CertificateUrl = $httpResponse.Content.Certificate; if($httpResponse.Headers.Location) { $this.ResourceUrl = $httpResponse.Headers.Location[0]; } else { $this.ResourceUrl = $httpResponse.RequestUri; } } [string] $ResourceUrl; [string] $Status; [string] $Expires; [Nullable[System.DateTimeOffset]] $NotBefore; [Nullable[System.DateTimeOffset]] $NotAfter; [AcmeIdentifier[]] $Identifiers; [string[]] $AuthorizationUrls; [string] $FinalizeUrl; [string] $CertificateUrl; [AcmeCsrOptions] $CSROptions; } class AcmeAuthorization { AcmeAuthorization([AcmeHttpResponse] $httpResponse) { $this.status = $httpResponse.Content.status; $this.expires = $httpResponse.Content.expires; $this.identifier = [AcmeIdentifier]::new($httpResponse.Content.identifier); $this.challenges = @($httpResponse.Content.challenges | ForEach-Object { [AcmeChallenge]::new($_, $this.identifier) }); $this.wildcard = $httpResponse.Content.wildcard; $this.ResourceUrl = $httpResponse.RequestUri; } [string] $ResourceUrl; [string] $Status; [System.DateTimeOffset] $Expires; [AcmeIdentifier] $Identifier; [AcmeChallenge[]] $Challenges; [bool] $Wildcard; } class AcmeState { [ValidateNotNull()] hidden [AcmeDirectory] $ServiceDirectory; [ValidateNotNull()] hidden [string] $Nonce; [ValidateNotNull()] hidden [IAccountKey] $AccountKey; [ValidateNotNull()] hidden [AcmeAccount] $Account; hidden [string] $SavePath; hidden [bool] $AutoSave; hidden [bool] $IsInitializing; hidden [AcmeStatePaths] $Filenames; AcmeState() { $this.Filenames = [AcmeStatePaths]::new(""); } AcmeState([string] $savePath, [bool]$autoSave) { $this.Filenames = [AcmeStatePaths]::new($savePath); if(-not (Test-Path $savePath)) { New-Item $savePath -ItemType Directory -Force; } $this.AutoSave = $autoSave; $this.SavePath = Resolve-Path $savePath; } [AcmeDirectory] GetServiceDirectory() { return $this.ServiceDirectory; } [string] GetNonce() { return $this.Nonce; } [IAccountKey] GetAccountKey() { return $this.AccountKey; } [AcmeAccount] GetAccount() { return $this.Account; } [void] Set([AcmeDirectory] $serviceDirectory) { $this.ServiceDirectory = $serviceDirectory; if($this.AutoSave) { $directoryPath = $this.Filenames.ServiceDirectory; Write-Debug "Storing the service directory to $directoryPath"; $this.ServiceDirectory | Export-AcmeObject $directoryPath -Force; } } [void] SetNonce([string] $nonce) { $this.Nonce = $nonce; if($this.AutoSave) { $noncePath = $this.Filenames.Nonce; Set-Content $noncePath -Value $nonce -NoNewLine; } } [void] Set([IAccountKey] $accountKey) { $this.AccountKey = $accountKey; if($this.AutoSave) { $accountKeyPath = $this.Filenames.AccountKey; $this.AccountKey | Export-AccountKey $accountKeyPath -Force; } elseif(-not $this.IsInitializing) { Write-Warning "The account key will not be exported." Write-Information "Make sure you save the account key or you might loose access to your ACME account."; } } [void] Set([AcmeAccount] $account) { $this.Account = $account; if($this.AutoSave) { $accountPath = $this.Filenames.Account; $this.Account | Export-AcmeObject $accountPath; } } hidden [void] LoadFromPath() { $this.IsInitializing = $true; $this.AutoSave = $false; $directoryPath = $this.Filenames.ServiceDirectory; $noncePath = $this.Filenames.Nonce; $accountKeyPath = $this.Filenames.AccountKey; $accountPath = $this.Filenames.Account; if(Test-Path $directoryPath) { Get-ServiceDirectory $this -Path $directoryPath } else { Write-Verbose "Could not find saved service directory at $directoryPath"; } if(Test-Path $noncePath) { $importedNonce = Get-Content $noncePath -Raw if($importedNonce) { $this.SetNonce($importedNonce); } else { New-Nonce $this; } } else { Write-Verbose "Could not find saved nonce at $noncePath"; } if(Test-Path $accountKeyPath) { Import-AccountKey $this -Path $accountKeyPath } else { Write-Verbose "Could not find saved account key at $accountKeyPath"; } if(Test-Path $accountPath) { $importedAccount = Import-AcmeObject -Path $accountPath -TypeName "AcmeAccount" $this.Set($importedAccount); } else { Write-Verbose "Could not find saved account at $accountPath"; } $this.AutoSave = $true; $this.IsInitializing = $false } hidden [string] GetOrderHash([AcmeOrder] $order) { $orderIdentifiers = $order.Identifiers | Foreach-Object { $_.ToString() } | Sort-Object; $identifier = [string]::Join('|', $orderIdentifiers); $sha256 = [System.Security.Cryptography.SHA256]::Create(); try { $identifierBytes = [System.Text.Encoding]::UTF8.GetBytes($identifier); $identifierHash = $sha256.ComputeHash($identifierBytes); $result = ConvertTo-UrlBase64 -InputBytes $identifierHash; return $result; } finally { $sha256.Dispose(); } } hidden [string] GetOrderFileName([string] $orderHash) { $fileName = $this.Filenames.Order.Replace("[hash]", $orderHash); return $fileName; } hidden [AcmeOrder] LoadOrder([string] $orderHash) { $orderFile = $this.GetOrderFileName($orderHash); if(Test-Path $orderFile) { $order = Import-AcmeObject -Path $orderFile -TypeName "AcmeOrder"; return $order; } return $null; } [void] AddOrder([AcmeOrder] $order) { $this.SetOrder($order); } [void] SetOrder([AcmeOrder] $order) { if($this.AutoSave) { $orderHash = $this.GetOrderHash($order); $orderFileName = $this.GetOrderFileName($orderHash); if(-not (Test-Path $order)) { $orderListFile = $this.Filenames.OrderList; foreach ($id in $order.Identifiers) { if(-not (Test-Path $orderListFile)) { New-Item $orderListFile -Force; } "$($id.ToString())=$orderHash" | Add-Content $orderListFile; } } $order | Export-AcmeObject $orderFileName -Force; } else { Write-Warning "auto saving the state has been disabled, so set order is a no-op". } } [void] RemoveOrder([AcmeOrder] $order) { if($this.AutoSave) { $orderHash = $this.GetOrderHash($order); $orderFileName = $this.GetOrderFileName($orderHash); if(Test-Path $orderFileName) { Remove-Item $orderFileName; } $orderListFile = $this.Filenames.OrderList; Set-Content -Path $orderListFile -Value (Get-Content -Path $orderListFile | Select-String -Pattern "=$orderHash" -NotMatch -SimpleMatch) } else { Write-Warning "auto saving the state has been disabled, so set order is a no-op". } } [AcmeOrder] FindOrder([string[]] $dnsNames) { $orderListFile = $this.Filenames.OrderList; $first = $true; $lastMatch = $null; foreach($dnsName in $dnsNames) { $match = Select-String -Path $orderListFile -Pattern "$dnsName=" -SimpleMatch | Select-Object -Last 1 if($first) { $lastMatch = $match; } if($match -ne $lastMatch) { return $null; } $lastMatch = $match; } $orderHash = ($lastMatch -split "=", 2)[1]; return $this.LoadOrder($orderHash); } [bool] Validate() { return $this.Validate("Account"); } [bool] Validate([string] $field) { $isValid = $true; if($field -in @("ServiceDirectory", "Nonce", "AccountKey", "Account")) { if($null -eq $this.ServiceDirectory) { $isValid = $false; Write-Warning "State does not contain a service directory. Run Get-ACMEServiceDirectory to get one." } } if($field -in @("Nonce", "AccountKey", "Account")) { if($null -eq $this.Nonce) { $isValid = $false; Write-Warning "State does not contain a nonce. Run New-ACMENonce to get one." } } if($field -in @("AccountKey", "Account")) { if($null -eq $this.AccountKey) { $isValid = $false; Write-Warning "State does not contain an account key. Run New-ACMEAccountKey to create one." } } if($field -in @("Account")) { if($null -eq $this.Account) { $isValid = $false; Write-Warning "State does not contain an account. Register one by running New-ACMEAccount." } } return $isValid; } static [AcmeState] FromPath([string] $Path) { $state = [AcmeState]::new($Path, $false); $state.LoadFromPath(); return $state; } } class AcmeStatePaths { [string] $ServiceDirectory; [string] $Nonce; [string] $AccountKey; [string] $Account; [string] $OrderList; [string] $Order; AcmeStatePaths([string] $basePath) { $this.ServiceDirectory = "$basePath/ServiceDirectory.xml"; $this.Nonce = "$basePath/NextNonce.txt"; $this.AccountKey = "$basePath/AccountKey.xml"; $this.Account = "$basePath/Account.xml"; $this.OrderList = "$basePath/Orders/OrderList.txt" $this.Order = "$basePath/Orders/Order-[hash].xml"; } } |