From 3c39a99b9a4cfa136a8d1f6a043efb77f23fd11f Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Thu, 4 Jun 2026 19:35:16 -0400 Subject: [PATCH] feat(scep): rework Get-InfisicalScepMdmProfile into FromEnrollment/FromProfile/Manual parameter sets FromEnrollment (new default) consumes an InfisicalCertificateApplicationEnrollment and auto-fills ServerUrl from scep.scepEndpointUrl, CAThumbprint from the RA certificate thumbprint, and mints a fresh dynamic challenge automatically when challengeType=dynamic and -Challenge is not supplied. FromProfile preserves the legacy projection from an InfisicalCertificateProfile but now requires -ApplicationId so the server URL is built against /scep/applications/{appId}/profiles/{profileId}/pkiclient.exe. Manual requires explicit -ServerUrl, -Challenge, and -UniqueId. Module manifest, help XML, and build.ps1 expectedCmds list updated to register the three new cmdlets. CHANGELOG updated. --- CHANGELOG.md | 14 +- Module/PSInfisicalAPI/PSInfisicalAPI.psd1 | 7 +- .../en-US/PSInfisicalAPI.dll-Help.xml | 113 +++++++++++- build.ps1 | 5 +- .../GetInfisicalScepMdmProfileCmdlet.cs | 171 +++++++++++++----- 5 files changed, 257 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3c41e..e45fce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,23 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.06.04.2305 + +- Build produced from commit 485ee8a7dd6a. + +## Unreleased (carried forward) + +- `Get-InfisicalCertificateApplication` added with `List` (default), `ById`, and `ByName` parameter sets. Binds to `/api/v1/cert-manager/applications` (list) and `/api/v1/cert-manager/applications/{applicationId}` / `/by-name/{name}` for single retrieval. Requests carry the `x-infisical-project-id` header so the certificate-manager scope resolves correctly. New `InfisicalCertificateApplication` model surfaces id, project, name, description, and counts. +- `Get-InfisicalCertificateApplicationEnrollment` added. Returns the API/EST/ACME/SCEP enrollment configuration for an application/profile pair (`GET /api/v1/cert-manager/applications/{applicationId}/profiles/{profileId}/enrollment`). The new `InfisicalCertificateApplicationEnrollment` model includes sub-blocks for each enrollment protocol; the SCEP block computes a SHA-1 `RaCertificateThumbprint` from the RA certificate PEM so it can be fed directly into MDM payloads. +- `New-InfisicalScepDynamicChallenge` added. Wraps `POST /scep/applications/{applicationId}/profiles/{profileId}/challenge` and returns the minted challenge as a `SecureString` (default) or string (`-AsPlainText`). The endpoint is gated by the dynamic-challenge feature on the target Infisical instance and by the calling identity's permission on `certificate-application-enrollment`. +- `Get-InfisicalScepMdmProfile` reworked into three parameter sets. `FromEnrollment` (new default) consumes an `InfisicalCertificateApplicationEnrollment` and auto-resolves `ServerUrl` from `scep.scepEndpointUrl`, `CAThumbprint` from the RA certificate, and the SCEP challenge (auto-minting when `challengeType=dynamic` and `-Challenge` is not supplied). `FromProfile` keeps the legacy projection from an `InfisicalCertificateProfile`, now requires `-ApplicationId`, and the default server URL is built against `/scep/applications/{appId}/profiles/{profileId}/pkiclient.exe`. `Manual` requires explicit `-ServerUrl`, `-Challenge`, and `-UniqueId`. +- `InfisicalApiInvoker` accepts an optional `extraHeaders` argument so callers can attach the `x-infisical-project-id` header and override `Accept` for plain-text responses (used by the new SCEP challenge endpoint). + ## 2026.06.04.2147 - Build produced from commit 183fb48c32ce. -## Unreleased (carried forward) +## Unreleased (carried forward) - `Get-InfisicalScepMdmProfile` added. Projects an `InfisicalCertificateProfile` (pipeline-bound) into a new `InfisicalScepMdmProfile` model that mirrors the Windows `ClientCertificateInstall/SCEP` CSP node set. `-ServerUrl` defaults to `{baseUri}/scep/{profileId}/pkiclient.exe` derived from the active connection (the `pkiclient.exe` suffix is the RFC 8894 / Cisco SCEP client compatibility holdover, not a server-side executable). `-UniqueId` defaults to a sanitized slug. `-Challenge` is a `SecureString` decrypted only when materializing the model. `KeyAlgorithm` and `EkuMapping` are inherited from the source profile defaults unless overridden. - `Export-InfisicalScepMdmProfile` added. Serializes the model via `InfisicalScepMdmProfile.ToSyncMl()` (XDocument build, XmlWriter emit, XmlReader round-trip validation) and writes the result to `-Path` as UTF-8 without BOM. Auto-creates the target directory, honors `-WhatIf`/`-Confirm`, and follows the project rule for `-Force`: if the destination exists without `-Force`, the cmdlet logs a warning and returns instead of throwing. `-PassThru` emits the resulting `FileInfo`. diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index 770a190..107de99 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.06.04.2147' + ModuleVersion = '2026.06.04.2305' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -46,6 +46,9 @@ 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', 'Export-InfisicalCertificate', + 'Get-InfisicalCertificateApplication', + 'Get-InfisicalCertificateApplicationEnrollment', + 'New-InfisicalScepDynamicChallenge', 'Get-InfisicalScepMdmProfile', 'Export-InfisicalScepMdmProfile', 'Write-InfisicalScepMdmProfileToWmi' @@ -60,7 +63,7 @@ LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html' ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI' ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.' - CommitHash = '183fb48c32ce' + CommitHash = '485ee8a7dd6a' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml index ffdcffb..c39f22b 100644 --- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml +++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml @@ -1477,33 +1477,130 @@ $UninstallInfisicalCertificateResult = Uninstall-InfisicalCertificate @Uninstall + + + Get-InfisicalCertificateApplication + Lists or retrieves an Infisical Certificate Manager Application from the active project. + Get + InfisicalCertificateApplication + + + Reads Infisical certificate-manager Applications (the join target used by EST/ACME/SCEP profile attachments) using the active connection's project scope. The List parameter set returns all applications visible to the caller; the ById and ByName sets return a single application. ProjectId falls back to the active connection when omitted. + + + + EXAMPLE 1 + Get-InfisicalCertificateApplication + Lists certificate-manager applications for the active project. + + + EXAMPLE 2 + Get-InfisicalCertificateApplication -ApplicationName 'workstation-mdm' + Retrieves a single application by name. + + + EXAMPLE 3 + $GetInfisicalCertificateApplicationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateApplicationParameters.Id = $ApplicationId +$GetInfisicalCertificateApplicationParameters.ProjectId = $ProjectId +$GetInfisicalCertificateApplicationParameters.Verbose = $True + +$GetInfisicalCertificateApplicationResult = Get-InfisicalCertificateApplication @GetInfisicalCertificateApplicationParameters + Retrieves a single application by id from an explicit project. + + + + + + + Get-InfisicalCertificateApplicationEnrollment + Retrieves the API/EST/ACME/SCEP enrollment configuration attached to an application/profile pair. + Get + InfisicalCertificateApplicationEnrollment + + + Returns the InfisicalCertificateApplicationEnrollment for the given application and certificate profile, including any configured SCEP sub-block (server URL, RA certificate PEM, computed SHA-1 RaCertificateThumbprint, challenge type, and challenge endpoint URL when dynamic). + + + + EXAMPLE 1 + Get-InfisicalCertificateApplicationEnrollment -ApplicationId $AppId -ProfileId $ProfileId + Fetches the enrollment configuration for an application/profile pair. + + + EXAMPLE 2 + $GetInfisicalCertificateApplicationEnrollmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateApplicationEnrollmentParameters.ApplicationId = $ApplicationId +$GetInfisicalCertificateApplicationEnrollmentParameters.ProfileId = $ProfileId +$GetInfisicalCertificateApplicationEnrollmentParameters.Verbose = $True + +$GetInfisicalCertificateApplicationEnrollmentResult = Get-InfisicalCertificateApplicationEnrollment @GetInfisicalCertificateApplicationEnrollmentParameters + Retrieves the enrollment configuration and feeds it downstream to Get-InfisicalScepMdmProfile. + + + + + + + New-InfisicalScepDynamicChallenge + Generates a one-time SCEP challenge from an application/profile that is configured with dynamic challenge mode. + New + InfisicalScepDynamicChallenge + + + POSTs to /scep/applications/{applicationId}/profiles/{profileId}/challenge and returns the minted challenge as a SecureString. Use -AsPlainText to return a string instead. Requires the active machine identity to have read access on certificate-application-enrollment, and the target SCEP profile must be set to challengeType=dynamic. Dynamic challenges are an Enterprise-tier feature on managed Infisical deployments. + + + + EXAMPLE 1 + $Challenge = New-InfisicalScepDynamicChallenge -ApplicationId $AppId -ProfileId $ProfileId + Mints a single-use SCEP challenge and stores it as a SecureString. + + + EXAMPLE 2 + $NewInfisicalScepDynamicChallengeParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalScepDynamicChallengeParameters.ApplicationId = $ApplicationId +$NewInfisicalScepDynamicChallengeParameters.ProfileId = $ProfileId +$NewInfisicalScepDynamicChallengeParameters.AsPlainText = $True +$NewInfisicalScepDynamicChallengeParameters.Verbose = $True + +$NewInfisicalScepDynamicChallengeResult = New-InfisicalScepDynamicChallenge @NewInfisicalScepDynamicChallengeParameters + Mints a plain-text challenge for use in environments where SecureString is inconvenient. + + + + Get-InfisicalScepMdmProfile - Builds an Infisical SCEP MDM profile model from a certificate profile, suitable for SyncML export or local MDM enrollment. + Builds an Infisical SCEP MDM profile model from an application enrollment, certificate profile, or fully manual inputs. Get InfisicalScepMdmProfile - Projects an InfisicalCertificateProfile (pipeline-bound) into an InfisicalScepMdmProfile that mirrors the Windows ClientCertificateInstall/SCEP CSP node set. -Challenge is accepted as a SecureString and decrypted into the model only at write-time. -ServerUrl defaults to {baseUri}/scep/{profileId}/pkiclient.exe derived from the active connection. -UniqueId defaults to a sanitized form of the source profile slug. KeyAlgorithm and EkuMapping are inherited from the source profile defaults unless overridden. + Produces an InfisicalScepMdmProfile that mirrors the Windows ClientCertificateInstall/SCEP CSP node set. FromEnrollment (default) consumes an InfisicalCertificateApplicationEnrollment and auto-fills ServerUrl from scep.scepEndpointUrl and CAThumbprint from the RA certificate; if the enrollment is configured for dynamic challenge mode, a fresh challenge is minted automatically when -Challenge is not supplied. FromProfile keeps the legacy projection from an InfisicalCertificateProfile and now requires -ApplicationId so the server URL can be built against /scep/applications/{appId}/profiles/{profileId}/pkiclient.exe. Manual requires explicit -ServerUrl, -Challenge, and -UniqueId. Notes - The SCEP endpoint URL ends in 'pkiclient.exe' for RFC 8894 / Cisco SCEP client compatibility. The source profile must have SCEP enrollment enabled on the server side for enrollment to succeed; this cmdlet does not validate that. + The SCEP endpoint URL ends in 'pkiclient.exe' for RFC 8894 / Cisco SCEP client compatibility. SecureString -Challenge is decrypted into the model only at write-time. EXAMPLE 1 - Get-InfisicalCertificateProfile -CertificateProfileId $ProfileId | Get-InfisicalScepMdmProfile -Challenge (Read-Host -AsSecureString 'SCEP challenge') - Builds a default SCEP MDM profile with the server URL inferred from the active connection. + Get-InfisicalCertificateApplicationEnrollment -ApplicationId $AppId -ProfileId $ProfileId | Get-InfisicalScepMdmProfile + Builds a SCEP MDM profile from an enrollment, auto-resolving ServerUrl, CAThumbprint, and (for dynamic mode) the challenge. EXAMPLE 2 + Get-InfisicalCertificateProfile -CertificateProfileId $ProfileId | Get-InfisicalScepMdmProfile -ApplicationId $AppId -Challenge (Read-Host -AsSecureString 'SCEP challenge') + Builds a profile from a certificate profile (legacy path) with an explicit application id and static challenge. + + + EXAMPLE 3 $GetInfisicalScepMdmProfileParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) -$GetInfisicalScepMdmProfileParameters.InputObject = (Get-InfisicalCertificateProfile -CertificateProfileId $ProfileId) -$GetInfisicalScepMdmProfileParameters.Challenge = (Read-Host -AsSecureString 'SCEP challenge') +$GetInfisicalScepMdmProfileParameters.EnrollmentObject = $Enrollment $GetInfisicalScepMdmProfileParameters.UniqueId = 'WindowsClientAuth' $GetInfisicalScepMdmProfileParameters.Scope = 'Device' $GetInfisicalScepMdmProfileParameters.SubjectName = "CN=$($env:COMPUTERNAME)" @@ -1514,7 +1611,7 @@ $GetInfisicalScepMdmProfileParameters.ValidPeriodUnits = 1 $GetInfisicalScepMdmProfileParameters.Verbose = $True $GetInfisicalScepMdmProfileResult = Get-InfisicalScepMdmProfile @GetInfisicalScepMdmProfileParameters - Builds a device-scope SCEP MDM profile with explicit subject and key parameters for downstream export or local enrollment. + Builds a device-scope SCEP MDM profile from an enrollment with overridden subject and key parameters. diff --git a/build.ps1 b/build.ps1 index 7be9ead..5e70b47 100644 --- a/build.ps1 +++ b/build.ps1 @@ -140,6 +140,9 @@ function Write-Manifest { 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', 'Export-InfisicalCertificate', + 'Get-InfisicalCertificateApplication', + 'Get-InfisicalCertificateApplicationEnrollment', + 'New-InfisicalScepDynamicChallenge', 'Get-InfisicalScepMdmProfile', 'Export-InfisicalScepMdmProfile', 'Write-InfisicalScepMdmProfileToWmi' @@ -207,7 +210,7 @@ if (`$cmds.Count -eq 0) { throw "No cmdlets were exported by the PSInfisicalAPI module." } -`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi') +`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi') foreach (`$expected in `$expectedCmds) { if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { throw "Cmdlet not found: `$expected" diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalScepMdmProfileCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalScepMdmProfileCmdlet.cs index 8621151..59336e0 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalScepMdmProfileCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalScepMdmProfileCmdlet.cs @@ -6,24 +6,39 @@ using System.Runtime.InteropServices; using System.Security; using PSInfisicalAPI.Connections; using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; namespace PSInfisicalAPI.Cmdlets { - [Cmdlet(VerbsCommon.Get, "InfisicalScepMdmProfile")] + [Cmdlet(VerbsCommon.Get, "InfisicalScepMdmProfile", DefaultParameterSetName = "FromEnrollment")] [OutputType(typeof(InfisicalScepMdmProfile))] public sealed class GetInfisicalScepMdmProfileCmdlet : InfisicalCmdletBase { private const string Component = "GetInfisicalScepMdmProfileCmdlet"; - [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] + [Parameter(ParameterSetName = "FromEnrollment", Mandatory = true, ValueFromPipeline = true, Position = 0)] + [Alias("Enrollment")] + public InfisicalCertificateApplicationEnrollment EnrollmentObject { get; set; } + + [Parameter(ParameterSetName = "FromProfile", Mandatory = true, ValueFromPipeline = true, Position = 0)] [Alias("Profile", "CertificateProfile")] public InfisicalCertificateProfile InputObject { get; set; } - [Parameter(Mandatory = true)] + [Parameter(ParameterSetName = "FromProfile", Mandatory = true)] + [Alias("AppId")] + public string ApplicationId { get; set; } + + [Parameter(ParameterSetName = "FromEnrollment")] + [Parameter(ParameterSetName = "FromProfile")] + [Parameter(ParameterSetName = "Manual", Mandatory = true)] public SecureString Challenge { get; set; } + [Parameter(ParameterSetName = "Manual", Mandatory = true)] + [Parameter(ParameterSetName = "FromProfile")] + [Parameter(ParameterSetName = "FromEnrollment")] + public string ServerUrl { get; set; } + [Parameter] public string UniqueId { get; set; } - [Parameter] public string ServerUrl { get; set; } [Parameter] [ValidateSet("Device", "User")] @@ -53,45 +68,21 @@ namespace PSInfisicalAPI.Cmdlets { try { - if (InputObject == null) { throw new InvalidOperationException("InputObject is required."); } - if (string.IsNullOrEmpty(InputObject.Id)) { throw new InvalidOperationException("InputObject.Id is required."); } - InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedServerUrl = !string.IsNullOrEmpty(ServerUrl) ? ServerUrl : BuildDefaultServerUrl(connection, InputObject.Id); - string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(!string.IsNullOrEmpty(InputObject.Slug) ? InputObject.Slug : InputObject.Id); - InfisicalCertificateProfileDefaults defaults = InputObject.Defaults; - string resolvedKeyAlgorithm = !string.IsNullOrEmpty(KeyAlgorithm) ? KeyAlgorithm : MapKeyAlgorithm(defaults != null ? defaults.KeyAlgorithm : null); - string resolvedEku = !string.IsNullOrEmpty(EkuMapping) ? EkuMapping : JoinEkuOids(defaults != null ? defaults.ExtendedKeyUsages : null); - - InfisicalScepMdmProfile result = new InfisicalScepMdmProfile + if (string.Equals(ParameterSetName, "FromEnrollment", StringComparison.Ordinal)) { - UniqueId = resolvedUniqueId, - Scope = Scope, - ServerUrl = resolvedServerUrl, - Challenge = SecureStringToPlainText(Challenge), - SubjectName = SubjectName, - SubjectAlternativeNames = SubjectAlternativeNames, - EkuMapping = resolvedEku, - KeyUsage = KeyUsage, - KeyLength = KeyLength, - KeyAlgorithm = resolvedKeyAlgorithm, - HashAlgorithm = HashAlgorithm, - KeyProtection = KeyProtection, - ContainerName = ContainerName, - ValidPeriod = ValidPeriod, - ValidPeriodUnits = ValidPeriodUnits, - RetryCount = RetryCount, - RetryDelay = RetryDelay, - TemplateName = TemplateName, - CAThumbprint = CAThumbprint, - CustomTextToShowInPrompt = CustomTextToShowInPrompt, - SourceProfileId = InputObject.Id, - SourceProfileSlug = InputObject.Slug - }; + WriteObject(BuildFromEnrollment(connection)); + return; + } - Logger.Verbose(Component, string.Concat("Built SCEP MDM profile for source profile '", InputObject.Slug ?? InputObject.Id, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ").")); - WriteObject(result); + if (string.Equals(ParameterSetName, "FromProfile", StringComparison.Ordinal)) + { + WriteObject(BuildFromProfile(connection)); + return; + } + + WriteObject(BuildManual(connection)); } catch (Exception exception) { @@ -99,11 +90,109 @@ namespace PSInfisicalAPI.Cmdlets } } - private static string BuildDefaultServerUrl(InfisicalConnection connection, string profileId) + private InfisicalScepMdmProfile BuildFromEnrollment(InfisicalConnection connection) + { + if (EnrollmentObject == null) { throw new InvalidOperationException("EnrollmentObject is required."); } + if (string.IsNullOrEmpty(EnrollmentObject.ApplicationId)) { throw new InvalidOperationException("EnrollmentObject.ApplicationId is required."); } + if (string.IsNullOrEmpty(EnrollmentObject.ProfileId)) { throw new InvalidOperationException("EnrollmentObject.ProfileId is required."); } + + InfisicalCertificateApplicationScepEnrollment scep = EnrollmentObject.Scep; + if (scep == null) { throw new InvalidOperationException("Enrollment does not have SCEP configured."); } + + string resolvedServerUrl = FirstNonEmpty(ServerUrl, scep.ScepEndpointUrl, BuildDefaultServerUrl(connection, EnrollmentObject.ApplicationId, EnrollmentObject.ProfileId)); + string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(EnrollmentObject.ProfileId); + string resolvedThumbprint = !string.IsNullOrEmpty(CAThumbprint) ? CAThumbprint : scep.RaCertificateThumbprint; + string resolvedChallenge = ResolveChallengeFromEnrollment(connection, scep); + + InfisicalScepMdmProfile result = NewProfileShell(resolvedUniqueId, resolvedServerUrl, resolvedChallenge, resolvedThumbprint, null, null); + result.SourceProfileId = EnrollmentObject.ProfileId; + Logger.Verbose(Component, string.Concat("Built SCEP MDM profile from enrollment for application '", EnrollmentObject.ApplicationId, "' / profile '", EnrollmentObject.ProfileId, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ", ChallengeType=", scep.ChallengeType ?? "", ").")); + return result; + } + + private InfisicalScepMdmProfile BuildFromProfile(InfisicalConnection connection) + { + if (InputObject == null) { throw new InvalidOperationException("InputObject is required."); } + if (string.IsNullOrEmpty(InputObject.Id)) { throw new InvalidOperationException("InputObject.Id is required."); } + if (string.IsNullOrEmpty(ApplicationId)) { throw new InvalidOperationException("ApplicationId is required when binding by certificate profile."); } + if (Challenge == null) { throw new InvalidOperationException("Challenge is required when building from a certificate profile."); } + + string resolvedServerUrl = !string.IsNullOrEmpty(ServerUrl) ? ServerUrl : BuildDefaultServerUrl(connection, ApplicationId, InputObject.Id); + string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(!string.IsNullOrEmpty(InputObject.Slug) ? InputObject.Slug : InputObject.Id); + InfisicalCertificateProfileDefaults defaults = InputObject.Defaults; + string resolvedKeyAlgorithm = !string.IsNullOrEmpty(KeyAlgorithm) ? KeyAlgorithm : MapKeyAlgorithm(defaults != null ? defaults.KeyAlgorithm : null); + string resolvedEku = !string.IsNullOrEmpty(EkuMapping) ? EkuMapping : JoinEkuOids(defaults != null ? defaults.ExtendedKeyUsages : null); + + InfisicalScepMdmProfile result = NewProfileShell(resolvedUniqueId, resolvedServerUrl, SecureStringToPlainText(Challenge), CAThumbprint, resolvedKeyAlgorithm, resolvedEku); + result.SourceProfileId = InputObject.Id; + result.SourceProfileSlug = InputObject.Slug; + Logger.Verbose(Component, string.Concat("Built SCEP MDM profile for source profile '", InputObject.Slug ?? InputObject.Id, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ").")); + return result; + } + + private InfisicalScepMdmProfile BuildManual(InfisicalConnection connection) + { + if (string.IsNullOrEmpty(UniqueId)) { throw new InvalidOperationException("UniqueId is required in Manual mode."); } + string resolvedChallenge = SecureStringToPlainText(Challenge); + InfisicalScepMdmProfile result = NewProfileShell(UniqueId, ServerUrl, resolvedChallenge, CAThumbprint, KeyAlgorithm, EkuMapping); + Logger.Verbose(Component, string.Concat("Built SCEP MDM profile in Manual mode targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ").")); + return result; + } + + private InfisicalScepMdmProfile NewProfileShell(string uniqueId, string serverUrl, string challenge, string thumbprint, string keyAlgorithm, string ekuMapping) + { + return new InfisicalScepMdmProfile + { + UniqueId = uniqueId, + Scope = Scope, + ServerUrl = serverUrl, + Challenge = challenge, + SubjectName = SubjectName, + SubjectAlternativeNames = SubjectAlternativeNames, + EkuMapping = ekuMapping, + KeyUsage = KeyUsage, + KeyLength = KeyLength, + KeyAlgorithm = keyAlgorithm, + HashAlgorithm = HashAlgorithm, + KeyProtection = KeyProtection, + ContainerName = ContainerName, + ValidPeriod = ValidPeriod, + ValidPeriodUnits = ValidPeriodUnits, + RetryCount = RetryCount, + RetryDelay = RetryDelay, + TemplateName = TemplateName, + CAThumbprint = thumbprint, + CustomTextToShowInPrompt = CustomTextToShowInPrompt + }; + } + + private string ResolveChallengeFromEnrollment(InfisicalConnection connection, InfisicalCertificateApplicationScepEnrollment scep) + { + if (Challenge != null) { return SecureStringToPlainText(Challenge); } + + string challengeType = scep.ChallengeType ?? string.Empty; + if (string.Equals(challengeType, "dynamic", StringComparison.OrdinalIgnoreCase)) + { + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + Logger.Verbose(Component, "Minting SCEP dynamic challenge for enrollment."); + return client.GenerateScepDynamicChallenge(connection, EnrollmentObject.ApplicationId, EnrollmentObject.ProfileId); + } + + throw new InvalidOperationException(string.Concat("Enrollment uses challengeType '", challengeType, "'. Supply -Challenge with the configured static challenge password.")); + } + + private static string BuildDefaultServerUrl(InfisicalConnection connection, string applicationId, string profileId) { if (connection == null || connection.BaseUri == null) { throw new InvalidOperationException("Active Infisical connection is required to derive ServerUrl."); } string baseUrl = connection.BaseUri.GetLeftPart(UriPartial.Authority); - return string.Concat(baseUrl, "/scep/", profileId, "/pkiclient.exe"); + return string.Concat(baseUrl, "/scep/applications/", applicationId, "/profiles/", profileId, "/pkiclient.exe"); + } + + private static string FirstNonEmpty(params string[] values) + { + if (values == null) { return null; } + foreach (string value in values) { if (!string.IsNullOrEmpty(value)) { return value; } } + return null; } private static string SanitizeForCspId(string input)