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";
    }
}