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)