mirror of
https://github.com/Grace-Solutions/Invoke-HTTPBootBiosConfiguration.git
synced 2026-09-11 21:39:32 +00:00
feat: Endpoint certificate chain retrieval and hardware-verified profile constraints
- Add Get-EndpointCertificateChain toolkit function: direct synchronous TLS handshake (TcpClient/SslStream) against the boot endpoint, captures the presented chain within the X509 validation callback (untrusted/expired/self-signed endpoints still harvest), converts each certificate to PEM (64 character wrapping), exports the full chain to a PEM file, and selects the last self-signed certificate as the root. Verified within both Windows PowerShell 5.1 and PowerShell 7 (isolated closure scope works around the 5.1 GetNewClosure validated-parameter clone issue) - Main script: when RootCertificateURL is not explicitly specified and the boot URL is https, the root certificate is retrieved directly from the boot endpoint with graceful fallback to downloading RootCertificateURL. Explicitly specifying the parameter skips the endpoint retrieval - Warn (but still export/embed) when a certificate is not RSA - Dell BIOS certificate import requires RSA and rejects ECDSA with "not RSA format" - Hardware-verified profile constraints (CCTK 5.2.2, live --Set/--Get/--Delete cycles): the cert field is capped at 2047 characters so only a single root certificate can be embedded (chain bundles are rejected with exit 150), and IntegrityInfo with a NON-EMPTY digest is mandatory (empty digest or missing element is rejected with exit 157) - Restore boot image digest computation by default (required by the BIOS) and add -BootImageDigest parameter to place a precomputed SHA-256 value without downloading the boot image - Warn when the certificate content exceeds the 2047 character BIOS field limit - Docs: hardware-verified field constraints section, updated Digest/cert element notes, exit code 157 and the 150 field-validation variant, boot image digest lifecycle guidance Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ The script writes `<StagingDirectory>\HttpBootProfile.xml` (UTF-8, no BOM):
|
||||
</UrlInfo>
|
||||
<IntegrityInfo>
|
||||
<Algorithm>sha256</Algorithm>
|
||||
<Digest />
|
||||
<Digest>aca902cd0efc5d357e2400db6fd4a760799061147147906a61a2ca0bb47ffea6</Digest>
|
||||
<SignValue />
|
||||
</IntegrityInfo>
|
||||
</HttpBootProfile>
|
||||
@@ -28,6 +28,11 @@ The script writes `<StagingDirectory>\HttpBootProfile.xml` (UTF-8, no BOM):
|
||||
|
||||
The document is built with an `XmlDocument` and written through an `XmlWriter` (two-space indentation, UTF-8 without BOM).
|
||||
|
||||
### Hardware verified field constraints (CCTK 5.2.2, applied via `--Set` on a live BIOS)
|
||||
|
||||
- **The `cert` field accepts a maximum of 2047 characters.** Only a single certificate fits (a 4096-bit RSA root like ISRG Root X1 is ~1,940 characters). Embedding a chain bundle (e.g. intermediates + root, "everything except the leaf") is rejected with exit 150 and the message `field certificate max allowed characters are 2047`. This is why the script embeds only the root.
|
||||
- **`IntegrityInfo` and a non-empty `Digest` are mandatory.** A profile with an empty `<Digest />` or without the `IntegrityInfo` element is rejected with exit 157, `some or all fields missing`.
|
||||
|
||||
Element notes (informed by the `BiosConnectProfile.xsd` schema that ships inside Dell Command | Configure):
|
||||
|
||||
| Element | Notes |
|
||||
@@ -35,9 +40,9 @@ Element notes (informed by the `BiosConnectProfile.xsd` schema that ships inside
|
||||
| `UrlInfo@Type` | `https` or `http`, derived from the boot URL scheme |
|
||||
| `Url` | The full boot image URL |
|
||||
| `CertInfo` | Optional per the schema. Only emitted for `https` URLs. `Type="pem"` is the only supported certificate encoding |
|
||||
| `cert` | The PEM encoded CA **root** certificate of the chain that signs the boot server's TLS certificate. For Let's Encrypt this is **ISRG Root X1** - not the leaf and not the R10/R11 intermediates |
|
||||
| `IntegrityInfo` | **Required** by the schema (it cannot be omitted). `Algorithm` supports `sha1` and `sha256` |
|
||||
| `Digest` | Intentionally left empty (the schema allows an empty value). This script places configuration values only - it does not download or hash the boot image, so the boot image on the web server can change freely without invalidating the profile |
|
||||
| `cert` | The PEM encoded CA **root** certificate of the chain that signs the boot server's TLS certificate. For Let's Encrypt this is **ISRG Root X1** - not the leaf and not the R10/R11 intermediates. By default the script retrieves the chain directly from the boot endpoint (`Get-EndpointCertificateChain`) and embeds its self-signed root; the full endpoint chain is also exported to `BootEndpointCertificateChain.pem` within the staging directory. **The BIOS requires an RSA certificate** - importing an ECDSA certificate fails with a "not RSA format" error, so the script warns (but still embeds) when the root is not RSA |
|
||||
| `IntegrityInfo` | **Required** - by the schema and by the BIOS (hardware verified: omitting it fails with exit 157). `Algorithm` supports `sha1` and `sha256` |
|
||||
| `Digest` | **Required non-empty** (hardware verified: an empty value fails with exit 157). Computed from the downloaded boot image by default, or placed directly via `-BootImageDigest`. The BIOS enforces it against the boot image at boot time: if the file on the web server changes (e.g. a DeployR/2PXE upgrade replaces `snponly_x64.efi`), HTTP boot fails until the profile is re-applied with the new digest |
|
||||
| `SignValue` | Left empty (unsigned profile) |
|
||||
|
||||
## CCTK command sequence
|
||||
@@ -66,13 +71,15 @@ CCTK exit codes are documented by Dell at <https://www.dell.com/support/kbdoc/en
|
||||
| Exit code | Meaning | Script handling |
|
||||
| --- | --- | --- |
|
||||
| 0 | Success | Accepted everywhere |
|
||||
| 150 | Profile Not Present | Accepted for `HttpBootProfile --Delete` and returned by `--Get` when nothing is configured (verified empirically on 5.2.2) |
|
||||
| 150 | Profile Not Present / profile field validation errors | Accepted for `HttpBootProfile --Delete` and returned by `--Get` when nothing is configured. Also returned by `--Set` for field limit violations such as `field certificate max allowed characters are 2047` (all verified empirically on 5.2.2) |
|
||||
| 157 | Some or all fields missing | Returned by `--Set` when the profile lacks a required field, e.g. an empty or missing `Digest`/`IntegrityInfo` (verified empirically on 5.2.2) |
|
||||
| 240-246 | Password related errors (wrong/required setup password) | Fails the script - check `-SetupPassword`. Note the asymmetry: a device WITH a setup password fails without the correct `-SetupPassword`, but supplying `-SetupPassword` on a device WITHOUT one is harmless - CCTK ignores `--ValSetupPwd` when no setup password is installed (verified on 5.2.2), so one fleet-wide command line works both ways |
|
||||
| 119 | Setting not supported on this platform | Fails the script - the model may not support HTTPS boot |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Profile applies but device does not boot**: confirm the boot URL actually serves the boot image (e.g. `snponly_x64.efi`), and confirm the certificate in the profile is the root of the server's actual chain (`openssl s_client -connect server:443 -showcerts`).
|
||||
- **Profile applies but device does not boot**: confirm the boot URL actually serves the boot image (e.g. `snponly_x64.efi`), and confirm the certificate in the profile is the root of the server's actual chain (`openssl s_client -connect server:443 -showcerts`). With the default endpoint retrieval this matches automatically; compare against `BootEndpointCertificateChain.pem` in the staging directory.
|
||||
- **`--Set` fails with a "not RSA format" error**: the embedded certificate is not RSA (Dell BIOS certificate import only accepts RSA). The script logs a warning when this is the case. Re-issue the boot server's certificate from an RSA chain (for Let's Encrypt, prefer the RSA/ISRG Root X1 chain over the ECDSA/ISRG Root X2 chain), or point `-RootCertificateURL` at an RSA root the server chain also validates against.
|
||||
- **URL change did not take effect**: this is the delete-before-set issue above - re-run without `-SkipProfileDeletion`, or as a last resort clear it in the BIOS setup UI.
|
||||
- **`--Set` returns a schema/parse error**: inspect the generated `HttpBootProfile.xml` in the staging directory; the full document is also written into the script log.
|
||||
- **Certificate expiry**: the script logs the downloaded root certificate's subject, thumbprint, and expiration, and warns when it is expired. ISRG Root X1 is valid until 2035-06-04.
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
For Dell devices, the following operations are then performed by using CCTK:
|
||||
|
||||
1. Locates cctk.exe from the staged toolkit tools directory (architecture specific first), the process path, or the standard installation directories.
|
||||
2. Downloads the certificate authority root certificate (the Let's Encrypt "ISRG Root X1" certificate by default) so that the BIOS can validate the TLS certificate presented by the HTTP(s) boot server.
|
||||
3. Generates the HttpBootProfile XML document containing the boot URL, the root certificate, and the integrity information section. The digest is intentionally left empty - this script places configuration values only and does not download or hash the boot image.
|
||||
4. Executes the CCTK commands to enable HTTPS boot, set the HTTPS boot mode to manual, delete any existing HTTP boot profile (this avoids a known issue where updating the URL within an existing profile does not always apply), apply the new profile, and read the profile back to verify that the configured URL was applied.
|
||||
2. Determines the certificate authority root certificate that the BIOS uses to validate the TLS certificate presented by the HTTP(s) boot server. When a root certificate URL was not explicitly specified, the certificate chain is retrieved directly from the boot endpoint by using the Get-EndpointCertificateChain toolkit function (the full chain is exported to PEM format within the staging directory, and the root of the chain is embedded within the profile). When the endpoint retrieval fails, the root certificate is downloaded from the RootCertificateURL as a graceful fallback (the Let's Encrypt "ISRG Root X1" certificate by default).
|
||||
3. Determines the boot image digest. The BIOS requires a non-empty digest value within the profile integrity information (verified on hardware - an empty digest is rejected with "some or all fields missing"), so the SHA-256 digest of the boot image is computed by downloading it from the boot URL, unless a precomputed value is supplied by using the BootImageDigest parameter.
|
||||
4. Generates the HttpBootProfile XML document containing the boot URL, the root certificate, and the integrity information section.
|
||||
5. Executes the CCTK commands to enable HTTPS boot, set the HTTPS boot mode to manual, delete any existing HTTP boot profile (this avoids a known issue where updating the URL within an existing profile does not always apply), apply the new profile, and read the profile back to verify that the configured URL was applied.
|
||||
|
||||
All downloads automatically honor the proxy configuration of the environment. The proxy configuration of the current user (a static WinINET proxy or an automatic configuration script) is preferred, followed by the machine WinHTTP proxy configuration (as set by "netsh winhttp set proxy"), and no proxy is used when neither is configured. Default credentials are supplied to authenticating proxies.
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
Required. The fully qualified HTTP(s) URL of the UEFI boot image the BIOS will boot from. Example: https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi. When the URL does not end with a file name (for example https://prod.ipxe.example.com/2PXE/boot/x64, with or without a trailing slash), the default boot image file name of "snponly_x64.efi" is appended automatically. Alias: URL, BURL.
|
||||
|
||||
.PARAMETER RootCertificateURL
|
||||
Optional. The URL of the PEM encoded certificate authority root certificate that the BIOS uses to validate the TLS certificate presented by the HTTP(s) boot server. Defaults to the Let's Encrypt "ISRG Root X1" root certificate at https://letsencrypt.org/certs/isrgrootx1.pem. Alias: RCURL.
|
||||
Optional. The URL of the PEM encoded certificate authority root certificate that the BIOS uses to validate the TLS certificate presented by the HTTP(s) boot server. When this parameter is NOT explicitly specified, the certificate chain is retrieved directly from the boot endpoint instead, and this URL (the Let's Encrypt "ISRG Root X1" root certificate at https://letsencrypt.org/certs/isrgrootx1.pem by default) is only used as a graceful fallback when the endpoint retrieval fails. Explicitly specifying this parameter skips the endpoint retrieval entirely. Alias: RCURL.
|
||||
|
||||
.PARAMETER CCTKDownloadURL
|
||||
Optional. The URL that the Dell Command | Configure content is downloaded from when cctk.exe cannot be located on the device. Supports a Dell Update Package executable (.exe), a ZIP archive (.zip), or a 7-Zip archive (.7z) containing a previously extracted portable "Command Configure" folder. Defaults to the Dell Command | Configure version 5.2.2 Dell Update Package hosted at dl.dell.com. Alias: CCTKURL.
|
||||
@@ -35,6 +36,9 @@
|
||||
.PARAMETER StagingDirectory
|
||||
Optional. The directory that downloaded and extracted content is staged within. Keep this path short, because the MSI administrative extraction can fail with "path too long" errors when the staging path is deep. Defaults to "$($Env:Windir)\Temp\HTTPBootBios". Alias: SD.
|
||||
|
||||
.PARAMETER BootImageDigest
|
||||
Optional. A precomputed SHA-256 digest (64 hexadecimal characters) of the boot image, placed directly into the profile integrity information without downloading the boot image. When not specified, the boot image is downloaded from the boot URL and its digest is computed. The BIOS requires a non-empty digest value, and enforces it against the downloaded boot image at boot time - a stale digest stops the device from HTTP booting until the profile is re-applied. Alias: BID, Digest.
|
||||
|
||||
.PARAMETER SetupPassword
|
||||
Optional. The BIOS setup (administrator) password. When specified, it is appended to each BIOS modification command by using the --ValSetupPwd argument, and the process command lines are obfuscated within the log. This parameter is safe to supply fleet wide: CCTK ignores the --ValSetupPwd argument on devices where no setup password is installed (verified on version 5.2.2), so the same command line works on both password protected and unprotected devices. When this parameter is omitted, the argument is not appended at all, which also works on unprotected devices. Alias: BIOSPassword, SP.
|
||||
|
||||
@@ -71,6 +75,10 @@
|
||||
.NOTES
|
||||
All Dell Command | Configure exit codes other than 0 indicate an error. Exit code 150 means "Profile Not Present" and is accepted for the profile deletion command, because a device that has never been configured will not have an existing profile.
|
||||
|
||||
Dell BIOS HTTP boot profile certificate import requires RSA certificates. When the certificate that will be embedded does not use an RSA public key (for example ECDSA), a warning is logged and the certificate is still exported and embedded, however the BIOS may reject the profile with a "not RSA format" error.
|
||||
|
||||
The following HTTP boot profile constraints were verified on hardware (CCTK 5.2.2): the certificate field accepts a maximum of 2047 characters, so only a single certificate (the root) can be embedded - a chain bundle does not fit and is rejected with exit code 150 ("field certificate max allowed characters are 2047"). The IntegrityInfo element and a non-empty digest value are mandatory - profiles without them are rejected with exit code 157 ("some or all fields missing").
|
||||
|
||||
The default Dell Command | Configure download details (version 5.2.2 A00, released 2026-03-31) were retrieved from the Dell support site (driver ID F2V9N) and validated end to end.
|
||||
|
||||
The BIOS setup password, when supplied, is passed to cctk.exe on its command line. The command lines are obfuscated within the script log, however any process auditing solution on the device may still record them.
|
||||
@@ -113,6 +121,11 @@
|
||||
[Alias('SD')]
|
||||
[System.IO.DirectoryInfo]$StagingDirectory,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('BID', 'Digest')]
|
||||
[String]$BootImageDigest,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('BIOSPassword', 'SP')]
|
||||
@@ -690,45 +703,92 @@ Switch (Test-ProcessElevationStatus)
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Download the certificate authority root certificate (Only required for HTTPS boot URLs)
|
||||
#region Determine the certificate authority root certificate (Only required for HTTPS boot URLs)
|
||||
[String]$RootCertificateContent = [System.String]::Empty
|
||||
|
||||
Switch ($BootURL.Scheme -ieq 'https')
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
[System.IO.FileInfo]$RootCertificatePath = [System.IO.Path]::Combine("$($StagingDirectory.FullName)", 'RootCertificate.pem')
|
||||
#region Retrieve the certificate chain directly from the boot endpoint (When a root certificate URL was not explicitly specified)
|
||||
Switch ($PSBoundParameters.ContainsKey('RootCertificateURL'))
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("A root certificate URL was not explicitly specified. Attempting to retrieve the certificate chain directly from the boot endpoint. Please Wait..."))
|
||||
|
||||
$Null = $DownloadFile.InvokeReturnAsIs($RootCertificateURL, $RootCertificatePath)
|
||||
#The endpoint probe is always made directly (never through a proxy), because the BIOS HTTP(s) boot feature contacts the endpoint directly as well.
|
||||
$GetEndpointCertificateChainParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$GetEndpointCertificateChainParameters.URL = $BootURL
|
||||
$GetEndpointCertificateChainParameters.ExportPath = [System.IO.FileInfo][System.IO.Path]::Combine("$($StagingDirectory.FullName)", 'BootEndpointCertificateChain.pem')
|
||||
$GetEndpointCertificateChainParameters.ContinueOnError = $True
|
||||
$GetEndpointCertificateChainParameters.Verbose = $True
|
||||
|
||||
[String]$RootCertificateContent = [System.IO.File]::ReadAllText($RootCertificatePath.FullName).Trim()
|
||||
$GetEndpointCertificateChainResult = Get-EndpointCertificateChain @GetEndpointCertificateChainParameters
|
||||
|
||||
Switch ($RootCertificateContent -imatch '(?s)(^.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----.*$)')
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
Throw "The content downloaded from `"$($RootCertificateURL.AbsoluteUri)`" does not appear to be a PEM encoded certificate."
|
||||
}
|
||||
}
|
||||
Switch (($GetEndpointCertificateChainResult.Succeeded -eq $True) -and ($Null -ine $GetEndpointCertificateChainResult.RootCertificate))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
[String]$RootCertificateContent = "$($GetEndpointCertificateChainResult.RootCertificate.PEMContent)".Trim()
|
||||
|
||||
Try
|
||||
{
|
||||
$RootCertificateObject = New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList @("$($RootCertificatePath.FullName)")
|
||||
$WriteLogMessage.Invoke(0, @("The root certificate retrieved from the boot endpoint will be embedded within the HTTP boot profile.", "Subject: $($GetEndpointCertificateChainResult.RootCertificate.Subject)", "Key Algorithm: $($GetEndpointCertificateChainResult.RootCertificate.KeyAlgorithm)", "Thumbprint: $($GetEndpointCertificateChainResult.RootCertificate.Thumbprint)"))
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Root Certificate Subject: $($RootCertificateObject.Subject)", "Root Certificate Thumbprint: $($RootCertificateObject.Thumbprint)", "Root Certificate Expiration: $($RootCertificateObject.NotAfter.ToString('o'))"))
|
||||
Default
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The certificate chain could not be retrieved from the boot endpoint. Falling back to downloading the root certificate from `"$($RootCertificateURL.AbsoluteUri)`"."))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
Switch ($RootCertificateObject.NotAfter -lt (Get-Date))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The root certificate downloaded from `"$($RootCertificateURL.AbsoluteUri)`" has expired. The BIOS may not be able to validate the HTTP(s) boot server."))
|
||||
}
|
||||
}
|
||||
}
|
||||
Catch
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The downloaded root certificate could not be parsed for informational logging purposes. The certificate content will still be embedded within the HTTP boot profile.", "Message: $($_.Exception.Message)"))
|
||||
}
|
||||
#region Download the root certificate (When it was not retrieved from the boot endpoint)
|
||||
Switch (([System.String]::IsNullOrEmpty($RootCertificateContent) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($RootCertificateContent) -eq $True))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
[System.IO.FileInfo]$RootCertificatePath = [System.IO.Path]::Combine("$($StagingDirectory.FullName)", 'RootCertificate.pem')
|
||||
|
||||
$Null = $DownloadFile.InvokeReturnAsIs($RootCertificateURL, $RootCertificatePath)
|
||||
|
||||
[String]$RootCertificateContent = [System.IO.File]::ReadAllText($RootCertificatePath.FullName).Trim()
|
||||
|
||||
Switch ($RootCertificateContent -imatch '(?s)(^.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----.*$)')
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
Throw "The content downloaded from `"$($RootCertificateURL.AbsoluteUri)`" does not appear to be a PEM encoded certificate."
|
||||
}
|
||||
}
|
||||
|
||||
Try
|
||||
{
|
||||
$RootCertificateObject = New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList @("$($RootCertificatePath.FullName)")
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Root Certificate Subject: $($RootCertificateObject.Subject)", "Root Certificate Thumbprint: $($RootCertificateObject.Thumbprint)", "Root Certificate Expiration: $($RootCertificateObject.NotAfter.ToString('o'))"))
|
||||
|
||||
Switch ($True)
|
||||
{
|
||||
{($RootCertificateObject.PublicKey.Oid.Value -ine '1.2.840.113549.1.1.1')}
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The root certificate downloaded from `"$($RootCertificateURL.AbsoluteUri)`" does not use an RSA public key. Dell BIOS HTTP boot profile certificate import requires RSA certificates, so the BIOS may reject this certificate with a `"not RSA format`" error. The certificate will still be embedded."))
|
||||
}
|
||||
|
||||
{($RootCertificateObject.NotAfter -lt (Get-Date))}
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The root certificate downloaded from `"$($RootCertificateURL.AbsoluteUri)`" has expired. The BIOS may not be able to validate the HTTP(s) boot server."))
|
||||
}
|
||||
}
|
||||
}
|
||||
Catch
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The downloaded root certificate could not be parsed for informational logging purposes. The certificate content will still be embedded within the HTTP boot profile.", "Message: $($_.Exception.Message)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
Default
|
||||
@@ -738,8 +798,51 @@ Switch (Test-ProcessElevationStatus)
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Determine the boot image digest (The BIOS requires a non-empty digest value)
|
||||
#Verified on hardware: applying a profile with an empty digest, or without the IntegrityInfo element entirely, fails with CCTK exit code 157 ("some or all fields missing"). A digest value is therefore mandatory. It is computed from the boot image by default, or placed directly when the BootImageDigest parameter is specified.
|
||||
Switch (([System.String]::IsNullOrEmpty($BootImageDigest) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($BootImageDigest) -eq $False))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
[String]$BootImageDigest = $BootImageDigest.Trim().ToLower()
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("The specified boot image digest will be placed within the HTTP boot profile without downloading the boot image. [Digest: $($BootImageDigest)]"))
|
||||
|
||||
Switch ($BootImageDigest -inotmatch '(^[0-9a-f]{64}$)')
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The specified boot image digest does not appear to be a valid SHA-256 value (64 hexadecimal characters). The BIOS may reject the profile."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
[String]$BootImageFileName = [System.IO.Path]::GetFileName($BootURL.LocalPath)
|
||||
|
||||
[System.IO.FileInfo]$BootImagePath = [System.IO.Path]::Combine("$($StagingDirectory.FullName)", $BootImageFileName)
|
||||
|
||||
$Null = $DownloadFile.InvokeReturnAsIs($BootURL, $BootImagePath)
|
||||
|
||||
[String]$BootImageDigest = (Get-FileHash -Path ($BootImagePath.FullName) -Algorithm SHA256).Hash.ToLower()
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Boot Image Digest (SHA-256): $($BootImageDigest)"))
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Generate the HTTP boot profile document
|
||||
#The document is built with an XmlDocument and written through an XmlWriter. The digest within the integrity information is intentionally left empty - this script places configuration values only and does not download or hash the boot image. The profile schema requires the IntegrityInfo element but allows an empty digest value.
|
||||
#The document is built with an XmlDocument and written through an XmlWriter.
|
||||
#Verified on hardware: the BIOS certificate field accepts a maximum of 2047 characters, so only a single certificate (the root) can be embedded - a multiple certificate bundle does not fit and is rejected by CCTK with "field certificate max allowed characters are 2047".
|
||||
Switch (([System.String]::IsNullOrWhiteSpace($RootCertificateContent) -eq $False) -and ($RootCertificateContent.Length -gt 2047))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The certificate content is $($RootCertificateContent.Length) characters long, which exceeds the BIOS certificate field maximum of 2047 characters. The profile application will likely fail."))
|
||||
}
|
||||
}
|
||||
|
||||
[System.IO.FileInfo]$HttpBootProfilePath = [System.IO.Path]::Combine("$($StagingDirectory.FullName)", 'HttpBootProfile.xml')
|
||||
|
||||
$HttpBootProfileDocument = New-Object -TypeName 'System.Xml.XmlDocument'
|
||||
@@ -787,6 +890,7 @@ Switch (Test-ProcessElevationStatus)
|
||||
$Null = $IntegrityInfoElement.AppendChild($AlgorithmElement)
|
||||
|
||||
$DigestElement = $HttpBootProfileDocument.CreateElement('Digest')
|
||||
$DigestElement.InnerText = "$($BootImageDigest)"
|
||||
|
||||
$Null = $IntegrityInfoElement.AppendChild($DigestElement)
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ A manufacturer switch statement selects the configuration method per device vend
|
||||
Then, for Dell devices:
|
||||
|
||||
1. **Locate CCTK** - the staged `Toolkit\Tools\<architecture>\CCTK` folder first (the toolkit resolves `X86`, `X64`, or `ARM64` automatically), then the process path, then the standard installation directories.
|
||||
2. **Download the root certificate** - the Let's Encrypt "ISRG Root X1" PEM by default, so the BIOS can validate the TLS certificate presented by the boot server.
|
||||
3. **Generate the HTTP boot profile XML** - built with an XmlDocument and written through an XmlWriter. The integrity digest is intentionally left empty: the script places configuration values only and does not download or hash the boot image. See [Docs/HttpBootProfile-Reference.md](Docs/HttpBootProfile-Reference.md) for the document format.
|
||||
4. **Apply the BIOS configuration** by executing the following CCTK commands in order:
|
||||
2. **Determine the root certificate** - when `-RootCertificateURL` is not explicitly specified, the `Get-EndpointCertificateChain` toolkit function performs a direct TLS handshake against the boot endpoint (exactly what the firmware will do - no proxy), captures the presented certificate chain, exports it to PEM (the full chain is written to the staging directory), and embeds the chain's self-signed root into the profile. If the endpoint cannot be reached, it gracefully falls back to downloading `-RootCertificateURL` (Let's Encrypt "ISRG Root X1" by default). Dell BIOS certificate import requires RSA - non-RSA (e.g. ECDSA) certificates are still exported and embedded, with a warning.
|
||||
3. **Determine the boot image digest** - the BIOS requires a non-empty SHA-256 digest in the profile (hardware verified: an empty digest is rejected with exit 157 "some or all fields missing"), so the boot image is downloaded and hashed - unless a precomputed value is supplied with `-BootImageDigest`, which places the value without downloading anything.
|
||||
4. **Generate the HTTP boot profile XML** - built with an XmlDocument and written through an XmlWriter. See [Docs/HttpBootProfile-Reference.md](Docs/HttpBootProfile-Reference.md) for the document format and the hardware verified field constraints (single certificate only - the BIOS cert field is capped at 2047 characters, so a chain bundle cannot be embedded).
|
||||
5. **Apply the BIOS configuration** by executing the following CCTK commands in order:
|
||||
|
||||
| # | Command | Notes |
|
||||
| --- | --- | --- |
|
||||
@@ -54,10 +55,11 @@ powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File ".\Invoke-HTTPBo
|
||||
| Parameter | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `-BootURL` | (required) | Fully qualified HTTP(s) URL of the UEFI boot image, e.g. `https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi`. When the URL does not end with a file name, `snponly_x64.efi` is appended automatically |
|
||||
| `-RootCertificateURL` | Let's Encrypt ISRG Root X1 (`https://letsencrypt.org/certs/isrgrootx1.pem`) | PEM encoded CA root certificate the BIOS uses to validate the boot server TLS certificate |
|
||||
| `-RootCertificateURL` | Let's Encrypt ISRG Root X1 (`https://letsencrypt.org/certs/isrgrootx1.pem`) | PEM encoded CA root certificate the BIOS uses to validate the boot server TLS certificate. When NOT explicitly specified, the chain is fetched directly from the boot endpoint instead, and this URL is only the graceful fallback; specifying it skips the endpoint retrieval |
|
||||
| `-CCTKDownloadURL` | Dell Command \| Configure 5.2.2 DUP on `dl.dell.com` | Source for the dynamic CCTK acquisition. Supports `.exe` (DUP), `.zip`, and `.7z` |
|
||||
| `-SevenZipDownloadURL` | `https://www.7-zip.org/a/7zr.exe` | Portable 7-Zip console executable used for payload extraction |
|
||||
| `-StagingDirectory` | `%WINDIR%\Temp\HTTPBootBios` | Working directory for downloads and extraction. Keep it short (MSI extraction fails on deep paths) |
|
||||
| `-BootImageDigest` | (computed) | Precomputed SHA-256 of the boot image (64 hex chars), placed into the profile without downloading the boot image. When omitted, the boot image is downloaded and hashed. The BIOS enforces the digest at boot time - a stale value stops HTTP boot until the profile is re-applied |
|
||||
| `-SetupPassword` | (none) | BIOS setup password, appended as `--ValSetupPwd=` on modification commands. Safe to supply fleet-wide: CCTK ignores the argument on devices with no setup password installed (verified on 5.2.2) |
|
||||
| `-SkipProfileDeletion` | off | Do not delete the existing HTTP boot profile before applying |
|
||||
| `-LogDirectory` | auto (toolkit) | Log folder override |
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
#region Get-EndpointCertificateChain
|
||||
Function Get-EndpointCertificateChain
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retrieves the TLS certificate chain presented by an HTTPS endpoint and exports it to PEM format.
|
||||
|
||||
.DESCRIPTION
|
||||
A synchronous TLS handshake is performed directly against the endpoint (TcpClient and SslStream) and the certificate chain is captured within the certificate validation callback. Validation always succeeds for the purposes of the capture, so untrusted, expired, or self signed endpoints can still be harvested. Each certificate within the chain is converted to PEM format by using the X509 classes, and the full chain can optionally be written to a PEM file.
|
||||
|
||||
The connection is deliberately made directly (never through a proxy), because the BIOS HTTP(s) boot feature contacts the endpoint directly as well - the probe therefore represents exactly what the firmware will experience. The handshake is performed synchronously so that the capture callback executes on the calling thread, which is required for it to work within both Windows PowerShell 5.1 and PowerShell 7.
|
||||
|
||||
The chain is returned leaf first. The root certificate is the last self signed certificate within the chain; when the chain does not resolve to a self signed certificate (for example when the issuing root is not present within the local certificate store), the topmost available certificate is returned as the root with a warning.
|
||||
|
||||
Dell BIOS HTTP boot profile certificate import requires RSA certificates. Certificates that do not use an RSA public key (for example ECDSA) are logged with a warning, but they are still converted and exported.
|
||||
|
||||
.PARAMETER URL
|
||||
The HTTPS URL of the endpoint to retrieve the certificate chain from. Only the scheme, host, and port are relevant - the path is not downloaded.
|
||||
|
||||
.PARAMETER Timeout
|
||||
The maximum duration to wait for the endpoint connection and handshake. Defaults to 15 seconds.
|
||||
|
||||
.PARAMETER ExportPath
|
||||
An optional file path that the full certificate chain is written to in PEM format (UTF-8 without a byte order mark).
|
||||
|
||||
.PARAMETER ContinueOnError
|
||||
Return a result object with the Succeeded property set to false instead of throwing a terminating error when the chain cannot be retrieved.
|
||||
|
||||
.EXAMPLE
|
||||
$GetEndpointCertificateChainResult = Get-EndpointCertificateChain -URL 'https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi' -ExportPath 'C:\Windows\Temp\Chain.pem' -ContinueOnError
|
||||
|
||||
Switch ($GetEndpointCertificateChainResult.Succeeded)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
Write-Output -InputObject ($GetEndpointCertificateChainResult.RootCertificate.PEMContent)
|
||||
}
|
||||
}
|
||||
|
||||
.NOTES
|
||||
The returned object contains: Succeeded, URL, ChainList (one entry per certificate: Index, Subject, Issuer, Thumbprint, NotBefore, NotAfter, KeyAlgorithm, IsSelfSigned, PEMContent, Certificate), LeafCertificate, RootCertificate, FullChainPEM, and ExportPath.
|
||||
|
||||
.LINK
|
||||
https://learn.microsoft.com/dotnet/api/system.security.cryptography.x509certificates.x509certificate2
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
Param
|
||||
(
|
||||
[Parameter(Mandatory=$True)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('U')]
|
||||
[System.URI]$URL,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('T')]
|
||||
[System.TimeSpan]$Timeout,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('EP')]
|
||||
[System.IO.FileInfo]$ExportPath,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[Alias('COE')]
|
||||
[Switch]$ContinueOnError
|
||||
)
|
||||
|
||||
Try
|
||||
{
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
[String]$CmdletName = $MyInvocation.MyCommand.Name
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is beginning. Please Wait..."))
|
||||
|
||||
#region Set default parameter values
|
||||
Switch ($True)
|
||||
{
|
||||
{($Null -ieq $Timeout)}
|
||||
{
|
||||
[System.TimeSpan]$Timeout = [System.TimeSpan]::FromSeconds(15)
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
$OutputObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$OutputObjectProperties.Succeeded = $False
|
||||
$OutputObjectProperties.URL = $URL
|
||||
$OutputObjectProperties.ChainList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
|
||||
$OutputObjectProperties.LeafCertificate = $Null
|
||||
$OutputObjectProperties.RootCertificate = $Null
|
||||
$OutputObjectProperties.FullChainPEM = [System.String]::Empty
|
||||
$OutputObjectProperties.ExportPath = $Null
|
||||
|
||||
Switch ($URL.Scheme -ieq 'https')
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
Throw "The URL scheme must be https in order to retrieve a TLS certificate chain. [URL: $($URL.AbsoluteUri)]"
|
||||
}
|
||||
}
|
||||
|
||||
#region Define the PEM conversion scriptblock
|
||||
[ScriptBlock]$ConvertToPEM = {
|
||||
Param
|
||||
(
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
|
||||
)
|
||||
|
||||
[String]$CertificateBase64 = [System.Convert]::ToBase64String($Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
|
||||
|
||||
$PEMBuilder = New-Object -TypeName 'System.Text.StringBuilder'
|
||||
$Null = $PEMBuilder.Append('-----BEGIN CERTIFICATE-----')
|
||||
$Null = $PEMBuilder.Append("`n")
|
||||
|
||||
For ($CertificateBase64Index = 0; $CertificateBase64Index -lt $CertificateBase64.Length; $CertificateBase64Index = $CertificateBase64Index + 64)
|
||||
{
|
||||
$Null = $PEMBuilder.Append($CertificateBase64.Substring($CertificateBase64Index, [System.Math]::Min(64, $CertificateBase64.Length - $CertificateBase64Index)))
|
||||
$Null = $PEMBuilder.Append("`n")
|
||||
}
|
||||
|
||||
$Null = $PEMBuilder.Append('-----END CERTIFICATE-----')
|
||||
|
||||
Write-Output -InputObject ($PEMBuilder.ToString())
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Perform the TLS handshake and capture the certificate chain
|
||||
$ChainCaptureList = New-Object -TypeName 'System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]'
|
||||
|
||||
#The callback always returns true so that untrusted, expired, or self signed endpoints can still be harvested. The certificates are cloned from their raw data so that they remain usable after the handshake completes. The closure is created within an isolated child scope so that only the capture list is captured, because within Windows PowerShell 5.1, GetNewClosure() clones every local variable including unbound validated parameters, which otherwise fails validation.
|
||||
[System.Net.Security.RemoteCertificateValidationCallback]$CertificateCaptureCallback = & {
|
||||
Param
|
||||
(
|
||||
$CaptureList
|
||||
)
|
||||
|
||||
#The closure is emitted as a bare expression, because Write-Output treats a script block argument as a delay-bind block and refuses it without pipeline input.
|
||||
({
|
||||
Param
|
||||
(
|
||||
$SenderObject,
|
||||
$Certificate,
|
||||
$Chain,
|
||||
$SslPolicyErrors
|
||||
)
|
||||
|
||||
Try
|
||||
{
|
||||
Switch (($Null -ine $Chain) -and ($Chain.ChainElements.Count -gt 0))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
ForEach ($ChainElement In $Chain.ChainElements)
|
||||
{
|
||||
$CaptureList.Add((New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList @(, $ChainElement.Certificate.RawData)))
|
||||
}
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
Switch ($Null -ine $Certificate)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$CaptureList.Add((New-Object -TypeName 'System.Security.Cryptography.X509Certificates.X509Certificate2' -ArgumentList @(, $Certificate.GetRawCertData())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Catch
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
Write-Output -InputObject ($True)
|
||||
}.GetNewClosure())
|
||||
} $ChainCaptureList
|
||||
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to retrieve the TLS certificate chain from `"$($URL.Host):$($URL.Port)`". Please Wait..."))
|
||||
|
||||
$TcpClient = New-Object -TypeName 'System.Net.Sockets.TcpClient'
|
||||
$TcpClient.ReceiveTimeout = $Timeout.TotalMilliseconds
|
||||
$TcpClient.SendTimeout = $Timeout.TotalMilliseconds
|
||||
|
||||
$SslStream = $Null
|
||||
|
||||
Try
|
||||
{
|
||||
$TcpClientConnectTask = $TcpClient.ConnectAsync($URL.Host, $URL.Port)
|
||||
|
||||
Switch ($TcpClientConnectTask.Wait($Timeout.TotalMilliseconds))
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
Throw "A TCP connection to `"$($URL.Host):$($URL.Port)`" could not be established within $($Timeout.TotalSeconds) second(s)."
|
||||
}
|
||||
}
|
||||
|
||||
#The handshake is performed synchronously so that the capture callback executes on the calling thread, which is required within both Windows PowerShell 5.1 and PowerShell 7.
|
||||
$SslStream = New-Object -TypeName 'System.Net.Security.SslStream' -ArgumentList @($TcpClient.GetStream(), $False, $CertificateCaptureCallback)
|
||||
$SslStream.ReadTimeout = $Timeout.TotalMilliseconds
|
||||
$SslStream.WriteTimeout = $Timeout.TotalMilliseconds
|
||||
|
||||
$Null = $SslStream.AuthenticateAsClient($URL.Host)
|
||||
}
|
||||
Finally
|
||||
{
|
||||
Switch ($Null -ine $SslStream)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$Null = $SslStream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$Null = $TcpClient.Dispose()
|
||||
}
|
||||
|
||||
Switch ($ChainCaptureList.Count -gt 0)
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
Throw "A TLS certificate chain could not be retrieved from `"$($URL.Scheme)://$($URL.Authority)`". The endpoint may be unreachable."
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Convert the captured certificate chain to PEM format
|
||||
$KeyAlgorithmOIDTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[String], [String]]'
|
||||
$KeyAlgorithmOIDTable.'1.2.840.113549.1.1.1' = 'RSA'
|
||||
$KeyAlgorithmOIDTable.'1.2.840.10045.2.1' = 'ECDSA'
|
||||
$KeyAlgorithmOIDTable.'1.2.840.10040.4.1' = 'DSA'
|
||||
|
||||
$FullChainPEMBuilder = New-Object -TypeName 'System.Text.StringBuilder'
|
||||
|
||||
For ($ChainCaptureListIndex = 0; $ChainCaptureListIndex -lt $ChainCaptureList.Count; $ChainCaptureListIndex++)
|
||||
{
|
||||
$ChainCertificate = $ChainCaptureList[$ChainCaptureListIndex]
|
||||
|
||||
[String]$ChainCertificateKeyAlgorithm = "$($ChainCertificate.PublicKey.Oid.Value)"
|
||||
|
||||
Switch ($KeyAlgorithmOIDTable.ContainsKey($ChainCertificateKeyAlgorithm))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
[String]$ChainCertificateKeyAlgorithm = $KeyAlgorithmOIDTable[$ChainCertificateKeyAlgorithm]
|
||||
}
|
||||
}
|
||||
|
||||
$ChainEntryProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$ChainEntryProperties.Index = $ChainCaptureListIndex
|
||||
$ChainEntryProperties.Subject = "$($ChainCertificate.Subject)"
|
||||
$ChainEntryProperties.Issuer = "$($ChainCertificate.Issuer)"
|
||||
$ChainEntryProperties.Thumbprint = "$($ChainCertificate.Thumbprint)"
|
||||
$ChainEntryProperties.NotBefore = $ChainCertificate.NotBefore
|
||||
$ChainEntryProperties.NotAfter = $ChainCertificate.NotAfter
|
||||
$ChainEntryProperties.KeyAlgorithm = $ChainCertificateKeyAlgorithm
|
||||
$ChainEntryProperties.IsSelfSigned = ($ChainCertificate.Subject -ieq $ChainCertificate.Issuer)
|
||||
$ChainEntryProperties.PEMContent = $ConvertToPEM.InvokeReturnAsIs($ChainCertificate)
|
||||
$ChainEntryProperties.Certificate = $ChainCertificate
|
||||
|
||||
$ChainEntry = New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ChainEntryProperties)
|
||||
|
||||
$OutputObjectProperties.ChainList.Add($ChainEntry)
|
||||
|
||||
$Null = $FullChainPEMBuilder.Append($ChainEntry.PEMContent)
|
||||
$Null = $FullChainPEMBuilder.Append("`n")
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Chain certificate $($ChainCaptureListIndex + 1) of $($ChainCaptureList.Count): [Subject: $($ChainEntry.Subject)] [Issuer: $($ChainEntry.Issuer)] [Key Algorithm: $($ChainEntry.KeyAlgorithm)] [Self Signed: $($ChainEntry.IsSelfSigned)] [Expiration: $($ChainEntry.NotAfter.ToString('o'))] [Thumbprint: $($ChainEntry.Thumbprint)]"))
|
||||
|
||||
Switch ($True)
|
||||
{
|
||||
{($ChainEntry.KeyAlgorithm -ine 'RSA')}
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The certificate `"$($ChainEntry.Subject)`" uses the `"$($ChainEntry.KeyAlgorithm)`" key algorithm.", "Dell BIOS HTTP boot profile certificate import requires RSA certificates, so the BIOS may reject this certificate with a `"not RSA format`" error. The certificate will still be exported."))
|
||||
}
|
||||
|
||||
{($ChainEntry.NotAfter -lt (Get-Date))}
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The certificate `"$($ChainEntry.Subject)`" has expired. [Expiration: $($ChainEntry.NotAfter.ToString('o'))]"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$OutputObjectProperties.FullChainPEM = $FullChainPEMBuilder.ToString()
|
||||
|
||||
$OutputObjectProperties.LeafCertificate = $OutputObjectProperties.ChainList[0]
|
||||
#endregion
|
||||
|
||||
#region Determine the root certificate (The last self signed certificate within the chain)
|
||||
For ($ChainListIndex = $OutputObjectProperties.ChainList.Count - 1; $ChainListIndex -ge 0; $ChainListIndex--)
|
||||
{
|
||||
Switch (($Null -ieq $OutputObjectProperties.RootCertificate) -and ($OutputObjectProperties.ChainList[$ChainListIndex].IsSelfSigned -eq $True))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$OutputObjectProperties.RootCertificate = $OutputObjectProperties.ChainList[$ChainListIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Switch ($Null -ieq $OutputObjectProperties.RootCertificate)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$OutputObjectProperties.RootCertificate = $OutputObjectProperties.ChainList[$OutputObjectProperties.ChainList.Count - 1]
|
||||
|
||||
$WriteLogMessage.Invoke(2, @("The retrieved certificate chain does not resolve to a self signed root certificate. The topmost available certificate will be used instead, which may stop working when the certificate authority rotates its intermediate certificates. [Subject: $($OutputObjectProperties.RootCertificate.Subject)]"))
|
||||
}
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Root Certificate: [Subject: $($OutputObjectProperties.RootCertificate.Subject)] [Key Algorithm: $($OutputObjectProperties.RootCertificate.KeyAlgorithm)] [Thumbprint: $($OutputObjectProperties.RootCertificate.Thumbprint)]"))
|
||||
#endregion
|
||||
|
||||
#region Export the full certificate chain to a PEM file (When requested)
|
||||
Switch (([System.String]::IsNullOrEmpty($ExportPath) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($ExportPath) -eq $False))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
Switch ([System.IO.Directory]::Exists($ExportPath.Directory.FullName))
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
$Null = [System.IO.Directory]::CreateDirectory($ExportPath.Directory.FullName)
|
||||
}
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to export the full certificate chain to `"$($ExportPath.FullName)`". Please Wait..."))
|
||||
|
||||
$Null = [System.IO.File]::WriteAllText("$($ExportPath.FullName)", $OutputObjectProperties.FullChainPEM, (New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList @($False)))
|
||||
|
||||
$OutputObjectProperties.ExportPath = $ExportPath
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
$OutputObjectProperties.Succeeded = $True
|
||||
|
||||
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($OutputObjectProperties))
|
||||
}
|
||||
Catch
|
||||
{
|
||||
$ExceptionPropertyDictionary = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$ExceptionPropertyDictionary.Add('Message', $_.Exception.Message)
|
||||
$ExceptionPropertyDictionary.Add('Category', $_.Exception.ErrorRecord.FullyQualifiedErrorID)
|
||||
$ExceptionPropertyDictionary.Add('LineNumber', $_.InvocationInfo.ScriptLineNumber)
|
||||
$ExceptionPropertyDictionary.Add('LinePosition', $_.InvocationInfo.OffsetInLine)
|
||||
$ExceptionPropertyDictionary.Add('Code', $_.InvocationInfo.Line.Trim())
|
||||
|
||||
$ExceptionMessageList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
|
||||
ForEach ($ExceptionProperty In $ExceptionPropertyDictionary.GetEnumerator())
|
||||
{
|
||||
$ExceptionMessageList.Add("[$($ExceptionProperty.Key): $($ExceptionProperty.Value)]")
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(2, @("$($ExceptionMessageList -Join ' ')"))
|
||||
|
||||
Switch ($ContinueOnError.IsPresent)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($OutputObjectProperties))
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
Finally
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is completed."))
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
Reference in New Issue
Block a user