SignPath.psm1

#Requires -Version 3.0
# Also requires .NET Version 4.7.2 or above

$ServiceUnavailableRetryTimeoutInSeconds = 30
$WaitForCompletionRetryTimeoutInSeconds = 5
$DefaultHttpClientTimeoutInSeconds = 100

<#
.SYNOPSIS
  Submits a new signing request or resubmits an existing one via the SignPath REST API.
.DESCRIPTION
  The Submit-SigningRequest cmdlet creates a new signing request. The signing request will be processed by SignPath according to authorization and policy rules.
 
  CREATING A NEW SIGNING REQUEST VS. RE-SIGNING
 
  When using the -InputArtifact parameter, the specified file will be uploaded for processing.
 
  When using the -Resubmit parameter, the specified signing request will be processed again using the specified signing policy. This is especially useful for conditional signing of release candidates. See Resubmit an existing signing request for more information.
 
  DOWNLOADING THE SIGNED ARTIFACT
 
  Processing a signing request may take several minutes, or even longer if manual approval is required. You can either
 
  * your own logic to wait for processing to complete and
    download the signed artifact using the Get-SignedArtifact
    cmdlet afterwards,
  * or use the -WaitForCompletion parameter of this cmdlet
    to wait for processing and then download the
    signed artifact in a single call.
 
.EXAMPLE
  Submit-SigningRequest `
    -OrganizationId $ORGANIZATION_ID -ApiToken $API_TOKEN `
    -ProjectSlug $PROJECT -SigningPolicySlug $SIGNING_POLICY `
    -ArtifactConfigurationSlug $ARTIFACT_CONFIGURATION `
    -InputArtifactPath $PATH_TO_INPUT_ARTIFACT `
    -WaitForCompletion `
    -OutputArtifactPath $PATH_TO_OUTPUT_ARTIFACT
.EXAMPLE
  Submit-SigningRequest `
    -OrganizationId $ORGANIZATION_ID -ApiToken $API_TOKEN `
    -ProjectSlug $PROJECT -SigningPolicySlug $SIGNING_POLICY `
    -ArtifactConfigurationSlug $ARTIFACT_CONFIGURATION `
    -ArtifactRetrievalLink $URL_TO_INPUT_ARTIFACT `
    -ArtifactRetrievalLinkFileName $FILE_NAME_OF_INPUT_ARTIFACT `
    -ArtifactRetrievalLinkSha256Hash $SHA256_HASH_OF_INPUT_ARTIFACT_IN_HEX_STRING_FORMAT `
    -ArtifactRetrievalLinkHttpHeaders @{
      "$ARTIFACT_RETRIEVAL_LINK_HTTP_HEADER_KEY1" = "$ARTIFACT_RETRIEVAL_LINK_HTTP_HEADER_VALUE1"
      "$ARTIFACT_RETRIEVAL_LINK_HTTP_HEADER_KEY2" = "$ARTIFACT_RETRIEVAL_LINK_HTTP_HEADER_VALUE2"
    } `
    -WaitForCompletion `
    -OutputArtifactPath $PATH_TO_OUTPUT_ARTIFACT
.EXAMPLE
  $signingRequestID = Submit-SigningRequest `
    -OrganizationId $ORGANIZATION_ID `-ApiToken $API_TOKEN `
    -ProjectSlug $PROJECT -SigningPolicySlug $SIGNING_POLICY `
    -ArtifactConfigurationSlug $ARTIFACT_CONFIGURATION `
    -InputArtifactPath $PATH_TO_INPUT_ARTIFACT
 
 Submit a signing request and get a signing request ID withoput waiting for completion and download the signed artifact later
 
PS > Get-SignedArtifact `
    -OrganizationId $ORGANIZATION_ID -ApiToken $API_TOKEN `
    -SigningRequestId $signingRequestID `
    -OutputArtifactPath $PATH_TO_OUTPUT_ARTIFACT
 
.EXAMPLE
  Submit-SigningRequestResubmit `
    -ApiToken $API_TOKEN -OrganizationId $ORGANIZATION_ID `
    -OriginalSigningRequestId $ORIGINAL_SIGNING_REQUEST_ID `
    -SigningPolicySlug $SIGNING_POLICY `
    -WaitForCompletion `
    -OutputArtifactPath $PATH_TO_OUTPUT_ARTIFACT
.OUTPUTS
  Returns the SigningRequestId which can be used with Get-SignedArtifact.
.NOTES
  Author: SignPath GmbH
.LINK
  https://about.signpath.io/documentation/powershell/Submit-SigningRequest
#>

function Submit-SigningRequest {
  [CmdletBinding(DefaultParameterSetName = 'Submit')]
  Param(
    # URL to the SignPath REST API, e.g. 'https://app.signpath.io/api/'.
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string] $ApiUrl = "https://app.signpath.io/api/",

    # API token you receive when adding a new CI user or generating an API token for an interactive user.
    [Parameter(Mandatory)]
    [Alias("CIUserToken")]
    [ValidateNotNullOrEmpty()]
    [string] $ApiToken,

    # ID of your SignPath organization.
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $OrganizationId,

    # ID of one of the project's artifact configurations.
    [Parameter(ParameterSetName = 'SubmitWithArtifact')]
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [string] $ArtifactConfigurationId,

    # ID of a project's signing policy.
    [Parameter()]
    [string] $SigningPolicyId,

    # Slug of the project.
    [Parameter(ParameterSetName = 'SubmitWithArtifact')]
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Alias("ProjectKey")]
    [string] $ProjectSlug,

    # Slug of one of the project's artifact configurations.
    # If not given, the default artifact configuration will be used instead.
    [Parameter(ParameterSetName = 'SubmitWithArtifact')]
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Alias("ArtifactConfigurationKey")]
    [string] $ArtifactConfigurationSlug,

    # Slug of one of the project's signing policies.
    [Parameter()]
    [Alias("SigningPolicyKey")]
    [string] $SigningPolicySlug,

    # Path of the artifact that you want to be signed.
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifact')]
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [ValidateNotNullOrEmpty()]
    [string] $InputArtifactPath,

    # URL where the artifact that you want to be signed will be downloaded from.
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [ValidateNotNullOrEmpty()]
    [string] $ArtifactRetrievalLink,

    # File name of the artifact to be signed.
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [ValidateNotNullOrEmpty()]
    [string] $ArtifactRetrievalLinkFileName,

    # Optional file hash (in hex string format) of the artifact to sign (used to verify the artifact download).
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [ValidateNotNullOrEmpty()]
    [string] $ArtifactRetrievalLinkSha256Hash,

    # Optional HTTP headers that will be used when downloading the artifact to sign.
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [ValidateNotNullOrEmpty()]
    [Hashtable] $ArtifactRetrievalLinkHttpHeaders,

    # Optional description of the signing request.
    [Parameter()]
    [string] $Description,

    # Information about the origin of the artifact, see https://about.signpath.io/documentation/powershell#submit-signingrequest
    [Parameter(ParameterSetName = 'SubmitWithArtifact')]
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Hashtable] $Origin,

    # Values for parameters defined in the artifact configuration. See https://about.signpath.io/documentation/artifact-configuration#user-defined-parameters
    [Parameter(ParameterSetName = 'SubmitWithArtifact')]
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Hashtable] $Parameters,

    # Client certificate used for a secure Web API request. Not supported by SignPath.io directly, use for proxies.
    [Parameter()]
    [System.Security.Cryptography.X509Certificates.X509Certificate2] $ClientCertificate,

    # Total time in seconds that the cmdlet will wait for a single service call to succeed (across several retries). Defaults to 600 seconds.
    [Parameter()]
    [int] $ServiceUnavailableTimeoutInSeconds = 600,

    # HTTP timeout used for upload and download HTTP requests. Defaults to 300 seconds.
    [Parameter()]
    [int] $UploadAndDownloadRequestTimeoutInSeconds = 300,

    # Re-sign an existing signing request. SigningPolicySlug or SigningPolicyId reference the original project's signing policy.
    [Parameter(Mandatory, ParameterSetName = 'Resubmit')]
    [Parameter(Mandatory, ParameterSetName = 'Resubmit_WaitForCompletion')]
    [switch] $Resubmit,

    # ID of the signing request that should be resubmitted.
    [Parameter(Mandatory, ParameterSetName = 'Resubmit')]
    [Parameter(Mandatory, ParameterSetName = 'Resubmit_WaitForCompletion')]
    [ValidateNotNullOrEmpty()]
    [string] $OriginalSigningRequestId,

    # Wait for the signing request to complete.
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Parameter(Mandatory, ParameterSetName = 'Resubmit_WaitForCompletion')]
    [switch] $WaitForCompletion,

    # Specifies the target path for the downloaded signed artifact. Defaults to InputArtifactPath with an added .signed extension (e.g. "Input.dll" => "Input.signed.dll").
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(Mandatory, ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Parameter(Mandatory, ParameterSetName = 'Resubmit_WaitForCompletion')]
    [ValidateNotNullOrEmpty()]
    [string] $OutputArtifactPath,

    # Maximum time in seconds that the cmdlet will wait for the signing request to complete (upload and download have no specific timeouts). Defaults to 600 seconds.
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Parameter(ParameterSetName = 'Resubmit_WaitForCompletion')]
    [int] $WaitForCompletionTimeoutInSeconds = 600,

    # Allows the cmdlet to overwrite the file at OutputArtifactPath.
    [Parameter(ParameterSetName = 'SubmitWithArtifact_WaitForCompletion')]
    [Parameter(ParameterSetName = 'SubmitWithArtifactRetrievalLink_WaitForCompletion')]
    [Parameter(ParameterSetName = 'Resubmit_WaitForCompletion')]
    [switch] $Force
  )

  Set-StrictMode -Version 2.0

  $ApiUrl = GetVersionedApiUrl $ApiUrl
  Write-Verbose "Using versioned API URL: $ApiUrl"

  if($Resubmit.IsPresent) {
    $resubmitUrl = [string]::Join("/", @($ApiUrl.Trim("/"), $OrganizationId, "SigningRequests", "Resubmit"))
    $requestFactory = CreateResubmitRequestFactory `
      -url $resubmitUrl `
      -originalSigningRequestId $OriginalSigningRequestId `
      -signingPolicySlug $SigningPolicySlug `
      -description $Description

    return SubmitHelper "Resubmit" "Resubmitted" $requestFactory `
      -apiToken $ApiToken `
      -clientCertificate $ClientCertificate `
      -defaultHttpClientTimeoutInSeconds $DefaultHttpClientTimeoutInSeconds `
      -uploadAndDownloadRequestTimeoutInSeconds $UploadAndDownloadRequestTimeoutInSeconds `
      -waitForCompletionTimeoutInSeconds $WaitForCompletionTimeoutInSeconds `
      -waitForCompletion $WaitForCompletion.IsPresent `
      -outputArtifactPath $OutputArtifactPath `
      -force $Force.IsPresent
  } else {
    if ($InputArtifactPath) {
      if (-not $OutputArtifactPath) {
        $extension = [System.IO.Path]::GetExtension($InputArtifactPath)
        $OutputArtifactPath = [System.IO.Path]::ChangeExtension($InputArtifactPath, "signed$extension")
      }

      $InputArtifactPath = PrepareInputArtifactPath $InputArtifactPath
      Write-Verbose "Using input artifact: $InputArtifactPath"

      $hash = (Get-FileHash -Path $InputArtifactPath -Algorithm "SHA256").Hash
      Write-Host "SHA256 hash: $hash"

      $submitUrl = [string]::Join("/", @($ApiUrl.Trim("/"), $OrganizationId, "SigningRequests"))
      $requestFactory = CreateSubmitWithArtifactRequestFactory `
        -Url $submitUrl `
        -ArtifactConfigurationId $ArtifactConfigurationId `
        -SigningPolicyId $SigningPolicyId `
        -ProjectSlug $ProjectSlug `
        -ArtifactConfigurationSlug $ArtifactConfigurationSlug `
        -SigningPolicySlug $SigningPolicySlug `
        -Description $Description `
        -InputArtifactPath $InputArtifactPath `
        -Origin $Origin `
        -Parameters $Parameters
    } else {
      $submitUrl = [string]::Join("/", @($ApiUrl.Trim("/"), $OrganizationId, "SigningRequests", "SubmitWithArtifactRetrievalLink"))
      $requestFactory = CreateSubmitWithArtifactRetrievalLinkRequestFactory `
        -Url $submitUrl `
        -ArtifactConfigurationId $ArtifactConfigurationId `
        -SigningPolicyId $SigningPolicyId `
        -ProjectSlug $ProjectSlug `
        -ArtifactConfigurationSlug $ArtifactConfigurationSlug `
        -SigningPolicySlug $SigningPolicySlug `
        -Description $Description `
        -ArtifactRetrievalLink $ArtifactRetrievalLink `
        -ArtifactRetrievalLinkFileName $ArtifactRetrievalLinkFileName `
        -ArtifactRetrievalLinkSha256Hash $ArtifactRetrievalLinkSha256Hash `
        -ArtifactRetrievalLinkHttpHeaders $ArtifactRetrievalLinkHttpHeaders `
        -Origin $Origin `
        -Parameters $Parameters
    }

    return SubmitHelper "Submit" "Submitted" $requestFactory `
        -apiToken $ApiToken `
        -clientCertificate $ClientCertificate `
        -defaultHttpClientTimeoutInSeconds $DefaultHttpClientTimeoutInSeconds `
        -uploadAndDownloadRequestTimeoutInSeconds $UploadAndDownloadRequestTimeoutInSeconds `
        -waitForCompletionTimeoutInSeconds $WaitForCompletionTimeoutInSeconds `
        -waitForCompletion $WaitForCompletion.IsPresent `
        -outputArtifactPath $OutputArtifactPath `
        -force $Force.IsPresent
  }
}

<#
.SYNOPSIS
    Downloads a signed artifact based on a signing request ID.
.DESCRIPTION
    The Get-SignedArtifact cmdlet waits for a given signing request to finish and downloads the resultiung artifact.
 
    If the request couldn't be downloaded in time, because the processing took to long or the request is invalid, this cmdlet will throw exceptions.
.EXAMPLE
  Get-SignedArtifact `
    -OutputArtifactPath Program.exe `
    -ApiToken /Joe3s2m7hkhVyoba4H4weqj9UxIk6nKRXGhGbH7nv4= `
    -OrganizationId 1c0ab26c-12f3-4c6e-a043-2568e133d2de `
    -SigningRequestId 711960ed-bdb8-41cd-a6bf-a10d0ae3cfcd
.EXAMPLE
  $signingRequestID = Submit-SigningRequest `
    -OrganizationId $ORGANIZATION_ID `-ApiToken $API_TOKEN `
    -ProjectSlug $PROJECT -SigningPolicySlug $SIGNING_POLICY `
    -ArtifactConfigurationSlug $ARTIFACT_CONFIGURATION `
    -InputArtifactPath $PATH_TO_INPUT_ARTIFACT
 
PS > Get-SignedArtifact `
    -OrganizationId $ORGANIZATION_ID -ApiToken $API_TOKEN `
    -SigningRequestId $signingRequestID `
    -OutputArtifactPath $PATH_TO_OUTPUT_ARTIFACT
.OUTPUTS
    Returns void but creates a file in the given OutputArtifactPath on success.
.NOTES
  Author: SignPath GmbH
.LINK
  https://about.signpath.io/documentation/powershell/Get-SignedArtifact
#>

function Get-SignedArtifact {
  [CmdletBinding()]
  Param(
    # URL to the SignPath REST API, e.g. https://app.signpath.io/api/.
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string] $ApiUrl = "https://app.signpath.io/api/",

    # API token you receive when adding a new CI user or generating an API token for an interactive user.
    [Parameter(Mandatory)]
    [Alias("CIUserToken")]
    [ValidateNotNullOrEmpty()]
    [string] $ApiToken,

    # ID of your SignPath organization.
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $OrganizationId,

    # ID of the siging request.
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $SigningRequestId,

    # Specifies the target path for the downloaded signed artifact.
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $OutputArtifactPath,

    # Client certificate used for a secure Web API request. Not supported by SignPath.io directly, use for proxies.
    [Parameter()]
    [System.Security.Cryptography.X509Certificates.X509Certificate2] $ClientCertificate,

    # Total time in seconds that the cmdlet will wait for a single service call to succeed (across several retries). Defaults to 600 seconds.
    [Parameter()]
    [int] $ServiceUnavailableTimeoutInSeconds = 600,

    # HTTP timeout used for upload and download HTTP requests. Defaults to 300 seconds.
    [Parameter()]
    [int] $UploadAndDownloadRequestTimeoutInSeconds = 300,

    # Maximum time in seconds that the cmdlet will wait for the signing request to complete (upload and download have no specific timeouts). Defaults to 600 seconds.
    [Parameter()]
    [int] $WaitForCompletionTimeoutInSeconds = 600,

    # Allows the cmdlet to overwrite the file at OutputArtifactPath.
    [Parameter()]
    [switch] $Force
  )

  Set-StrictMode -Version 2.0

  $ApiUrl = GetVersionedApiUrl $ApiUrl
  Write-Verbose "Using versioned API URL: $ApiUrl"

  $OutputArtifactPath = PrepareOutputArtifactPath $Force $OutputArtifactPath
  Write-Verbose "Will write signed artifact to: $OutputArtifactPath"

  CreateAndUseAuthorizedHttpClient $ApiToken -ClientCertificate $ClientCertificate -Timeout $DefaultHttpClientTimeoutInSeconds {
    Param ([System.Net.Http.HttpClient] $defaultHttpClient)

    CreateAndUseAuthorizedHttpClient $ApiToken -Timeout $UploadAndDownloadRequestTimeoutInSeconds {
      Param ([System.Net.Http.HttpClient] $uploadAndDownloadHttpClient)

      $expectedSigningRequestUrl = [string]::Join("/", @($ApiUrl.Trim("/"), $OrganizationId, "SigningRequests", $SigningRequestId))

      $downloadUrl = WaitForCompletionAndRetrieveSignedArtifactDownloadLink `
        -httpClient $defaultHttpClient `
        -url $expectedSigningRequestUrl `
        -WaitForCompletionTimeoutInSeconds $WaitForCompletionTimeoutInSeconds `
        -WaitForCompletionRetryTimeoutInSeconds $WaitForCompletionRetryTimeoutInSeconds `
        -ServiceUnavailableRetryTimeoutInSeconds $ServiceUnavailableRetryTimeoutInSeconds

      DownloadArtifact `
        -HttpClient $uploadAndDownloadHttpClient `
        -Url $downloadUrl `
        -Path $OutputArtifactPath `
        -ServiceUnavailableRetryTimeoutInSeconds $ServiceUnavailableRetryTimeoutInSeconds
    }
  }
}

<#
.SYNOPSIS
    Gets a certificate from the specified certificate store by its Microsoft AD CS template ID.
.DESCRIPTION
    Use this cmdlet to get an X.509 certificate object enrolled using Active Directory Certificate Services (AD CS) from its template ID.
    This can be used to authenticate group memberships via mTLS client certificates:
      * Create a client certificate template in AD CS and assign it to a user or computer group
      * Use this cmdlet to select the correct certificate for mTLS authentication
      * Provide the certificate to SignPath cmdlets using the -ClientCertificate parameter
.EXAMPLE
  $certificate = Get-CertificateByMicrosoftTemplateId -Store CurrentUser -TemplateId 1.3.6.1.4.1.311.21.8.1.2.3.4.5.6.7.8
.OUTPUTS
    Returns a X509Certificate2 instance on success.
.NOTES
  Author: SignPath GmbH
.LINK
  https://about.signpath.io/documentation/powershell/Get-CertificateByMicrosoftTemplateId
#>

function Get-CertificateByMicrosoftTemplateId {
  [CmdletBinding()]
  Param(
    # The store that will be searched for the certificate.
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [System.Security.Cryptography.X509Certificates.StoreLocation] $Store,

    # The Microsoft AD CS template ID in OID format (a dotted number sequence, e.g. 1.2.3.4).
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $TemplateId
  )

  $storeLocationPath = "cert:\${Store}\My"

  # Actual template data contains a ASN.1 data structure, but we don't have access to ASN.1 reader classes in PowerShell classic therefore
  # we just look for the byte sequence of the template ID (but in a string to make use of IndexOf)
  $templateIdBytes = [System.Security.Cryptography.CryptoConfig]::EncodeOID($TemplateId)
  $templateIdString = [BitConverter]::ToString($templateIdBytes)

  $matchingCertificates = @(
    Get-ChildItem $storeLocationPath `
    | ?{
      # = MS CertificateTemplate, see https://oidref.com/1.3.6.1.4.1.311.21.7
      $extension = $_.Extensions["1.3.6.1.4.1.311.21.7"]

      if ($extension -and $extension.RawData) {
        $extensionBytesString = [BitConverter]::ToString($extension.RawData)
        return $extensionBytesString.IndexOf($templateIdString) -ge 0
      }

      return $false
    }
  )

  if ($matchingCertificates.Count -eq 0) {
    throw "Cannot find any certificate in '$Store' that matches the template ID '$TemplateId'."
  } elseif ($matchingCertificates.Count -gt 1) {
    throw "Found multiple certificates in '$Store' that match the template ID '$TemplateId'."
  } else {
    return $matchingCertificates[0]
  }
}

function GetVersionedApiUrl ([string] $apiUrl) {
  $supportedApiVersion = "v1"
  return [string]::Join("/", @($apiUrl.Trim("/"), $supportedApiVersion))
}

function SubmitHelper ([string] $verb,
                       [string] $verbPastTense,
                       [ScriptBlock] $requestFactory,
                       [string] $apiToken,
                       [System.Security.Cryptography.X509Certificates.X509Certificate2] $clientCertificate,
                       [int] $defaultHttpClientTimeoutInSeconds,
                       [int] $uploadAndDownloadRequestTimeoutInSeconds,
                       [int] $waitForCompletionTimeoutInSeconds,
                       [bool] $waitForCompletion,
                       [string] $outputArtifactPath,
                       [bool] $force) {
  CreateAndUseAuthorizedHttpClient $apiToken -ClientCertificate $clientCertificate -Timeout $defaultHttpClientTimeoutInSeconds {
    Param ([System.Net.Http.HttpClient] $defaultHttpClient)

    CreateAndUseAuthorizedHttpClient $apiToken -ClientCertificate $clientCertificate -Timeout $uploadAndDownloadRequestTimeoutInSeconds {
      Param ([System.Net.Http.HttpClient] $uploadAndDownloadHttpClient)

      if ($waitForCompletion) {
        $outputArtifactPath = PrepareOutputArtifactPath $force $outputArtifactPath
        Write-Verbose "Will write output artifact to: $outputArtifactPath"
      }

      $response = $null
      try {
        Write-Verbose "$verb signing request..."
        $response = SendWithRetry `
          -HttpClient $HttpClient `
          -RequestFactory $requestFactory `
          -ServiceUnavailableRetryTimeoutInSeconds $ServiceUnavailableRetryTimeoutInSeconds
        CheckResponse $response -ExpectLocationHeader

        $getUrl = $response.Headers.Location.AbsoluteUri
        Write-Host "$verbPastTense signing request at '$getUrl'"
      } finally {
        if ((Test-Path variable:response) -and $null -ne $response) {
          $response.Dispose()
        }
      }

      if ($waitForCompletion) {
        $downloadUrl = WaitForCompletionAndRetrieveSignedArtifactDownloadLink `
          -HttpClient $defaultHttpClient `
          -Url $getUrl `
          -WaitForCompletionTimeoutInSeconds $waitForCompletionTimeoutInSeconds `
          -WaitForCompletionRetryTimeoutInSeconds $WaitForCompletionRetryTimeoutInSeconds `
          -ServiceUnavailableRetryTimeoutInSeconds $ServiceUnavailableRetryTimeoutInSeconds

        DownloadArtifact `
          -HttpClient $uploadAndDownloadHttpClient `
          -Url $downloadUrl `
          -Path $outputArtifactPath `
          -ServiceUnavailableRetryTimeoutInSeconds $ServiceUnavailableRetryTimeoutInSeconds
      }

      $guidRegex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
      $pattern = [regex]"SigningRequests/($guidRegex)"
      $getUrl -match $pattern | Out-Null
      $signingRequestId = $matches[1]
      Write-Verbose "Parsed signing request ID: $signingRequestId"
      return $signingRequestId
    }
  }
}

function CreateSubmitWithArtifactRequestFactory (
  [string] $url,
  [string] $artifactConfigurationId,
  [string] $signingPolicyId,
  [string] $projectSlug,
  [string] $artifactConfigurationSlug,
  [string] $signingPolicySlug,
  [string] $description,
  [string] $inputArtifactPath,
  [Hashtable] $origin,
  [Hashtable] $parameters
) {
  return CreateSubmitRequestFactory `
    -Url $url `
    -ArtifactConfigurationId $artifactConfigurationId `
    -SigningPolicyId $signingPolicyId `
    -ProjectSlug $projectSlug `
    -ArtifactConfigurationSlug $artifactConfigurationSlug `
    -SigningPolicySlug $signingPolicySlug `
    -Description $description `
    -Origin $origin `
    -Parameters $parameters `
    -AddAdditionalContent {
      Param ($content)

      $packageFileStream = New-Object System.IO.FileStream ($inputArtifactPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
      $streamContent = New-Object System.Net.Http.StreamContent $packageFileStream
      # PLANNED SIGN-988 This shouldn't be needed anymore
      $streamContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue "application/octet-stream"
      $fileName = [System.IO.Path]::GetFileName($inputArtifactPath)
      $content.Add($streamContent, "Artifact", $fileName)
      Write-Verbose "Artifact: $fileName"
    }
}

function CreateSubmitWithArtifactRetrievalLinkRequestFactory (
  [string] $url,
  [string] $artifactConfigurationId,
  [string] $signingPolicyId,
  [string] $projectSlug,
  [string] $artifactConfigurationSlug,
  [string] $signingPolicySlug,
  [string] $description,
  [string] $artifactRetrievalLink,
  [string] $artifactRetrievalLinkFileName,
  [string] $artifactRetrievalLinkSha256Hash,
  [HashTable] $artifactRetrievalLinkHttpHeaders,
  [Hashtable] $origin,
  [Hashtable] $parameters
) {
  return CreateSubmitRequestFactory `
    -Url $url `
    -ArtifactConfigurationId $artifactConfigurationId `
    -SigningPolicyId $signingPolicyId `
    -ProjectSlug $projectSlug `
    -ArtifactConfigurationSlug $artifactConfigurationSlug `
    -SigningPolicySlug $signingPolicySlug `
    -Description $description `
    -Origin $origin `
    -Parameters $parameters `
    -AddAdditionalContent {
      Param ($content, $addHashTableToHttpContent)

      $content.Add((New-Object System.Net.Http.StringContent $artifactRetrievalLink), "ArtifactRetrievalLink.Url")
      Write-Verbose "ArtifactRetrievalLink: $artifactRetrievalLink"

      $content.Add((New-Object System.Net.Http.StringContent $artifactRetrievalLinkFileName), "ArtifactRetrievalLink.FileName")
      Write-Verbose "ArtifactRetrievalLinkFileName: $artifactRetrievalLinkFileName"

      $content.Add((New-Object System.Net.Http.StringContent $artifactRetrievalLinkSha256Hash), "ArtifactRetrievalLink.Sha256Hash")
      Write-Verbose "ArtifactRetrievalLinkSha256Hash: $artifactRetrievalLinkSha256Hash"

      if ($artifactRetrievalLinkHttpHeaders) {
        & $addHashTableToHttpContent $content $artifactRetrievalLinkHttpHeaders "ArtifactRetrievalLink.HttpHeaders"
      }
    }
}

function CreateSubmitRequestFactory (
  [string] $url,
  [string] $artifactConfigurationId,
  [string] $signingPolicyId,
  [string] $projectSlug,
  [string] $artifactConfigurationSlug,
  [string] $signingPolicySlug,
  [string] $description,
  [Hashtable] $origin,
  [Hashtable] $parameters,
  [ScriptBlock] $addAdditionalContent) {
  $local:IsVerboseEnabled = $null -ne $PSCmdlet.MyInvocation.BoundParameters["Verbose"] -and $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent

  return {
    function AddHashTableToHttpContent ($content, $hashtable, $baseKey) {
      function AddToHttpContent ($content, $baseKey, $key, $value) {
        # All parameters ending in "File" are interpreted as streams
        if ( $key.ToLower().EndsWith("file")) {
          if ( $value.StartsWith("@")) {
            $filePath = $value.Substring(1)
            $packageFileStream = New-Object System.IO.FileStream ($filePath, [System.IO.FileMode]::Open)
            $streamContent = New-Object System.Net.Http.StreamContent $packageFileStream
            # PLANNED SIGN-988 This shouldn't be needed anymore
            $streamContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue "application/octet-stream"
            $fileName = [System.IO.Path]::GetFileName($filePath)
            $content.Add($streamContent, "$baseKey.$key", $fileName)
            Write-Verbose "Adding file content to origin: $baseKey.$key = $fileName"
          } else {
            # IDEA: We will later on support non-file parameter values here too, for now we throw
            throw "*File origin parameters must start with @ to indicate a file path"
          }
        } else {
          $stringContent = New-Object System.Net.Http.StringContent $value
          $content.Add($stringContent, "$baseKey.$key")
          Write-Verbose "Add normal content to origin: $baseKey.$key = $value"
        }
      }

      Write-Verbose "Recursive add base key: $baseKey"
      foreach ($kvp in $hashtable.GetEnumerator()) {
        if ($kvp.Value.GetType().Name -eq "Hashtable") {
          Write-Verbose "$($kvp.Key) is a Hashtable, enter next recursion level"
          AddHashTableToHttpContent $content $kvp.Value "$baseKey.$($kvp.Key)"
        }
        else {
          AddToHttpContent $content $baseKey $kvp.Key $kvp.Value
        }
      }
    }

    if ($IsVerboseEnabled) {
      $VerbosePreference = "Continue"
    }

    $content = New-Object System.Net.Http.MultipartFormDataContent

    try {
      if ($artifactConfigurationId) {
        Write-Verbose "ArtifactConfigurationId: $artifactConfigurationId"
        $artifactConfigurationIdContent = New-Object System.Net.Http.StringContent $artifactConfigurationId
        $content.Add($artifactConfigurationIdContent, "ArtifactConfigurationId")
      }

      if ($signingPolicyId) {
        Write-Verbose "SigningPolicyId: $signingPolicyId"
        $signingPolicyIdContent = New-Object System.Net.Http.StringContent $signingPolicyId
        $content.Add($signingPolicyIdContent, "SigningPolicyId")
      }

      if ($projectSlug) {
        Write-Verbose "ProjectSlug: $projectSlug"
        $projectSlugContent = New-Object System.Net.Http.StringContent $projectSlug
        $content.Add($projectSlugContent, "ProjectSlug")
      }

      if ($artifactConfigurationSlug) {
        Write-Verbose "ArtifactConfigurationSlug: $artifactConfigurationSlug"
        $artifactConfigurationSlugContent = New-Object System.Net.Http.StringContent $artifactConfigurationSlug
        $content.Add($artifactConfigurationSlugContent, "ArtifactConfigurationSlug")
      }

      if ($signingPolicySlug) {
        Write-Verbose "SigningPolicySlug: $signingPolicySlug"
        $signingPolicySlugContent = New-Object System.Net.Http.StringContent $signingPolicySlug
        $content.Add($signingPolicySlugContent, "SigningPolicySlug")
      }

      Write-Verbose "Description: $description"
      $descriptionContent = New-Object System.Net.Http.StringContent $description
      $content.Add($descriptionContent, "Description")

      $addHashTableToHttpContent = Get-Item Function:/AddHashTableToHttpContent

      . $addAdditionalContent $content $addHashTableToHttpContent

      if ($origin) {
        Write-Verbose "Adding all origin parameters..."
        AddHashTableToHttpContent $content $origin "Origin"
      }

      if ($parameters) {
        Write-Verbose "Adding all signing request parameters..."
        AddHashTableToHttpContent $content $parameters "Parameters"
      }

      $request = New-Object System.Net.Http.HttpRequestMessage Post, $url
      $request.Content = $content

      Write-Verbose "Request URL: $url"
      return $request
      # Only dispose the content in case of exceptions, otherwise the caller is responsible for disposing the whole request after it has been performed.
    } catch {
      $content.Dispose()
      throw
    }
  }.GetNewClosure()
}

function CreateResubmitRequestFactory (
  [string] $url,
  [string] $originalSigningRequestId,
  [string] $signingPolicySlug,
  [string] $description
) {
  $local:IsVerboseEnabled = $null -ne $PSCmdlet.MyInvocation.BoundParameters["Verbose"] -and $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent

  return {
    if ($IsVerboseEnabled) {
      $VerbosePreference = "Continue"
    }

    $content = New-Object System.Net.Http.MultipartFormDataContent

    try {

      Write-Verbose "OriginalSigningRequestId: $originalSigningRequestId"
      $originalSigningRequestIdContent = New-Object System.Net.Http.StringContent $originalSigningRequestId
      $content.Add($originalSigningRequestIdContent, "OriginalSigningRequestId")

      Write-Verbose "SigningPolicySlug: $signingPolicySlug"
      $signingPolicySlugContent = New-Object System.Net.Http.StringContent $signingPolicySlug
      $content.Add($signingPolicySlugContent, "SigningPolicySlug")

      Write-Verbose "Description: $description"
      $descriptionContent = New-Object System.Net.Http.StringContent $description
      $content.Add($descriptionContent, "Description")

      $request = New-Object System.Net.Http.HttpRequestMessage Post, $url
      $request.Content = $content

      Write-Verbose "Request URL: $url"
      return $request
      # Only dispose the content in case of exceptions, otherwise the caller is responsible for disposing the whole request after it has been performed.
    } catch {
      $content.Dispose()
      throw
    }
  }.GetNewClosure()
}

function GetWithRetry ([System.Net.Http.HttpClient] $httpClient, [string] $url, [int] $serviceUnavailableRetryTimeoutInSeconds) {
  $response = SendWithRetry `
    -HttpClient $httpClient `
    -RequestFactory { New-Object System.Net.Http.HttpRequestMessage Get, $url }.GetNewClosure() `
    -ServiceUnavailableRetryTimeoutInSeconds $serviceUnavailableRetryTimeoutInSeconds
  return $response
}

function SendWithRetry (
  [System.Net.Http.HttpClient] $httpClient,
  [ScriptBlock] $requestFactory,
  [int] $serviceUnavailableRetryTimeoutInSeconds) {

  # Implements rules from https://wiki.rubicon.eu/x/WxWZGg

  $sw = [System.Diagnostics.Stopwatch]::StartNew()
  $retry = 0

  while ($true) {
    $retryReason = $null

    if ($retry -gt 0) {
      Write-Host "Retry $retry..."
    }

    $request = $null
    try {
      Write-Verbose "Generating request..."
      $request = & $requestFactory

      $userAgentString = GetUserAgentString
      $request.Headers.Add("User-Agent", $userAgentString)

      Write-Verbose "HttpClient timeout: $($httpClient.Timeout)"
      Write-Verbose "Sending request..."
      $response = $httpClient.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()

      Write-Verbose "Response status code: $($response.StatusCode)"
      if ($response.StatusCode -eq 502 -or $response.StatusCode -eq 503) {
        $retryReason = "SignPath REST API is temporarily unavailable. Please try again in a few moments."
      } elseif ($response.StatusCode -eq 504) {
        $retryReason = "SignPath REST API answer time exceeded the timeout ($($HttpClient.Timeout))."
      } elseif ($response.StatusCode -eq 429) {
        $retryReason = "SignPath REST API encountered too many requests. Please try again in a few moments."
      } else {
        return $response
      }
    } catch [System.Net.Http.HttpRequestException] {
      Write-Verbose "Request failed with HttpRequestException"
      $retryReason = $_
    } catch [System.Threading.Tasks.TaskCanceledException] {
      Write-Verbose "Request failed with TaskCanceledException"
      $retryReason = "SignPath REST API answer time exceeded the timeout ($($HttpClient.Timeout))."
    } finally {
      Write-Verbose "Disposing request"
      if ($null -ne $request) {
        $request.Dispose()
      }
    }

    Write-Verbose "Retry reason: $retryReason"

    if (($sw.Elapsed.TotalSeconds + $serviceUnavailableRetryTimeoutInSeconds) -lt $ServiceUnavailableTimeoutInSeconds) {
      Write-Host "SignPath REST API call failed. Retrying in ${serviceUnavailableRetryTimeoutInSeconds}s..."
      Start-Sleep -Seconds $serviceUnavailableRetryTimeoutInSeconds
    } else {
      Write-Host "SignPath REST API could not be called successfully in $($retry + 1) tries. Aborting"
      throw $retryReason
    }

    $retry++
  }
}

function GetUserAgentString {
  $moduleName = "SignPath.PowerShellModule"
  $moduleVersion = $MyInvocation.MyCommand.ScriptBlock.Module.Version
  $operatingSystem = [System.Environment]::OSVersion.VersionString
  if ([System.Environment]::Is64BitProcess) { $architecture = "x64" } else { $architecture = "x86" }
  $powerShellVersion = "$($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)"

  return "${moduleName}/${moduleVersion} (${operatingSystem}; ${architecture}; ${powerShellVersion})"
}

function DownloadArtifact (
  [System.Net.Http.HttpClient] $httpClient,
  [string] $url,
  [string] $path,
  [int] $serviceUnavailableRetryTimeoutInSeconds) {

  $downloadResponse = $null
  $streamToWriteTo = $null
  try {
    Write-Host "Downloading signed artifact..."
    $downloadResponse = GetWithRetry `
      -HttpClient $httpClient `
      -Url $url `
      -ServiceUnavailableRetryTimeoutInSeconds $serviceUnavailableRetryTimeoutInSeconds
    CheckResponse $downloadResponse

    $pathWithoutFile = [System.IO.Path]::GetDirectoryName($path)
    [System.IO.Directory]::CreateDirectory($pathWithoutFile) | Out-Null

    $stream = $downloadResponse.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
    $streamToWriteTo = [System.IO.File]::Open($path, 'Create')
    $stream.CopyToAsync($streamToWriteTo).GetAwaiter().GetResult() | Out-Null
    Write-Host "Downloaded signed artifact and saved at '$path'"
  } finally {
    if ((Test-Path variable:downloadResponse) -and $null -ne $downloadResponse) {
      $downloadResponse.Dispose()
    }

    if ((Test-Path variable:stream) -and $null -ne $stream) {
      $stream.Dispose()
    }

    if ((Test-Path variable:streamToWriteTo) -and $null -ne $streamToWriteTo) {
      $streamToWriteTo.Dispose()
    }
  }
}

function WaitForCompletionAndRetrieveSignedArtifactDownloadLink (
  [System.Net.Http.HttpClient] $httpClient,
  [string] $url,
  [int] $waitForCompletionTimeoutInSeconds,
  [int] $waitForCompletionRetryTimeoutInSeconds,
  [int] $serviceUnavailableRetryTimeoutInSeconds) {

  $StatusComplete = "Completed"
  $StatusFailed = "Failed"
  $StatusDenied = "Denied"
  $StatusCanceled = "Canceled"
  $WorkflowStatusArtifactRetrievalFailed = "ArtifactRetrievalFailed"

  $getResponse = $null
  try {
    $resultJson = $null
    $status = $null
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    do {
      Write-Host "Checking status... " -NoNewline

      $getResponse = GetWithRetry -HttpClient $httpClient -Url $url -ServiceUnavailableRetryTimeoutInSeconds $serviceUnavailableRetryTimeoutInSeconds

      CheckResponse $getResponse
      $resultJson = $getResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult() | ConvertFrom-Json
      $status = $resultJson.status
      $workflowStatus = $resultJson.workflowStatus
      Write-Host $status

      if ($resultJson.isFinalStatus) {
        break
      }

      Write-Verbose "Waiting for $waitForCompletionRetryTimeoutInSeconds seconds until checking again..."
      Start-Sleep -Seconds $waitForCompletionRetryTimeoutInSeconds
    } while ($sw.Elapsed.TotalSeconds -lt $waitForCompletionTimeoutInSeconds)

    $timeoutExpired = $sw.Elapsed.TotalSeconds -ge $waitForCompletionTimeoutInSeconds
    if ($status -ne $StatusComplete) {
      if ($status -eq $StatusDenied) {
        throw "Terminating because signing request was denied"
      } elseif ($status -eq $StatusCanceled) {
        throw "Terminating because signing request was canceled"
      } elseif ($workflowStatus -eq $WorkflowStatusArtifactRetrievalFailed) {
        throw "Terminating because artifact retrieval failed"
      } elseif ($status -eq $StatusFailed) {
        throw "Terminating because signing request failed"
      } elseif ($timeoutExpired) {
        throw "Timeout expired while waiting for signing request to complete"
      } else {
        throw "Terminating because of unexpected signing request status: $status"
      }
    }

    if($resultJson.PSObject.Properties.Name -contains "signedArtifactLink") {
      return $resultJson.signedArtifactLink
    }

    throw "Downloading the signed artifact is not possible since the signing request's artifacts have been deleted."
  } finally {
    if ((Test-Path variable:getResponse) -and $null -ne $getResponse) {
      $getResponse.Dispose()
    }
  }
}

function CreateAndUseAuthorizedHttpClient ([string] $apiToken, [int] $timeout, [ScriptBlock] $scriptBlock, [System.Security.Cryptography.X509Certificates.X509Certificate2] $clientCertificate) {
  Add-Type -AssemblyName System.IO
  Add-Type -AssemblyName System.Net.Http

  $previousSecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol
  [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12

  $httpClientHandler = New-Object System.Net.Http.HttpClientHandler
  if ($null -ne $clientCertificate) {
    if (-not $clientCertificate.HasPrivateKey) {
      throw "The given client certificate has not private key and therefore cannot be used as client certificate."
    }

    Write-Verbose "Adding HttpClient client certificate: $clientCertificate"
    $httpClientHandler.ClientCertificates.Add($clientCertificate) | Out-Null
  }

  $httpClient = New-Object System.Net.Http.HttpClient $httpClientHandler
  $httpClient.Timeout = [TimeSpan]::FromSeconds($timeout)
  $httpClient.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue @("Bearer", $apiToken)

  try {
    & $scriptBlock $httpClient
  } finally {
    if ($null -ne $httpClient) {
      $httpClient.Dispose()
    }
    [System.Net.ServicePointManager]::SecurityProtocol = $previousSecurityProtocol
  }
}

function PrepareInputArtifactPath ([string] $inputArtifactPath) {
  $inputArtifactPath = NormalizePath $inputArtifactPath

  if (-not (Test-Path -Path $inputArtifactPath)) {
    throw "The input artifact path '$inputArtifactPath' does not exist"
  }
  return $inputArtifactPath
}

function PrepareOutputArtifactPath ([bool] $force, [string] $outputArtifactPath) {
  $outputArtifactPath = NormalizePath $outputArtifactPath

  if (-not $force -and (Test-Path -Path $outputArtifactPath)) {
    throw "There is already a file at '$outputArtifactPath'. If you want to overwrite it use the -Force switch"
  }
  return $outputArtifactPath
}

function NormalizePath ([string] $path) {
  if (-not [System.IO.Path]::IsPathRooted($path)) {
    return Join-Path $PWD $path
  }
  return $path
}

function CheckResponse ([System.Net.Http.HttpResponseMessage] $response, [switch] $expectLocationHeader) {
  Write-Verbose "Checking response: $response"
  if (-not $response.IsSuccessStatusCode) {
    Write-Verbose "No success response."
    $responseBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()

    $additionalReason = ""
    if (401 -eq $response.StatusCode) {
      $additionalReason = " Did you provide the correct API token?"
    }
    elseif(403 -eq $response.StatusCode -and -not $responseBody.Contains("feature is disabled")) {
      $additionalReason = " Did you add the user to the list of submitters in the specified signing policy? Did you provide the correct OrganizationId? In case you are using a trusted build system, did you link it to the specified project?"
    }

    $serverMessage = ""
    if ($responseBody -ne "") {
      $serverMessage = " (Server reported the following message: '" + $responseBody + "')"
    }

    $errorMessage = "Error {0} {1}.{2}{3}" -f $response.StatusCode.value__, $response.ReasonPhrase, $additionalReason, $serverMessage

    throw [System.Net.Http.HttpRequestException]$errorMessage
  }

  $hasValidLocationHeader = $response.Headers.PSObject.Properties.Name -contains "Location" -and $response.Headers.Location

  if ($expectLocationHeader.IsPresent -and -not $hasValidLocationHeader) {
    $errorMessage = "The server did not provide a location header in the response. Are you sure you are using the correct URL?"
    throw [System.Net.Http.HttpRequestException]$errorMessage
  }
}

Export-ModuleMember Submit-SigningRequest
Export-ModuleMember Get-SignedArtifact
Export-ModuleMember Get-CertificateByMicrosoftTemplateId
# SIG # Begin signature block
# MIIvZQYJKoZIhvcNAQcCoIIvVjCCL1ICAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB9PFhMSMoWq35o
# OyNVCETjtCLVeWqvtAwy/YRSJs1njqCCFDMwggWQMIIDeKADAgECAhAFmxtXno4h
# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z
# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z
# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ
# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s
# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL
# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb
# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3
# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c
# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx
# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0
# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL
# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud
# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf
# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk
# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS
# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK
# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB
# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp
# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg
# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri
# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7
# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5
# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3
# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H
# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G
# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C
# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce
# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da
# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T
# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA
# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh
# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM
# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z
# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05
# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY
# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP
# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN
# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry
# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL
# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf
# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh
# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh
# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV
# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j
# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH
# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC
# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l
# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW
# eE4wggfnMIIFz6ADAgECAhAFxsIaZbhUUbOgqnx1dNw2MA0GCSqGSIb3DQEBCwUA
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwHhcNMjIwODA0MDAwMDAwWhcNMjUwODA2MjM1OTU5WjCBwDET
# MBEGCysGAQQBgjc8AgEDEwJBVDEVMBMGCysGAQQBgjc8AgECEwRXaWVuMRUwEwYL
# KwYBBAGCNzwCAQETBFdpZW4xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9u
# MRAwDgYDVQQFEwc0NzU1MDZ6MQswCQYDVQQGEwJBVDENMAsGA1UEBxMEV2llbjEW
# MBQGA1UEChMNU2lnblBhdGggR21iSDEWMBQGA1UEAxMNU2lnblBhdGggR21iSDCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL6HS5wK+wIHHZ9uJUlOQn5O
# 7J443ConoGd9WICI2zmrzk/potZRcAoB0WYHu/dqK/U2IYvzDOzIFs4HjgGqJQDf
# Hx/75O3rUAv5owW53bmUjt9jrusvB0ObUNm3FrIWQYMFQJR0M3ymnGbXvDBs+hoW
# dhu12uYbmPJHm4pmsqV+Y8CBCWbJm3DyfGLD/qpOMkhjmh22h1X5G4m8+7EOWCup
# B1Fx1lmYpMu391x6ysVEe4g5E8Tus7VDeKB6aFoLiuVW3UtrucsmROY0iExrVC+h
# izGQqwFJn7gp8sxSzutZokfVXWf0dnw/1I2GIfLW5S9a9TpBz3Tz2Jh4BZs2lBXL
# lo/5+zlphlmMjiXvBoXquchQFko1DLSdXtEEeAQUVpeFsG60bDwj5X0R7UL/vZpw
# GD0s4XNUckeMwFFRTpHIxk2QbRSyZgS5Nfy5oILTtTity4IcnLf8aV4OLl7dgn39
# VfQ8hxYNfPMqn1NsbEBFuQ30maDGl8/4kCYMgLOnT19u+/wuBBCFmSSWa3Y5RyeJ
# 0LmAk3tvfoHqXR+wPVPekVwzViSQ/k7PYtq5zF4bMT/9dHpriE13absSZPfigSde
# /eyL7+kJZLYw+ZwZABwMyM/B3rLc7RJPSFCmqgyyFjc7i25iKv8Z//6884clbZ7W
# BYwLVTcIsfD/QG+0YoJlAgMBAAGjggIxMIICLTAfBgNVHSMEGDAWgBRoN+Drtjv4
# XxGG+/5hewiIZfROQjAdBgNVHQ4EFgQUxXD4yqLal5zX9MnW4OnSsnTYRH4wKgYD
# VR0RBCMwIaAfBggrBgEFBQcIA6ATMBEMD0FULVdJRU4tNDc1NTA2ejAOBgNVHQ8B
# Af8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGg
# T4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29k
# ZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9j
# cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNB
# NDA5NlNIQTM4NDIwMjFDQTEuY3JsMD0GA1UdIAQ2MDQwMgYFZ4EMAQMwKTAnBggr
# BgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcB
# AQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwG
# CCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAMBgNV
# HRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQCTJY3zlB6w17Y/7FHoW1Mh+vwz
# 3L0KfOWwqvRmPZm1/SQ9dbRVepLkc1jgSvrc8CwZI0J+SoeKaAmesLHt1onvaUW2
# /o/5P9X0HeHgue0jsE+QKKjU2RxomK4DrPdDdGVoCJiR5OJMmZzUUu3tGAr3MmTU
# 6b+UeDeWG0IjT+75LW0cOHgWqcE8aZLIuw/3YB6xPXs8AAayJysbmRe4jQNLg3u8
# DAErUY1yA5eYklThko1uIwJy90GYVwz6pWEifR1sV4sitzF0EV4UO82pgVxEhJB9
# eBZ3pHTPmjXEaRHQHZudGACW7+DDIEw8979qWDOJmiDEgL0ylb49ATOWW0kyGPnh
# 4tlbgi6QB8AAzMWgM+LENT8TPli1YrKmONq5ImgIBwOMtsSOwBH+TS1VvqBxdvF8
# YZA1+VTNeeTKMCkqJbCRm3M+d076nIGjddMISkloTeVeCjGT2Bt0vyJTfCcraPYK
# A6FDQndkNuzXurBmHazf7HLL/SxrLJGRMFo10BKVdC3Ihs6m9yNI/Bj0v/Rkykix
# sPvjJPJ4I9EjhrtI9Bz4CJujLosuqH3TYWfUqbi/F1W9pnJxzCBGX25+WMX8+8q5
# NLvQ4ykrhOrphjiM3hTfYVxd0W8lDD83qrbAnbdWNPq1Vcr9JMlNIDSGWRtoZXcL
# 6t/F7az0ldN5/Vv7DzGCGogwghqEAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV
# BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0
# IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQBcbCGmW4VFGz
# oKp8dXTcNjANBglghkgBZQMEAgEFAKCBmjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGC
# NwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAuBgorBgEEAYI3AgEM
# MSAwHqAcgBoAUwBpAGcAbgBQAGEAdABoAC4AcABzAG0AMTAvBgkqhkiG9w0BCQQx
# IgQgm8cRuItr+4yBUIDW+KBXO3//DeFHqe4omt6GiBLhMQEwDQYJKoZIhvcNAQEB
# BQAEggIAfhiwTLY2UqtgqyH5z2WatvCXd3vnJMv236sR50Ou5baGmY6blGQHHsZT
# GL6esMkHQhfF+QgC6ENpAsbZBIAUdGsTckQBktpICLf9MTFUCWpL9X0PWimjdVmW
# KpLFE913wI54bhvTkgBHbpcSm4rudJzOWSF8ippjwCTrP1gxEUlDESVrl+Sa2+U5
# t/q4QUfZ2kKEnq8EqzDG8A/hnr6kTOKCIW3sa0CcsL5KpgsO9AyhyHfkA+7jotG/
# eK9HYxna1uVrHjHrwXNt4S8znsFRhYp1rzfbpsH56k+grva3OfEzDBMXpGfi9WFX
# UjB6/81rUwoNFtQPL0yg7wa49xQVh5VcxNagmC2V7Zv1QNFYlZ3Smgf7exEqXRB/
# BEgR2ncipxPTT3kEDzH9Z41q/EO7Tne6ZlHqmDWqXL/Q550bmpttdp1BjYkahNbU
# P659gJvHOGIZKr03LHxXFTti5cf1Ko2wpSzeARBQSXeUwNXyvonK2ypplKLtosGX
# rBM/qX4SgOC4AyfnLUnMNTaSv1VgsMt+r0NAGokwoh5EzeohP26FFuXiyjBPkI78
# yX0UHIkOwdxq35a5CEaptCAMO0OrkN+UAc+AaL+e3qrM0d7Y0DNr6i/X8SPdkZc6
# vOpHfoBNP/ZDB15wuimCFbezpdlrvweLbjRMkZa91qchkHXd7Guhghc/MIIXOwYK
# KwYBBAGCNwMDATGCFyswghcnBgkqhkiG9w0BBwKgghcYMIIXFAIBAzEPMA0GCWCG
# SAFlAwQCAQUAMHcGCyqGSIb3DQEJEAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTAN
# BglghkgBZQMEAgEFAAQg0SU8WbXrNdAN25iZnCbbXyvPTI8KaxhKXqrE4giDYhcC
# EHENyawKWU2ySa09EXtv8lMYDzIwMjQwMTI1MTUwNzI3WqCCEwkwggbCMIIEqqAD
# AgECAhAFRK/zlJ0IOaa/2z9f5WEWMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYT
# AlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQg
# VHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjMw
# NzE0MDAwMDAwWhcNMzQxMDEzMjM1OTU5WjBIMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xIDAeBgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFtcCAy
# MDIzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAo1NFhx2DjlusPlSz
# I+DPn9fl0uddoQ4J3C9Io5d6OyqcZ9xiFVjBqZMRp82qsmrdECmKHmJjadNYnDVx
# vzqX65RQjxwg6seaOy+WZuNp52n+W8PWKyAcwZeUtKVQgfLPywemMGjKg0La/H8J
# JJSkghraarrYO8pd3hkYhftF6g1hbJ3+cV7EBpo88MUueQ8bZlLjyNY+X9pD04T1
# 0Mf2SC1eRXWWdf7dEKEbg8G45lKVtUfXeCk5a+B4WZfjRCtK1ZXO7wgX6oJkTf8j
# 48qG7rSkIWRw69XloNpjsy7pBe6q9iT1HbybHLK3X9/w7nZ9MZllR1WdSiQvrCuX
# vp/k/XtzPjLuUjT71Lvr1KAsNJvj3m5kGQc3AZEPHLVRzapMZoOIaGK7vEEbeBlt
# 5NkP4FhB+9ixLOFRr7StFQYU6mIIE9NpHnxkTZ0P387RXoyqq1AVybPKvNfEO2hE
# o6U7Qv1zfe7dCv95NBB+plwKWEwAPoVpdceDZNZ1zY8SdlalJPrXxGshuugfNJgv
# OuprAbD3+yqG7HtSOKmYCaFxsmxxrz64b5bV4RAT/mFHCoz+8LbH1cfebCTwv0KC
# yqBxPZySkwS0aXAnDU+3tTbRyV8IpHCj7ArxES5k4MsiK8rxKBMhSVF+BmbTO776
# 65E42FEHypS34lCh8zrTioPLQHsCAwEAAaOCAYswggGHMA4GA1UdDwEB/wQEAwIH
# gDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMCAGA1UdIAQZ
# MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATAfBgNVHSMEGDAWgBS6FtltTYUvcyl2
# mi91jGogj57IbzAdBgNVHQ4EFgQUpbbvE+fvzdBkodVWqWUxo97V40kwWgYDVR0f
# BFMwUTBPoE2gS4ZJaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFtcGluZ0NBLmNybDCBkAYIKwYBBQUH
# AQEEgYMwgYAwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBY
# BggrBgEFBQcwAoZMaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
# VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVTdGFtcGluZ0NBLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAgRrW3qCptZgXvHCNT4o8aJzYJf/LLOTN6l0ikuyMIgKpuM+A
# qNnn48XtJoKKcS8Y3U623mzX4WCcK+3tPUiOuGu6fF29wmE3aEl3o+uQqhLXJ4Xz
# jh6S2sJAOJ9dyKAuJXglnSoFeoQpmLZXeY/bJlYrsPOnvTcM2Jh2T1a5UsK2nTip
# gedtQVyMadG5K8TGe8+c+njikxp2oml101DkRBK+IA2eqUTQ+OVJdwhaIcW0z5iV
# GlS6ubzBaRm6zxbygzc0brBBJt3eWpdPM43UjXd9dUWhpVgmagNF3tlQtVCMr1a9
# TMXhRsUo063nQwBw3syYnhmJA+rUkTfvTVLzyWAhxFZH7doRS4wyw4jmWOK22z75
# X7BC1o/jF5HRqsBV44a/rCcsQdCaM0qoNtS5cpZ+l3k4SF/Kwtw9Mt911jZnWon4
# 9qfH5U81PAC9vpwqbHkB3NpE5jreODsHXjlY9HxzMVWggBHLFAx+rrz+pOt5Zapo
# 1iLKO+uagjVXKBbLafIymrLS2Dq4sUaGa7oX/cR3bBVsrquvczroSUa31X/MtjjA
# 2Owc9bahuEMs305MfR5ocMB3CtQC4Fxguyj/OOVSWtasFyIjTvTs0xf7UGv/B3cf
# cZdEQcm4RtNsMnxYL2dHZeUbc7aZ+WssBkbvQR7w8F/g29mtkIBEr4AQQYowggau
# MIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJ
# BgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k
# aWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAe
# Fw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcw
# FQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3Rl
# ZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3
# DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbSg9Ge
# TKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0
# hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXnHwZl
# jZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0VAsh
# aG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4fsbVY
# TXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40NjgHt1
# biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0QCir
# c0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+
# DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T/jnA
# +bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk42Pg
# puE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5rmQzS
# M7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU
# uhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6
# mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsG
# AQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t
# MEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl
# cnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAE
# GTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAH1Z
# jsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxpwc8d
# B+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIlzpVp
# P0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp8
# 76i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2
# nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3
# ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJshIUDQ
# txMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6OOmc
# 4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDwN7+Y
# AN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZ
# vAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQr
# H4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIFjTCCBHWgAwIBAgIQDpsY
# jvnQLefv21DiCEAYWjANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQGEwJVUzEVMBMG
# A1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw
# IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAxMDAw
# MDAwWhcNMzExMTA5MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln
# aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhE
# aWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEppz1Yq3aaza57
# G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o
# k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFh
# mzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463J
# T17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFw
# q1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yh
# Tzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU
# 75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LV
# jHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJ
# bOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8Qg
# UWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo4IB
# OjCCATYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6
# mK4cD08wHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/
# BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3Au
# ZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2Vy
# dC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4
# oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv
# b3RDQS5jcmwwEQYDVR0gBAowCDAGBgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBw
# oL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyhhyzshV6pGrsi+IcaaVQi7aSId229GhT0
# E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtD
# IeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlU
# sLihVo7spNU96LHc/RzY9HdaXFSMb++hUD38dglohJ9vytsgjTVgHAIDyyCwrFig
# DkBjxZgiwbJZ9VVrzyerbHbObyMt9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwY
# w02fc7cBqZ9Xql4o4rmUMYIDdjCCA3ICAQEwdzBjMQswCQYDVQQGEwJVUzEXMBUG
# A1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQg
# RzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAFRK/zlJ0IOaa/2z9f
# 5WEWMA0GCWCGSAFlAwQCAQUAoIHRMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRAB
# BDAcBgkqhkiG9w0BCQUxDxcNMjQwMTI1MTUwNzI3WjArBgsqhkiG9w0BCRACDDEc
# MBowGDAWBBRm8CsywsLJD4JdzqqKycZPGZzPQDAvBgkqhkiG9w0BCQQxIgQgAJNg
# bYrr8I6pfylHJShFnLzQd+YzfwNwSfOAXI3iaPAwNwYLKoZIhvcNAQkQAi8xKDAm
# MCQwIgQg0vbkbe10IszR1EBXaEE2b4KK2lWarjMWr00amtQMeCgwDQYJKoZIhvcN
# AQEBBQAEggIAmN7Zyynlq+uQwpvlvt+FieNgpXlPoJt4lUtDTA6Sa/gLg+hS9EBo
# FGQY12bC7K4Oxuj2CDphyz9GzYj3WGp3J0lXpnZkdH46cX/okNKb7tIyThP7d5Nx
# yCrXFb6qKO57E0xJ64Z9glSH8mfQEvJMIBefxsN9BFSXNb/2CaQJRMO1M6fFFr1A
# GWdkzq+nSY3YbME7Gu9ID31b61rTtHaMLIgAnAlezUX4O3r8ghGCyRX8awiiHKN4
# BntUGiXJU2jSkZh1fR/srTqLEfj46BGN+qXwd/s8N9kJbbGVjjZiiivz6ZSNZevn
# X/qDki2oIdjRyII079E0JnpvR2hAH/LvnvUzREQJua13YfPOuaVJA5txpBsnzRDS
# G44D8fuQrBgHETzIOmGx5T+6L6Jv3L9iWVUmoe7y0kwlD7T+gpn9n3cbXXRTRt3y
# HSh8EU5CB005e1jZuRffs21sngvngZHQWvEN5eyGtn7jzQgQI6STMHRkH0N0Drjl
# 8E13Y3Bc/c3guj0zLbEGYL8Mmuadvbp6FQUNhLl1AVIouIWzAulDVnr4lOaAM+AI
# sNnhqc4yZzkhtHID08bkqoS4X44maD+SVoUDau/lPX5e76FcnJAbas4wEs+ZCryg
# of2NlwdPs43zjJVLQwHjECkh0NRXaz7H5c+xFMW1HcQfanf0WhKXyNQ=
# SIG # End signature block