Add profile-based issuance to Request-InfisicalCertificate

New ByProfile parameter set bound by -CertificateProfileId (alias ProfileId) POSTs to /api/v1/cert-manager/certificates with the profile id, the locally generated CSR, and an attributes envelope (subject fields, ttl, notBefore, notAfter, keyUsages, extendedKeyUsages). The wrapped response is unwrapped into the existing InfisicalSignedCertificate so reuse, install, chain-completion and key-protection paths remain unchanged. Issuance that returns without a certificate (e.g. status pending_approval) raises a configuration exception that surfaces the reported status and message. Ttl/NotBefore/NotAfter/KeyUsage/ExtendedKeyUsage parameters are now shared by ByCa and ByProfile. MAML help and existing parameter-set test updated.
This commit is contained in:
GraceSolutions
2026-06-04 17:02:03 -04:00
parent c9c8a8446b
commit ebabd6cf26
8 changed files with 142 additions and 7 deletions
@@ -158,6 +158,7 @@ namespace PSInfisicalAPI.Tests
Assert.NotNull(cmdletType.GetProperty("PkiSubscriberSlug"));
Assert.NotNull(cmdletType.GetProperty("CertificateAuthorityId"));
Assert.NotNull(cmdletType.GetProperty("CertificateProfileId"));
Assert.NotNull(cmdletType.GetProperty("Subject"));
Assert.NotNull(cmdletType.GetProperty("CommonName"));
Assert.NotNull(cmdletType.GetProperty("DnsName"));
@@ -23,6 +23,10 @@ namespace PSInfisicalAPI.Cmdlets
[Alias("CaId")]
public string CertificateAuthorityId { get; set; }
[Parameter(ParameterSetName = "ByProfile", Mandatory = true, Position = 0)]
[Alias("ProfileId")]
public string CertificateProfileId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public IDictionary Subject { get; set; }
[Parameter] public string CommonName { get; set; }
@@ -38,13 +42,18 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter] public int KeySize { get; set; } = 2048;
[Parameter] public InfisicalEcCurve Curve { get; set; } = InfisicalEcCurve.P256;
[Parameter(ParameterSetName = "ByCa")] public string Ttl { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string NotBefore { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string NotAfter { get; set; }
[Parameter(ParameterSetName = "ByCa")]
[Parameter(ParameterSetName = "ByProfile")] public string Ttl { get; set; }
[Parameter(ParameterSetName = "ByCa")]
[Parameter(ParameterSetName = "ByProfile")] public string NotBefore { get; set; }
[Parameter(ParameterSetName = "ByCa")]
[Parameter(ParameterSetName = "ByProfile")] public string NotAfter { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string FriendlyName { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string PkiCollectionId { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string[] KeyUsage { get; set; }
[Parameter(ParameterSetName = "ByCa")] public string[] ExtendedKeyUsage { get; set; }
[Parameter(ParameterSetName = "ByCa")]
[Parameter(ParameterSetName = "ByProfile")] public string[] KeyUsage { get; set; }
[Parameter(ParameterSetName = "ByCa")]
[Parameter(ParameterSetName = "ByProfile")] public string[] ExtendedKeyUsage { get; set; }
[Parameter] public SwitchParameter Install { get; set; }
[Parameter] public StoreName StoreName { get; set; } = StoreName.My;
@@ -104,7 +113,7 @@ namespace PSInfisicalAPI.Cmdlets
return;
}
string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "' or CA '", CertificateAuthorityId ?? "(n/a)", "' for CN=", csrSubject.CommonName);
string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "', CA '", CertificateAuthorityId ?? "(n/a)", "', or profile '", CertificateProfileId ?? "(n/a)", "' for CN=", csrSubject.CommonName);
if (!ShouldProcess(target, "Request new certificate")) { return; }
InfisicalCsrOptions csrOptions = new InfisicalCsrOptions { KeyAlgorithm = KeyAlgorithm, RsaKeySize = KeySize, EcCurve = Curve };
@@ -198,6 +207,12 @@ namespace PSInfisicalAPI.Cmdlets
return client.SignCertificateBySubscriber(connection, PkiSubscriberSlug, projectId, csrPem);
}
if (string.Equals(ParameterSetName, "ByProfile", StringComparison.Ordinal))
{
InfisicalCsrSubject subject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress);
return client.IssueCertificateByProfile(connection, CertificateProfileId, csrPem, subject.CommonName, subject.Organization, subject.OrganizationalUnit, subject.Country, subject.State, subject.Locality, Ttl, NotBefore, NotAfter, KeyUsage, ExtendedKeyUsage);
}
return client.SignCertificateByCa(connection, CertificateAuthorityId, csrPem, CommonName, null, Ttl, NotBefore, NotAfter, FriendlyName, PkiCollectionId, KeyUsage, ExtendedKeyUsage);
}
}
@@ -51,6 +51,7 @@ namespace PSInfisicalAPI.Endpoints
public const string GetCertificateBundle = "GetCertificateBundle";
public const string SignCertificateBySubscriber = "SignCertificateBySubscriber";
public const string SignCertificateByCa = "SignCertificateByCa";
public const string IssueCertificateByProfile = "IssueCertificateByProfile";
public const string ListPkiSubscribers = "ListPkiSubscribers";
public const string GetPkiSubscriber = "GetPkiSubscriber";
@@ -623,6 +623,17 @@ namespace PSInfisicalAPI.Endpoints
ContainsSecretMaterialInResponse = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.IssueCertificateByProfile,
Resource = "Pki",
Version = "v1",
Method = "POST",
Template = "/api/v1/cert-manager/certificates",
RequiresAuthorization = true,
ContainsSecretMaterialInResponse = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListPkiSubscribers,
@@ -300,6 +300,66 @@ namespace PSInfisicalAPI.Pki
};
}
public InfisicalSignedCertificate IssueCertificateByProfile(InfisicalConnection connection, string profileId, string csrPem, string commonName, string organization, string organizationalUnit, string country, string state, string locality, string ttl, string notBefore, string notAfter, IEnumerable<string> keyUsages, IEnumerable<string> extendedKeyUsages)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("CertificateProfileId is required."); }
if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); }
InfisicalIssueCertificateAttributesDto attributes = new InfisicalIssueCertificateAttributesDto
{
CommonName = commonName,
Organization = organization,
OrganizationalUnit = organizationalUnit,
Country = country,
State = state,
Locality = locality,
Ttl = ttl,
NotBefore = notBefore,
NotAfter = notAfter,
KeyUsages = keyUsages != null ? new List<string>(keyUsages) : null,
ExtendedKeyUsages = extendedKeyUsages != null ? new List<string>(extendedKeyUsages) : null
};
InfisicalIssueCertificateByProfileRequestDto request = new InfisicalIssueCertificateByProfileRequestDto
{
ProfileId = profileId,
Csr = csrPem,
Attributes = attributes
};
string body = _serializer.Serialize(request);
try
{
_logger.Information(Component, string.Concat("Attempting to issue certificate via profile '", profileId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.IssueCertificateByProfile, "IssueCertificateByProfile", null, null, body);
InfisicalIssueCertificateResponseDto dto = _serializer.Deserialize<InfisicalIssueCertificateResponseDto>(response.Body);
response.Clear();
if (dto == null || dto.Certificate == null || string.IsNullOrEmpty(dto.Certificate.Certificate))
{
string status = dto != null ? dto.Status : "unknown";
string message = dto != null ? dto.Message : null;
throw new InfisicalConfigurationException(string.Concat("Certificate was not issued (status='", status ?? "unknown", "'", string.IsNullOrEmpty(message) ? "" : string.Concat(", message='", message, "'"), "). The certificate profile may require manual approval or additional validation."));
}
InfisicalSignedCertificate signed = new InfisicalSignedCertificate
{
SerialNumber = dto.Certificate.SerialNumber,
CertificatePem = dto.Certificate.Certificate,
CertificateChainPem = dto.Certificate.CertificateChain,
IssuingCaCertificatePem = dto.Certificate.IssuingCaCertificate
};
_logger.Information(Component, "Infisical certificate issuance (profile) was successful.");
return signed;
}
catch (Exception)
{
_logger.Error(Component, "Infisical certificate issuance (profile) failed.");
throw;
}
}
public InfisicalPkiSubscriber[] ListPkiSubscribers(InfisicalConnection connection, string projectId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
@@ -30,4 +30,44 @@ namespace PSInfisicalAPI.Pki
[JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; }
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
}
internal sealed class InfisicalIssueCertificateByProfileRequestDto
{
[JsonProperty("profileId")] public string ProfileId { get; set; }
[JsonProperty("csr", NullValueHandling = NullValueHandling.Ignore)] public string Csr { get; set; }
[JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] public InfisicalIssueCertificateAttributesDto Attributes { get; set; }
}
internal sealed class InfisicalIssueCertificateAttributesDto
{
[JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; }
[JsonProperty("organization", NullValueHandling = NullValueHandling.Ignore)] public string Organization { get; set; }
[JsonProperty("organizationalUnit", NullValueHandling = NullValueHandling.Ignore)] public string OrganizationalUnit { get; set; }
[JsonProperty("country", NullValueHandling = NullValueHandling.Ignore)] public string Country { get; set; }
[JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] public string State { get; set; }
[JsonProperty("locality", NullValueHandling = NullValueHandling.Ignore)] public string Locality { get; set; }
[JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; }
[JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; }
[JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; }
[JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List<string> KeyUsages { get; set; }
[JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List<string> ExtendedKeyUsages { get; set; }
}
internal sealed class InfisicalIssueCertificateResponseDto
{
[JsonProperty("certificate")] public InfisicalIssueCertificateInnerDto Certificate { get; set; }
[JsonProperty("certificateRequestId")] public string CertificateRequestId { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("message")] public string Message { get; set; }
}
internal sealed class InfisicalIssueCertificateInnerDto
{
[JsonProperty("certificate")] public string Certificate { get; set; }
[JsonProperty("certificateChain")] public string CertificateChain { get; set; }
[JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; }
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
[JsonProperty("certificateId")] public string CertificateId { get; set; }
[JsonProperty("privateKey")] public string PrivateKey { get; set; }
}
}