M10 PKI: add 6 cmdlets (Get-/Search-/ConvertTo-/Install-/Uninstall-/Export-InfisicalCertificate), BouncyCastle-backed PemCertificateBuilder, formatting/type metadata for PKI models, and cert-manager <-> pki route alias fallback via InvokeWithCandidateF… #4

Merged
gsadmin merged 2 commits from dev into main 2026-06-04 01:31:39 +00:00
31 changed files with 2097 additions and 12 deletions
+3 -3
View File
@@ -41,7 +41,7 @@ jobs:
Write-Host "Manifest OK: $($manifest.Name) $($manifest.Version)"
- name: Upload module artifact
uses: actions/upload-artifact@v4
uses: christopherhx/gitea-upload-artifact@v4
with:
name: PSInfisicalAPI-module
path: Module/PSInfisicalAPI
@@ -71,7 +71,7 @@ jobs:
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
- name: Download module artifact
uses: actions/download-artifact@v4
uses: christopherhx/gitea-download-artifact@v4
with:
name: PSInfisicalAPI-module
path: Module/PSInfisicalAPI
@@ -213,7 +213,7 @@ jobs:
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
- name: Download module artifact
uses: actions/download-artifact@v4
uses: christopherhx/gitea-download-artifact@v4
with:
name: PSInfisicalAPI-module
path: Module/PSInfisicalAPI
+29 -1
View File
@@ -6,6 +6,34 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased
- **CI — Gitea artifact upload fix**: Replaced `actions/upload-artifact@v4` and `actions/download-artifact@v4` with the Gitea-compatible forks `christopherhx/gitea-upload-artifact@v4` and `christopherhx/gitea-download-artifact@v4` in `.gitea/workflows/publish-psgallery.yml`. The upstream v4 actions abort on Gitea because Gitea is detected as GHES, which the upstream v4 actions do not support (see [go-gitea/gitea#28853](https://github.com/go-gitea/gitea/issues/28853)).
## 2026.06.04.0123
- Build produced from commit 2cbd5c2008f5.
## Unreleased (carried forward)
- **M10 polish — formatting, type metadata, and PKI route aliases**:
- Added default table views and `DefaultDisplayPropertySet` entries for `InfisicalCertificateAuthority`, `InfisicalCertificate`, and `InfisicalCertificateBundle` in the module `Format.ps1xml` / `Types.ps1xml`.
- Realigned PKI endpoint registry to current Infisical paths: `ListInternalCertificateAuthorities` and `RetrieveInternalCertificateAuthority` now use `/api/v1/cert-manager/ca/internal[/{caId}]` as primary, with legacy `/api/v1/pki/ca/internal[/{caId}]` retained as a fallback alias. `GetCertificateBundle` and `RetrieveCertificate` similarly carry `cert-manager` fallback aliases.
- `InfisicalApiInvoker.InvokeWithCandidateFallback` walks the candidate list and falls back on `404`/`405`, used by `InfisicalPkiClient` so older self-hosted Infisical instances are tolerated transparently.
## 2026.06.04.0114
- Build produced from commit 2cbd5c2008f5.
## Unreleased (carried forward)
- **M10 — PKI Internal CAs, Certificates & Windows Store integration**:
- **`Get-InfisicalCertificateAuthority`** lists internal certificate authorities for the current project, or returns a single CA with `-CaId`.
- **`Search-InfisicalCertificate`** wraps `POST /api/v1/projects/{projectId}/certificates/search` with rich filters (`-CommonName`, `-FriendlyName`, `-Search`, `-Status`, `-CaId`, `-ProfileId`, `-ApplicationId`, `-EnrollmentType`, `-KeyAlgorithm`, `-SignatureAlgorithm`, `-Source`, `-NotAfterFrom/To`, `-NotBeforeFrom/To`, `-SortBy/-SortOrder`, `-Limit/-Offset`). Auto-paginates unless `-NoAutoPage` is set.
- **`ConvertTo-InfisicalCertificate`** accepts an `InfisicalCertificate`, `InfisicalCertificateBundle`, or `-SerialNumber`, fetches the bundle endpoint when needed, and emits a `System.Security.Cryptography.X509Certificates.X509Certificate2` with the private key attached. `-NoPrivateKey` skips key parsing; `-IncludeChain` additionally emits intermediates; `-KeyStorageFlags` controls import behavior.
- **`Install-InfisicalCertificate`** / **`Uninstall-InfisicalCertificate`** perform idempotent installs/removes against a Windows `X509Store` (`-StoreName`, `-StoreLocation`, defaults `My`/`CurrentUser`), matching by thumbprint. Install is a no-op when the thumbprint is already present unless `-Force` is supplied (which replaces the existing entry). Both honor `ShouldProcess` and accept pipeline input.
- **`Export-InfisicalCertificate`** writes PEM, PFX, or CER to disk via `-Format`, with `-Password` (SecureString) for PFX, `-IncludeChain` for full-chain PEM, `-NoPrivateKey` to omit the key, and `-Force` to overwrite.
- **BouncyCastle dependency**: Added `BouncyCastle.Cryptography` to bridge PEM/PKCS#8 parsing on .NET Standard 2.0 / Windows PowerShell 5.1 (where `X509Certificate2.CreateFromPem` and `RSA.ImportFromPem` are unavailable). The shared `PemCertificateBuilder` assembles cert + chain + key into an in-memory PKCS#12 blob and imports it back into `X509Certificate2`. The DLL ships in the published module bin directory.
- PKI endpoint registry entries for `ListInternalCertificateAuthorities` (`GET /api/v1/pki/ca/internal`), `RetrieveInternalCertificateAuthority` (`GET /api/v1/pki/ca/internal/{caId}`), `SearchCertificates` (`POST /api/v1/projects/{projectId}/certificates/search`), `RetrieveCertificate`, and `GetCertificateBundle` (`GET /api/v1/pki/certificates/{serialNumber}/bundle`).
## 2026.06.04.0020
- Build produced from commit 211fbcf34dbb.
@@ -24,7 +52,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## 2026.06.03.2207
- Build produced from commit 09c3d5c68bbc.
- **M9 — Bulk, Duplicate & Inheritance**:
- **M9 — Bulk, Duplicate & Inheritance**:
- **Bulk parameter sets** added to `New-InfisicalSecret`, `Update-InfisicalSecret`, and `Remove-InfisicalSecret` accepting `-Secrets Hashtable[]`; client methods `CreateBatch`/`UpdateBatch`/`DeleteBatch` wrap `POST|PATCH|DELETE /api/v3/secrets/batch/raw`.
- **`Copy-InfisicalSecret`** cmdlet added, wrapping `POST /api/v4/secrets/duplicate` with source/destination environment + path parameters and per-attribute copy toggles.
- **Connection inheritance** centralized in `InfisicalCmdletBase` (`ResolveProjectId`/`ResolveEnvironment`/`ResolveSecretPath`/`ResolveApiVersion`/`ResolveOrganizationId`). Explicit parameters always win; missing values fall back to the active connection and emit a `-Verbose` line.
@@ -31,5 +31,63 @@
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSInfisicalAPI.Models.InfisicalCertificateAuthority</Name>
<ViewSelectedBy>
<TypeName>PSInfisicalAPI.Models.InfisicalCertificateAuthority</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader><Label>Name</Label><Width>28</Width></TableColumnHeader>
<TableColumnHeader><Label>CommonName</Label><Width>32</Width></TableColumnHeader>
<TableColumnHeader><Label>Type</Label><Width>10</Width></TableColumnHeader>
<TableColumnHeader><Label>Status</Label><Width>10</Width></TableColumnHeader>
<TableColumnHeader><Label>KeyAlgorithm</Label><Width>14</Width></TableColumnHeader>
<TableColumnHeader><Label>NotAfter</Label><Width>22</Width></TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>CommonName</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Type</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>KeyAlgorithm</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>NotAfter</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSInfisicalAPI.Models.InfisicalCertificate</Name>
<ViewSelectedBy>
<TypeName>PSInfisicalAPI.Models.InfisicalCertificate</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader><Label>CommonName</Label><Width>32</Width></TableColumnHeader>
<TableColumnHeader><Label>FriendlyName</Label><Width>24</Width></TableColumnHeader>
<TableColumnHeader><Label>Status</Label><Width>10</Width></TableColumnHeader>
<TableColumnHeader><Label>SerialNumber</Label><Width>20</Width></TableColumnHeader>
<TableColumnHeader><Label>NotAfterUtc</Label><Width>22</Width></TableColumnHeader>
<TableColumnHeader><Label>HasKey</Label><Width>6</Width></TableColumnHeader>
<TableColumnHeader><Label>CaName</Label><Width>18</Width></TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem><PropertyName>CommonName</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>FriendlyName</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>SerialNumber</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>NotAfterUtc</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>HasPrivateKey</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>CaName</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
@@ -46,4 +46,64 @@
</MemberSet>
</Members>
</Type>
<Type>
<Name>PSInfisicalAPI.Models.InfisicalCertificateAuthority</Name>
<Members>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Name</Name>
<Name>CommonName</Name>
<Name>Type</Name>
<Name>Status</Name>
<Name>KeyAlgorithm</Name>
<Name>NotAfter</Name>
<Name>Id</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</MemberSet>
</Members>
</Type>
<Type>
<Name>PSInfisicalAPI.Models.InfisicalCertificate</Name>
<Members>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>CommonName</Name>
<Name>FriendlyName</Name>
<Name>Status</Name>
<Name>SerialNumber</Name>
<Name>NotAfterUtc</Name>
<Name>HasPrivateKey</Name>
<Name>CaName</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</MemberSet>
</Members>
</Type>
<Type>
<Name>PSInfisicalAPI.Models.InfisicalCertificateBundle</Name>
<Members>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>SerialNumber</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</MemberSet>
</Members>
</Type>
</Types>
+9 -3
View File
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = '2026.06.04.0020'
ModuleVersion = '2026.06.04.0123'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Grace Solutions'
CompanyName = 'Grace Solutions'
@@ -39,7 +39,13 @@
'Get-InfisicalTag',
'New-InfisicalTag',
'Update-InfisicalTag',
'Remove-InfisicalTag'
'Remove-InfisicalTag',
'Get-InfisicalCertificateAuthority',
'Search-InfisicalCertificate',
'ConvertTo-InfisicalCertificate',
'Install-InfisicalCertificate',
'Uninstall-InfisicalCertificate',
'Export-InfisicalCertificate'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -51,7 +57,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 = '211fbcf34dbb'
CommitHash = '2cbd5c2008f5'
}
}
}
Binary file not shown.
+9 -3
View File
@@ -127,7 +127,13 @@ function Write-Manifest {
'Get-InfisicalTag',
'New-InfisicalTag',
'Update-InfisicalTag',
'Remove-InfisicalTag'
'Remove-InfisicalTag',
'Get-InfisicalCertificateAuthority',
'Search-InfisicalCertificate',
'ConvertTo-InfisicalCertificate',
'Install-InfisicalCertificate',
'Uninstall-InfisicalCertificate',
'Export-InfisicalCertificate'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -187,7 +193,7 @@ if (`$null -eq `$manifest) {
Import-Module -Name '$($ModuleDirectory.FullName)' -Force
`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag')
`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Search-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate')
foreach (`$c in `$cmds) {
if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$c"
@@ -283,7 +289,7 @@ $publishArgs = @(
Invoke-DotNet -Arguments $publishArgs
Clear-Directory -Directory $ModuleBinDir
$desiredAssemblies = @('PSInfisicalAPI.dll','Newtonsoft.Json.dll','YamlDotNet.dll')
$desiredAssemblies = @('PSInfisicalAPI.dll','Newtonsoft.Json.dll','YamlDotNet.dll','BouncyCastle.Cryptography.dll')
foreach ($assembly in $desiredAssemblies) {
$source = [System.IO.FileInfo][System.IO.Path]::Combine($publishOutput.FullName, $assembly)
if ($source.Exists) {
@@ -0,0 +1,109 @@
using System;
using System.Reflection;
using PSInfisicalAPI.Models;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class CertificateMapperTests
{
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
private static readonly Type CertMapperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateMapper", true);
private static readonly Type CertDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateResponseDto", true);
private static readonly Type CaMapperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCaMapper", true);
private static readonly Type CaDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalInternalCaResponseDto", true);
private static readonly Type BundleDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateBundleResponseDto", true);
private static InfisicalCertificate InvokeCertMap(object dto, string fallbackProjectId)
{
MethodInfo map = CertMapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
return (InfisicalCertificate)map.Invoke(null, new object[] { dto, fallbackProjectId });
}
private static InfisicalCertificateAuthority InvokeCaMap(object dto, string fallbackProjectId)
{
MethodInfo map = CaMapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
return (InfisicalCertificateAuthority)map.Invoke(null, new object[] { dto, fallbackProjectId });
}
private static InfisicalCertificateBundle InvokeBundleMap(object dto)
{
MethodInfo map = CertMapperType.GetMethod("MapBundle", BindingFlags.Public | BindingFlags.Static);
return (InfisicalCertificateBundle)map.Invoke(null, new object[] { dto });
}
[Fact]
public void CertificateMap_Null_Returns_Null()
{
Assert.Null(InvokeCertMap(null, "proj-x"));
}
[Fact]
public void CertificateMap_Populates_Fields_And_Parses_Timestamps()
{
object dto = Activator.CreateInstance(CertDtoType);
CertDtoType.GetProperty("Id").SetValue(dto, "cert-1");
CertDtoType.GetProperty("SerialNumber").SetValue(dto, "AABBCC");
CertDtoType.GetProperty("CommonName").SetValue(dto, "example.com");
CertDtoType.GetProperty("FriendlyName").SetValue(dto, "Example");
CertDtoType.GetProperty("HasPrivateKey").SetValue(dto, true);
CertDtoType.GetProperty("NotAfter").SetValue(dto, "2030-01-02T03:04:05Z");
InfisicalCertificate mapped = InvokeCertMap(dto, "proj-fallback");
Assert.Equal("cert-1", mapped.Id);
Assert.Equal("AABBCC", mapped.SerialNumber);
Assert.Equal("example.com", mapped.CommonName);
Assert.Equal("Example", mapped.FriendlyName);
Assert.True(mapped.HasPrivateKey);
Assert.Equal("proj-fallback", mapped.ProjectId);
Assert.True(mapped.NotAfterUtc.HasValue);
Assert.Equal(new DateTimeOffset(2030, 1, 2, 3, 4, 5, TimeSpan.Zero), mapped.NotAfterUtc.Value);
}
[Fact]
public void CertificateMap_Explicit_ProjectId_Wins_Over_Fallback()
{
object dto = Activator.CreateInstance(CertDtoType);
CertDtoType.GetProperty("Id").SetValue(dto, "cert-2");
CertDtoType.GetProperty("ProjectId").SetValue(dto, "proj-real");
InfisicalCertificate mapped = InvokeCertMap(dto, "proj-fallback");
Assert.Equal("proj-real", mapped.ProjectId);
}
[Fact]
public void CaMap_Populates_Fields()
{
object dto = Activator.CreateInstance(CaDtoType);
CaDtoType.GetProperty("Id").SetValue(dto, "ca-1");
CaDtoType.GetProperty("Name").SetValue(dto, "internal-root");
CaDtoType.GetProperty("Type").SetValue(dto, "internal");
CaDtoType.GetProperty("Status").SetValue(dto, "active");
CaDtoType.GetProperty("CommonName").SetValue(dto, "Internal Root CA");
InfisicalCertificateAuthority mapped = InvokeCaMap(dto, "proj-fallback");
Assert.Equal("ca-1", mapped.Id);
Assert.Equal("internal-root", mapped.Name);
Assert.Equal("internal", mapped.Type);
Assert.Equal("active", mapped.Status);
Assert.Equal("Internal Root CA", mapped.CommonName);
Assert.Equal("proj-fallback", mapped.ProjectId);
}
[Fact]
public void BundleMap_Maps_All_Pem_Fields()
{
object dto = Activator.CreateInstance(BundleDtoType);
BundleDtoType.GetProperty("SerialNumber").SetValue(dto, "AABBCC");
BundleDtoType.GetProperty("Certificate").SetValue(dto, "CERT-PEM");
BundleDtoType.GetProperty("CertificateChain").SetValue(dto, "CHAIN-PEM");
BundleDtoType.GetProperty("PrivateKey").SetValue(dto, "KEY-PEM");
InfisicalCertificateBundle mapped = InvokeBundleMap(dto);
Assert.Equal("AABBCC", mapped.SerialNumber);
Assert.Equal("CERT-PEM", mapped.CertificatePem);
Assert.Equal("CHAIN-PEM", mapped.CertificateChainPem);
Assert.Equal("KEY-PEM", mapped.PrivateKeyPem);
}
}
}
@@ -0,0 +1,98 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using PSInfisicalAPI.Pki;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class PemCertificateBuilderTests
{
private static (string CertPem, string KeyPem, string Thumbprint) CreateSelfSigned(string commonName)
{
using (RSA rsa = RSA.Create(2048))
{
CertificateRequest request = new CertificateRequest(
"CN=" + commonName,
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
DateTimeOffset notAfter = DateTimeOffset.UtcNow.AddDays(1);
using (X509Certificate2 cert = request.CreateSelfSigned(notBefore, notAfter))
{
byte[] derBytes = cert.Export(X509ContentType.Cert);
string certPem = "-----BEGIN CERTIFICATE-----\n" +
Convert.ToBase64String(derBytes, Base64FormattingOptions.InsertLineBreaks) +
"\n-----END CERTIFICATE-----\n";
byte[] pkcs8 = rsa.ExportPkcs8PrivateKey();
string keyPem = "-----BEGIN PRIVATE KEY-----\n" +
Convert.ToBase64String(pkcs8, Base64FormattingOptions.InsertLineBreaks) +
"\n-----END PRIVATE KEY-----\n";
return (certPem, keyPem, cert.Thumbprint);
}
}
}
[Fact]
public void Build_From_Cert_Only_Returns_X509Certificate2_Without_Key()
{
(string certPem, _, string thumbprint) = CreateSelfSigned("PemBuilderTest.NoKey");
X509Certificate2 cert = PemCertificateBuilder.Build(certPem, null, null, X509KeyStorageFlags.DefaultKeySet);
try
{
Assert.NotNull(cert);
Assert.Equal(thumbprint, cert.Thumbprint);
Assert.False(cert.HasPrivateKey);
}
finally
{
cert.Dispose();
}
}
[Fact]
public void Build_With_Pkcs8_Key_Attaches_Private_Key()
{
(string certPem, string keyPem, string thumbprint) = CreateSelfSigned("PemBuilderTest.WithKey");
X509Certificate2 cert = PemCertificateBuilder.Build(certPem, keyPem, null, X509KeyStorageFlags.Exportable);
try
{
Assert.NotNull(cert);
Assert.Equal(thumbprint, cert.Thumbprint);
Assert.True(cert.HasPrivateKey);
}
finally
{
cert.Dispose();
}
}
[Fact]
public void ReadCertificateChain_Returns_All_Certificates()
{
(string leafPem, _, _) = CreateSelfSigned("PemBuilderTest.Leaf");
(string intermediatePem, _, _) = CreateSelfSigned("PemBuilderTest.Intermediate");
string combined = leafPem + intermediatePem;
System.Collections.Generic.List<X509Certificate2> chain = PemCertificateBuilder.ReadCertificateChain(combined);
try
{
Assert.Equal(2, chain.Count);
}
finally
{
foreach (X509Certificate2 c in chain) { c.Dispose(); }
}
}
[Fact]
public void Build_Empty_Certificate_Pem_Throws()
{
Assert.Throws<ArgumentException>(() => PemCertificateBuilder.Build(null, null, null, X509KeyStorageFlags.DefaultKeySet));
}
}
}
@@ -0,0 +1,70 @@
using System.Collections.Generic;
using PSInfisicalAPI.Endpoints;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class PkiEndpointRegistryTests
{
[Fact]
public void Get_ListInternalCertificateAuthorities_Returns_CertManager_Primary()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListInternalCertificateAuthorities);
Assert.Equal("GET", definition.Method);
Assert.Equal("v1", definition.Version);
Assert.Equal("/api/v1/cert-manager/ca/internal", definition.Template);
Assert.True(definition.RequiresAuthorization);
}
[Fact]
public void Get_RetrieveInternalCertificateAuthority_Has_CaId_Placeholder_Under_CertManager()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveInternalCertificateAuthority);
Assert.Equal("GET", definition.Method);
Assert.Equal("/api/v1/cert-manager/ca/internal/{caId}", definition.Template);
}
[Fact]
public void Get_SearchCertificates_Is_Post_With_ProjectId_Placeholder()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.SearchCertificates);
Assert.Equal("POST", definition.Method);
Assert.Equal("/api/v1/projects/{projectId}/certificates/search", definition.Template);
Assert.True(definition.RequiresAuthorization);
}
[Fact]
public void Get_GetCertificateBundle_Marks_Response_As_Secret_With_Pki_Primary()
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.GetCertificateBundle);
Assert.Equal("GET", definition.Method);
Assert.Equal("/api/v1/pki/certificates/{serialNumber}/bundle", definition.Template);
Assert.True(definition.ContainsSecretMaterialInResponse);
Assert.True(definition.RequiresAuthorization);
}
[Fact]
public void Candidates_For_ListInternalCertificateAuthorities_Include_Pki_Legacy_Alias()
{
IReadOnlyList<InfisicalEndpointDefinition> candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.ListInternalCertificateAuthorities);
Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/ca/internal");
Assert.Contains(candidates, c => c.Template == "/api/v1/pki/ca/internal");
}
[Fact]
public void Candidates_For_RetrieveInternalCertificateAuthority_Include_Pki_Legacy_Alias()
{
IReadOnlyList<InfisicalEndpointDefinition> candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.RetrieveInternalCertificateAuthority);
Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/ca/internal/{caId}");
Assert.Contains(candidates, c => c.Template == "/api/v1/pki/ca/internal/{caId}");
}
[Fact]
public void Candidates_For_GetCertificateBundle_Include_CertManager_Alias()
{
IReadOnlyList<InfisicalEndpointDefinition> candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.GetCertificateBundle);
Assert.Contains(candidates, c => c.Template == "/api/v1/pki/certificates/{serialNumber}/bundle");
Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/certificates/{serialNumber}/bundle");
}
}
}
@@ -0,0 +1,84 @@
using System;
using System.Management.Automation;
using System.Security.Cryptography.X509Certificates;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsData.ConvertTo, "InfisicalCertificate", DefaultParameterSetName = "FromPipeline")]
[OutputType(typeof(X509Certificate2))]
public sealed class ConvertToInfisicalCertificateCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "FromPipeline", Mandatory = true, ValueFromPipeline = true)]
public InfisicalCertificate Certificate { get; set; }
[Parameter(ParameterSetName = "FromBundle", Mandatory = true, ValueFromPipeline = true)]
public InfisicalCertificateBundle Bundle { get; set; }
[Parameter(ParameterSetName = "FromSerial", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
public string SerialNumber { get; set; }
[Parameter] public SwitchParameter NoPrivateKey { get; set; }
[Parameter] public X509KeyStorageFlags KeyStorageFlags { get; set; } = X509KeyStorageFlags.DefaultKeySet;
[Parameter] public SwitchParameter IncludeChain { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalCertificateBundle resolvedBundle = ResolveBundle();
if (resolvedBundle == null)
{
return;
}
string privateKeyPem = NoPrivateKey.IsPresent ? null : resolvedBundle.PrivateKeyPem;
X509Certificate2 cert = PemCertificateBuilder.Build(resolvedBundle.CertificatePem, privateKeyPem, resolvedBundle.CertificateChainPem, KeyStorageFlags);
WriteObject(cert);
if (IncludeChain.IsPresent)
{
foreach (X509Certificate2 chainCert in PemCertificateBuilder.ReadCertificateChain(resolvedBundle.CertificateChainPem))
{
WriteObject(chainCert);
}
}
}
catch (Exception exception)
{
ThrowTerminatingForException("ConvertToInfisicalCertificateCmdlet", "ConvertToCertificate", exception);
}
}
private InfisicalCertificateBundle ResolveBundle()
{
if (string.Equals(ParameterSetName, "FromBundle", StringComparison.Ordinal))
{
return Bundle;
}
string serial = null;
if (string.Equals(ParameterSetName, "FromSerial", StringComparison.Ordinal))
{
serial = SerialNumber;
}
else if (string.Equals(ParameterSetName, "FromPipeline", StringComparison.Ordinal) && Certificate != null)
{
serial = Certificate.SerialNumber;
}
if (string.IsNullOrEmpty(serial))
{
return null;
}
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
return client.GetCertificateBundle(connection, serial);
}
}
}
@@ -0,0 +1,161 @@
using System;
using System.IO;
using System.Management.Automation;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
using PSInfisicalAPI.Security;
namespace PSInfisicalAPI.Cmdlets
{
public enum InfisicalCertificateExportFormat
{
Pem,
Pfx,
Cer
}
[Cmdlet(VerbsData.Export, "InfisicalCertificate", SupportsShouldProcess = true, DefaultParameterSetName = "FromCertificate")]
[OutputType(typeof(FileInfo))]
public sealed class ExportInfisicalCertificateCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "FromCertificate", Mandatory = true, ValueFromPipeline = true)]
public X509Certificate2 Certificate { get; set; }
[Parameter(ParameterSetName = "FromBundle", Mandatory = true, ValueFromPipeline = true)]
public InfisicalCertificateBundle Bundle { get; set; }
[Parameter(ParameterSetName = "FromInfisical", Mandatory = true, ValueFromPipeline = true)]
public InfisicalCertificate InfisicalCertificate { get; set; }
[Parameter(ParameterSetName = "FromSerial", Mandatory = true, Position = 1)]
public string SerialNumber { get; set; }
[Parameter(Mandatory = true, Position = 0)] public string Path { get; set; }
[Parameter(Mandatory = true)] public InfisicalCertificateExportFormat Format { get; set; }
[Parameter] public SecureString Password { get; set; }
[Parameter] public SwitchParameter IncludeChain { get; set; }
[Parameter] public SwitchParameter NoPrivateKey { get; set; }
[Parameter] public SwitchParameter Force { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
string resolvedPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(Path);
if (File.Exists(resolvedPath) && !Force.IsPresent)
{
throw new IOException(string.Concat("File '", resolvedPath, "' already exists. Pass -Force to overwrite."));
}
if (!ShouldProcess(resolvedPath, string.Concat("Export certificate as ", Format.ToString())))
{
return;
}
InfisicalCertificateBundle bundle = null;
X509Certificate2 cert = ResolveCertificate(out bundle);
if (cert == null && bundle == null)
{
return;
}
switch (Format)
{
case InfisicalCertificateExportFormat.Pem:
WritePem(resolvedPath, cert, bundle);
break;
case InfisicalCertificateExportFormat.Pfx:
WritePfx(resolvedPath, cert);
break;
case InfisicalCertificateExportFormat.Cer:
WriteCer(resolvedPath, cert);
break;
}
Logger.Information("ExportInfisicalCertificateCmdlet", string.Concat("Exported certificate to '", resolvedPath, "'."));
if (PassThru.IsPresent)
{
WriteObject(new FileInfo(resolvedPath));
}
}
catch (Exception exception)
{
ThrowTerminatingForException("ExportInfisicalCertificateCmdlet", "ExportCertificate", exception);
}
}
private X509Certificate2 ResolveCertificate(out InfisicalCertificateBundle bundle)
{
bundle = null;
if (string.Equals(ParameterSetName, "FromCertificate", StringComparison.Ordinal))
{
return Certificate;
}
if (string.Equals(ParameterSetName, "FromBundle", StringComparison.Ordinal))
{
bundle = Bundle;
}
else
{
string serial = string.Equals(ParameterSetName, "FromSerial", StringComparison.Ordinal)
? SerialNumber
: (InfisicalCertificate != null ? InfisicalCertificate.SerialNumber : null);
if (string.IsNullOrEmpty(serial)) { return null; }
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
bundle = client.GetCertificateBundle(connection, serial);
}
if (bundle == null) { return null; }
string keyPem = NoPrivateKey.IsPresent ? null : bundle.PrivateKeyPem;
return PemCertificateBuilder.Build(bundle.CertificatePem, keyPem, bundle.CertificateChainPem, X509KeyStorageFlags.Exportable);
}
private void WritePem(string path, X509Certificate2 cert, InfisicalCertificateBundle bundle)
{
StringBuilder sb = new StringBuilder();
if (bundle != null && !string.IsNullOrEmpty(bundle.CertificatePem))
{
sb.Append(bundle.CertificatePem.TrimEnd()).Append('\n');
if (IncludeChain.IsPresent && !string.IsNullOrEmpty(bundle.CertificateChainPem))
{
sb.Append(bundle.CertificateChainPem.TrimEnd()).Append('\n');
}
if (!NoPrivateKey.IsPresent && !string.IsNullOrEmpty(bundle.PrivateKeyPem))
{
sb.Append(bundle.PrivateKeyPem.TrimEnd()).Append('\n');
}
}
else
{
sb.Append("-----BEGIN CERTIFICATE-----\n");
sb.Append(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
sb.Append("\n-----END CERTIFICATE-----\n");
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
private void WritePfx(string path, X509Certificate2 cert)
{
byte[] bytes = Password != null
? SecureStringUtility.UsePlainText(Password, plain => cert.Export(X509ContentType.Pfx, plain ?? string.Empty))
: cert.Export(X509ContentType.Pfx);
File.WriteAllBytes(path, bytes);
}
private void WriteCer(string path, X509Certificate2 cert)
{
File.WriteAllBytes(path, cert.Export(X509ContentType.Cert));
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "InfisicalCertificateAuthority", DefaultParameterSetName = "List")]
[OutputType(typeof(InfisicalCertificateAuthority))]
public sealed class GetInfisicalCertificateAuthorityCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "ById", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id")]
public string CaId { get; set; }
[Parameter] public string ProjectId { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
string resolvedProjectId = ResolveProjectId(connection, ProjectId);
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal))
{
InfisicalCertificateAuthority ca = client.GetInternalCertificateAuthority(connection, CaId, resolvedProjectId);
if (ca != null)
{
WriteObject(ca);
}
return;
}
InfisicalCertificateAuthority[] all = client.ListInternalCertificateAuthorities(connection, resolvedProjectId);
foreach (InfisicalCertificateAuthority ca in all)
{
WriteObject(ca);
}
}
catch (Exception exception)
{
ThrowTerminatingForException("GetInfisicalCertificateAuthorityCmdlet", "GetCertificateAuthority", exception);
}
}
}
}
@@ -0,0 +1,149 @@
using System;
using System.Management.Automation;
using System.Security.Cryptography.X509Certificates;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsLifecycle.Install, "InfisicalCertificate", SupportsShouldProcess = true, DefaultParameterSetName = "FromCertificate")]
[OutputType(typeof(X509Certificate2))]
public sealed class InstallInfisicalCertificateCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "FromCertificate", Mandatory = true, ValueFromPipeline = true)]
public X509Certificate2 Certificate { get; set; }
[Parameter(ParameterSetName = "FromInfisical", Mandatory = true, ValueFromPipeline = true)]
public InfisicalCertificate InfisicalCertificate { get; set; }
[Parameter(ParameterSetName = "FromSerial", Mandatory = true, Position = 0)]
public string SerialNumber { get; set; }
[Parameter] public StoreName StoreName { get; set; } = StoreName.My;
[Parameter] public StoreLocation StoreLocation { get; set; } = StoreLocation.CurrentUser;
[Parameter] public X509KeyStorageFlags KeyStorageFlags { get; set; } = X509KeyStorageFlags.DefaultKeySet;
[Parameter] public SwitchParameter IncludeChain { get; set; }
[Parameter] public SwitchParameter NoPrivateKey { get; set; }
[Parameter] public SwitchParameter Force { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
X509Certificate2 cert = ResolveCertificate();
if (cert == null)
{
return;
}
InstallCertificate(cert, StoreName, StoreLocation);
if (IncludeChain.IsPresent && string.Equals(ParameterSetName, "FromCertificate", StringComparison.Ordinal) == false)
{
foreach (X509Certificate2 chainCert in ResolveChain())
{
InstallCertificate(chainCert, StoreName.CertificateAuthority, StoreLocation);
}
}
if (PassThru.IsPresent)
{
WriteObject(cert);
}
}
catch (Exception exception)
{
ThrowTerminatingForException("InstallInfisicalCertificateCmdlet", "InstallCertificate", exception);
}
}
private void InstallCertificate(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation)
{
string target = string.Concat(storeLocation.ToString(), @"\", storeName.ToString(), " [", cert.Thumbprint, "]");
X509Store store = new X509Store(storeName, storeLocation);
try
{
store.Open(OpenFlags.ReadWrite);
X509Certificate2Collection existing = store.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, false);
if (existing.Count > 0)
{
if (!Force.IsPresent)
{
Logger.Information("InstallInfisicalCertificateCmdlet", string.Concat("Certificate already present in ", target, "; no action taken."));
return;
}
if (!ShouldProcess(target, "Replace existing certificate"))
{
return;
}
store.RemoveRange(existing);
}
else if (!ShouldProcess(target, "Install certificate"))
{
return;
}
store.Add(cert);
Logger.Information("InstallInfisicalCertificateCmdlet", string.Concat("Installed certificate to ", target, "."));
}
finally
{
store.Close();
}
}
private X509Certificate2 ResolveCertificate()
{
if (string.Equals(ParameterSetName, "FromCertificate", StringComparison.Ordinal))
{
return Certificate;
}
string serial = null;
if (string.Equals(ParameterSetName, "FromSerial", StringComparison.Ordinal))
{
serial = SerialNumber;
}
else if (InfisicalCertificate != null)
{
serial = InfisicalCertificate.SerialNumber;
}
if (string.IsNullOrEmpty(serial))
{
return null;
}
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
InfisicalCertificateBundle bundle = client.GetCertificateBundle(connection, serial);
if (bundle == null)
{
return null;
}
string keyPem = NoPrivateKey.IsPresent ? null : bundle.PrivateKeyPem;
return PemCertificateBuilder.Build(bundle.CertificatePem, keyPem, bundle.CertificateChainPem, KeyStorageFlags);
}
private System.Collections.Generic.List<X509Certificate2> ResolveChain()
{
string serial = string.Equals(ParameterSetName, "FromSerial", StringComparison.Ordinal)
? SerialNumber
: (InfisicalCertificate != null ? InfisicalCertificate.SerialNumber : null);
if (string.IsNullOrEmpty(serial))
{
return new System.Collections.Generic.List<X509Certificate2>();
}
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
InfisicalCertificateBundle bundle = client.GetCertificateBundle(connection, serial);
return bundle != null ? PemCertificateBuilder.ReadCertificateChain(bundle.CertificateChainPem) : new System.Collections.Generic.List<X509Certificate2>();
}
}
}
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Search, "InfisicalCertificate")]
[OutputType(typeof(InfisicalCertificate))]
public sealed class SearchInfisicalCertificateCmdlet : InfisicalCmdletBase
{
[Parameter] public string ProjectId { get; set; }
[Parameter] public string FriendlyName { get; set; }
[Parameter] public string CommonName { get; set; }
[Parameter] public string Search { get; set; }
[Parameter] public string Status { get; set; }
[Parameter] public string[] CaId { get; set; }
[Parameter] public string[] ProfileId { get; set; }
[Parameter] public string[] ApplicationId { get; set; }
[Parameter] public string[] EnrollmentType { get; set; }
[Parameter] public string ExtendedKeyUsage { get; set; }
[Parameter] public string[] KeyAlgorithm { get; set; }
[Parameter] public string SignatureAlgorithm { get; set; }
[Parameter] public string[] Source { get; set; }
[Parameter] public DateTimeOffset? NotAfterFrom { get; set; }
[Parameter] public DateTimeOffset? NotAfterTo { get; set; }
[Parameter] public DateTimeOffset? NotBeforeFrom { get; set; }
[Parameter] public DateTimeOffset? NotBeforeTo { get; set; }
[Parameter] public string SortBy { get; set; }
[Parameter] [ValidateSet("asc", "desc")] public string SortOrder { get; set; }
[Parameter] public int? Limit { get; set; }
[Parameter] public int? Offset { get; set; }
[Parameter] public SwitchParameter NoAutoPage { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
string resolvedProjectId = ResolveProjectId(connection, ProjectId);
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
InfisicalCertificateSearchQuery query = BuildQuery(resolvedProjectId);
int requestedLimit = query.Limit ?? 100;
query.Limit = requestedLimit;
query.Offset = query.Offset ?? 0;
int emitted = 0;
while (true)
{
InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query);
if (page == null || page.Certificates == null || page.Certificates.Length == 0)
{
break;
}
foreach (InfisicalCertificate cert in page.Certificates)
{
WriteObject(cert);
emitted++;
}
if (NoAutoPage.IsPresent || page.Certificates.Length < requestedLimit)
{
break;
}
if (page.TotalCount > 0 && emitted >= page.TotalCount)
{
break;
}
query.Offset = (query.Offset ?? 0) + page.Certificates.Length;
}
}
catch (Exception exception)
{
ThrowTerminatingForException("SearchInfisicalCertificateCmdlet", "SearchCertificates", exception);
}
}
private InfisicalCertificateSearchQuery BuildQuery(string projectId)
{
return new InfisicalCertificateSearchQuery
{
ProjectId = projectId,
FriendlyName = FriendlyName,
CommonName = CommonName,
Search = Search,
Status = Status,
CaIds = CaId,
ProfileIds = ProfileId,
ApplicationIds = ApplicationId,
EnrollmentTypes = EnrollmentType,
ExtendedKeyUsage = ExtendedKeyUsage,
KeyAlgorithm = KeyAlgorithm,
SignatureAlgorithm = SignatureAlgorithm,
Source = Source,
NotAfterFrom = NotAfterFrom,
NotAfterTo = NotAfterTo,
NotBeforeFrom = NotBeforeFrom,
NotBeforeTo = NotBeforeTo,
SortBy = SortBy,
SortOrder = SortOrder,
Limit = Limit,
Offset = Offset
};
}
}
}
@@ -0,0 +1,120 @@
using System;
using System.Management.Automation;
using System.Security.Cryptography.X509Certificates;
using PSInfisicalAPI.Models;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsLifecycle.Uninstall, "InfisicalCertificate", SupportsShouldProcess = true, DefaultParameterSetName = "ByThumbprint")]
[OutputType(typeof(X509Certificate2))]
public sealed class UninstallInfisicalCertificateCmdlet : InfisicalCmdletBase
{
[Parameter(ParameterSetName = "ByThumbprint", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
public string Thumbprint { get; set; }
[Parameter(ParameterSetName = "ByCertificate", Mandatory = true, ValueFromPipeline = true)]
public X509Certificate2 Certificate { get; set; }
[Parameter(ParameterSetName = "ByInfisical", Mandatory = true, ValueFromPipeline = true)]
public InfisicalCertificate InfisicalCertificate { get; set; }
[Parameter(ParameterSetName = "BySubject", Mandatory = true)]
public string Subject { get; set; }
[Parameter] public StoreName StoreName { get; set; } = StoreName.My;
[Parameter] public StoreLocation StoreLocation { get; set; } = StoreLocation.CurrentUser;
[Parameter] public SwitchParameter Force { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
X509Store store = new X509Store(StoreName, StoreLocation);
try
{
store.Open(OpenFlags.ReadWrite);
X509Certificate2Collection matches = FindMatches(store);
string target = string.Concat(StoreLocation.ToString(), @"\", StoreName.ToString());
if (matches == null || matches.Count == 0)
{
Logger.Information("UninstallInfisicalCertificateCmdlet", string.Concat("No matching certificates found in ", target, "; no action taken."));
return;
}
if (matches.Count > 1 && !Force.IsPresent)
{
throw new InvalidOperationException(string.Concat(
"Found ", matches.Count.ToString(System.Globalization.CultureInfo.InvariantCulture),
" matching certificates in ", target, ". Pass -Force to remove all of them."));
}
foreach (X509Certificate2 match in matches)
{
string description = string.Concat(target, " [", match.Thumbprint, "]");
if (!ShouldProcess(description, "Remove certificate"))
{
continue;
}
store.Remove(match);
Logger.Information("UninstallInfisicalCertificateCmdlet", string.Concat("Removed certificate from ", description, "."));
if (PassThru.IsPresent)
{
WriteObject(match);
}
}
}
finally
{
store.Close();
}
}
catch (Exception exception)
{
ThrowTerminatingForException("UninstallInfisicalCertificateCmdlet", "UninstallCertificate", exception);
}
}
private X509Certificate2Collection FindMatches(X509Store store)
{
string thumbprint = ResolveThumbprint();
if (!string.IsNullOrEmpty(thumbprint))
{
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
}
if (string.Equals(ParameterSetName, "BySubject", StringComparison.Ordinal))
{
return store.Certificates.Find(X509FindType.FindBySubjectName, Subject, false);
}
return null;
}
private string ResolveThumbprint()
{
if (string.Equals(ParameterSetName, "ByThumbprint", StringComparison.Ordinal))
{
return Thumbprint;
}
if (string.Equals(ParameterSetName, "ByCertificate", StringComparison.Ordinal) && Certificate != null)
{
return Certificate.Thumbprint;
}
if (string.Equals(ParameterSetName, "ByInfisical", StringComparison.Ordinal) && InfisicalCertificate != null)
{
if (!string.IsNullOrEmpty(InfisicalCertificate.FingerprintSha1))
{
return InfisicalCertificate.FingerprintSha1.Replace(":", string.Empty).Replace(" ", string.Empty);
}
}
return null;
}
}
}
@@ -43,5 +43,11 @@ namespace PSInfisicalAPI.Endpoints
public const string CreateTag = "CreateTag";
public const string UpdateTag = "UpdateTag";
public const string DeleteTag = "DeleteTag";
public const string ListInternalCertificateAuthorities = "ListInternalCertificateAuthorities";
public const string RetrieveInternalCertificateAuthority = "RetrieveInternalCertificateAuthority";
public const string SearchCertificates = "SearchCertificates";
public const string RetrieveCertificate = "RetrieveCertificate";
public const string GetCertificateBundle = "GetCertificateBundle";
}
}
@@ -16,6 +16,7 @@ namespace PSInfisicalAPI.Endpoints
RegisterEnvironments(Candidates);
RegisterFolders(Candidates);
RegisterTags(Candidates);
RegisterPki(Candidates);
}
private static void Add(Dictionary<string, List<InfisicalEndpointDefinition>> map, InfisicalEndpointDefinition definition)
@@ -495,6 +496,101 @@ namespace PSInfisicalAPI.Endpoints
});
}
private static void RegisterPki(Dictionary<string, List<InfisicalEndpointDefinition>> map)
{
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListInternalCertificateAuthorities,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/cert-manager/ca/internal",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListInternalCertificateAuthorities,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/pki/ca/internal",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveInternalCertificateAuthority,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/cert-manager/ca/internal/{caId}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveInternalCertificateAuthority,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/pki/ca/internal/{caId}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.SearchCertificates,
Resource = "Pki",
Version = "v1",
Method = "POST",
Template = "/api/v1/projects/{projectId}/certificates/search",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveCertificate,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/pki/certificates/{serialNumber}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveCertificate,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/cert-manager/certificates/{serialNumber}",
RequiresAuthorization = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.GetCertificateBundle,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/pki/certificates/{serialNumber}/bundle",
RequiresAuthorization = true,
ContainsSecretMaterialInResponse = true
});
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.GetCertificateBundle,
Resource = "Pki",
Version = "v1",
Method = "GET",
Template = "/api/v1/cert-manager/certificates/{serialNumber}/bundle",
RequiresAuthorization = true,
ContainsSecretMaterialInResponse = true
});
}
public static InfisicalEndpointDefinition Get(string name)
{
List<InfisicalEndpointDefinition> list = GetCandidatesInternal(name);
@@ -43,6 +43,53 @@ namespace PSInfisicalAPI.Http
throw exception;
}
public InfisicalHttpResponse InvokeWithCandidateFallback(
InfisicalConnection connection,
string endpointName,
string operationName,
IDictionary<string, string> pathParameters,
IEnumerable<KeyValuePair<string, string>> queryParameters,
string body)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(endpointName)) { throw new ArgumentNullException(nameof(endpointName)); }
IReadOnlyList<InfisicalEndpointDefinition> candidates = InfisicalEndpointRegistry.GetCandidates(endpointName);
InfisicalApiException lastException = null;
for (int index = 0; index < candidates.Count; index++)
{
InfisicalEndpointDefinition definition = candidates[index];
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters);
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body);
if (response.StatusCode >= 200 && response.StatusCode < 300)
{
return response;
}
InfisicalApiException exception = BuildApiException(response, definition);
response.Clear();
bool hasMoreCandidates = (index + 1) < candidates.Count;
if (hasMoreCandidates && IsRouteAliasMismatch(exception))
{
lastException = exception;
continue;
}
throw exception;
}
throw lastException ?? new InfisicalApiException(string.Concat(
"All route candidates exhausted for '", endpointName, "'."));
}
private static bool IsRouteAliasMismatch(InfisicalApiException exception)
{
return exception.StatusCode == 404 || exception.StatusCode == 405;
}
private InfisicalHttpResponse ExecuteAuthorized(
InfisicalConnection connection,
InfisicalEndpointDefinition definition,
@@ -0,0 +1,55 @@
using System;
namespace PSInfisicalAPI.Models
{
public sealed class InfisicalCertificate
{
public string Id { get; set; }
public string ProjectId { get; set; }
public string CaId { get; set; }
public string CaName { get; set; }
public string CaCertId { get; set; }
public string CertificateTemplateId { get; set; }
public string ProfileId { get; set; }
public string ProfileName { get; set; }
public string ApplicationId { get; set; }
public string ApplicationName { get; set; }
public string PkiSubscriberId { get; set; }
public string Status { get; set; }
public string SerialNumber { get; set; }
public string FriendlyName { get; set; }
public string CommonName { get; set; }
public string AltNames { get; set; }
public string[] KeyUsages { get; set; }
public string[] ExtendedKeyUsages { get; set; }
public string KeyAlgorithm { get; set; }
public string SignatureAlgorithm { get; set; }
public string SubjectOrganization { get; set; }
public string SubjectOrganizationalUnit { get; set; }
public string SubjectCountry { get; set; }
public string SubjectState { get; set; }
public string SubjectLocality { get; set; }
public string FingerprintSha256 { get; set; }
public string FingerprintSha1 { get; set; }
public bool? IsCA { get; set; }
public int? PathLength { get; set; }
public string Source { get; set; }
public string EnrollmentType { get; set; }
public bool HasPrivateKey { get; set; }
public int? RevocationReason { get; set; }
public string RenewalError { get; set; }
public int? RenewBeforeDays { get; set; }
public string RenewedFromCertificateId { get; set; }
public string RenewedByCertificateId { get; set; }
public DateTimeOffset? NotBeforeUtc { get; set; }
public DateTimeOffset? NotAfterUtc { get; set; }
public DateTimeOffset? RevokedAtUtc { get; set; }
public DateTimeOffset? CreatedAtUtc { get; set; }
public DateTimeOffset? UpdatedAtUtc { get; set; }
public override string ToString()
{
return FriendlyName ?? CommonName ?? SerialNumber ?? Id;
}
}
}
@@ -0,0 +1,36 @@
using System;
namespace PSInfisicalAPI.Models
{
public sealed class InfisicalCertificateAuthority
{
public string Id { get; set; }
public string ProjectId { get; set; }
public string Name { get; set; }
public string FriendlyName { get; set; }
public string Type { get; set; }
public string Status { get; set; }
public bool? EnableDirectIssuance { get; set; }
public string KeyAlgorithm { get; set; }
public string DistinguishedName { get; set; }
public string OrganizationName { get; set; }
public string OrganizationUnit { get; set; }
public string Country { get; set; }
public string State { get; set; }
public string Locality { get; set; }
public string CommonName { get; set; }
public int? MaxPathLength { get; set; }
public string NotBefore { get; set; }
public string NotAfter { get; set; }
public string SerialNumber { get; set; }
public string ParentCaId { get; set; }
public string ActiveCaCertId { get; set; }
public DateTimeOffset? CreatedAtUtc { get; set; }
public DateTimeOffset? UpdatedAtUtc { get; set; }
public override string ToString()
{
return FriendlyName ?? Name ?? CommonName ?? Id;
}
}
}
@@ -0,0 +1,15 @@
namespace PSInfisicalAPI.Models
{
public sealed class InfisicalCertificateBundle
{
public string SerialNumber { get; set; }
public string CertificatePem { get; set; }
public string CertificateChainPem { get; set; }
public string PrivateKeyPem { get; set; }
public override string ToString()
{
return SerialNumber;
}
}
}
+1
View File
@@ -21,6 +21,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="YamlDotNet" Version="15.1.6" />
+44
View File
@@ -0,0 +1,44 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace PSInfisicalAPI.Pki
{
internal sealed class InfisicalInternalCaResponseDto
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("projectId")] public string ProjectId { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("friendlyName")] public string FriendlyName { get; set; }
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("enableDirectIssuance")] public bool? EnableDirectIssuance { get; set; }
[JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; }
[JsonProperty("dn")] public string DistinguishedName { get; set; }
[JsonProperty("organization")] public string OrganizationName { get; set; }
[JsonProperty("ou")] public string OrganizationUnit { get; set; }
[JsonProperty("country")] public string Country { get; set; }
[JsonProperty("province")] public string State { get; set; }
[JsonProperty("locality")] public string Locality { get; set; }
[JsonProperty("commonName")] public string CommonName { get; set; }
[JsonProperty("maxPathLength")] public int? MaxPathLength { get; set; }
[JsonProperty("notBefore")] public string NotBefore { get; set; }
[JsonProperty("notAfter")] public string NotAfter { get; set; }
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
[JsonProperty("parentCaId")] public string ParentCaId { get; set; }
[JsonProperty("activeCaCertId")] public string ActiveCaCertId { get; set; }
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
}
internal sealed class InfisicalInternalCaListResponseDto
{
[JsonProperty("certificateAuthorities")] public List<InfisicalInternalCaResponseDto> CertificateAuthorities { get; set; }
[JsonProperty("cas")] public List<InfisicalInternalCaResponseDto> Cas { get; set; }
}
internal sealed class InfisicalInternalCaSingleResponseDto
{
[JsonProperty("certificateAuthority")] public InfisicalInternalCaResponseDto CertificateAuthority { get; set; }
[JsonProperty("ca")] public InfisicalInternalCaResponseDto Ca { get; set; }
}
}
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using PSInfisicalAPI.Models;
namespace PSInfisicalAPI.Pki
{
internal static class InfisicalCaMapper
{
public static InfisicalCertificateAuthority Map(InfisicalInternalCaResponseDto dto, string fallbackProjectId)
{
if (dto == null)
{
return null;
}
return new InfisicalCertificateAuthority
{
Id = dto.Id,
ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId,
Name = dto.Name,
FriendlyName = dto.FriendlyName,
Type = dto.Type,
Status = dto.Status,
EnableDirectIssuance = dto.EnableDirectIssuance,
KeyAlgorithm = dto.KeyAlgorithm,
DistinguishedName = dto.DistinguishedName,
OrganizationName = dto.OrganizationName,
OrganizationUnit = dto.OrganizationUnit,
Country = dto.Country,
State = dto.State,
Locality = dto.Locality,
CommonName = dto.CommonName,
MaxPathLength = dto.MaxPathLength,
NotBefore = dto.NotBefore,
NotAfter = dto.NotAfter,
SerialNumber = dto.SerialNumber,
ParentCaId = dto.ParentCaId,
ActiveCaCertId = dto.ActiveCaCertId,
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
};
}
public static InfisicalCertificateAuthority[] MapMany(IEnumerable<InfisicalInternalCaResponseDto> items, string fallbackProjectId)
{
if (items == null)
{
return Array.Empty<InfisicalCertificateAuthority>();
}
List<InfisicalCertificateAuthority> results = new List<InfisicalCertificateAuthority>();
foreach (InfisicalInternalCaResponseDto dto in items)
{
InfisicalCertificateAuthority mapped = Map(dto, fallbackProjectId);
if (mapped != null)
{
results.Add(mapped);
}
}
return results.ToArray();
}
private static DateTimeOffset? ParseTimestamp(string value)
{
if (string.IsNullOrEmpty(value))
{
return null;
}
DateTimeOffset parsed;
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed))
{
return parsed;
}
return null;
}
}
}
@@ -0,0 +1,97 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace PSInfisicalAPI.Pki
{
internal sealed class InfisicalCertificateResponseDto
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("projectId")] public string ProjectId { get; set; }
[JsonProperty("caId")] public string CaId { get; set; }
[JsonProperty("caName")] public string CaName { get; set; }
[JsonProperty("caCertId")] public string CaCertId { get; set; }
[JsonProperty("certificateTemplateId")] public string CertificateTemplateId { get; set; }
[JsonProperty("profileId")] public string ProfileId { get; set; }
[JsonProperty("profileName")] public string ProfileName { get; set; }
[JsonProperty("applicationId")] public string ApplicationId { get; set; }
[JsonProperty("applicationName")] public string ApplicationName { get; set; }
[JsonProperty("pkiSubscriberId")] public string PkiSubscriberId { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
[JsonProperty("friendlyName")] public string FriendlyName { get; set; }
[JsonProperty("commonName")] public string CommonName { get; set; }
[JsonProperty("altNames")] public string AltNames { get; set; }
[JsonProperty("keyUsages")] public List<string> KeyUsages { get; set; }
[JsonProperty("extendedKeyUsages")] public List<string> ExtendedKeyUsages { get; set; }
[JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; }
[JsonProperty("signatureAlgorithm")] public string SignatureAlgorithm { get; set; }
[JsonProperty("subjectOrganization")] public string SubjectOrganization { get; set; }
[JsonProperty("subjectOrganizationalUnit")] public string SubjectOrganizationalUnit { get; set; }
[JsonProperty("subjectCountry")] public string SubjectCountry { get; set; }
[JsonProperty("subjectState")] public string SubjectState { get; set; }
[JsonProperty("subjectLocality")] public string SubjectLocality { get; set; }
[JsonProperty("fingerprintSha256")] public string FingerprintSha256 { get; set; }
[JsonProperty("fingerprintSha1")] public string FingerprintSha1 { get; set; }
[JsonProperty("isCA")] public bool? IsCA { get; set; }
[JsonProperty("pathLength")] public int? PathLength { get; set; }
[JsonProperty("source")] public string Source { get; set; }
[JsonProperty("enrollmentType")] public string EnrollmentType { get; set; }
[JsonProperty("hasPrivateKey")] public bool HasPrivateKey { get; set; }
[JsonProperty("revocationReason")] public int? RevocationReason { get; set; }
[JsonProperty("renewalError")] public string RenewalError { get; set; }
[JsonProperty("renewBeforeDays")] public int? RenewBeforeDays { get; set; }
[JsonProperty("renewedFromCertificateId")] public string RenewedFromCertificateId { get; set; }
[JsonProperty("renewedByCertificateId")] public string RenewedByCertificateId { get; set; }
[JsonProperty("notBefore")] public string NotBefore { get; set; }
[JsonProperty("notAfter")] public string NotAfter { get; set; }
[JsonProperty("revokedAt")] public string RevokedAt { get; set; }
[JsonProperty("createdAt")] public string CreatedAt { get; set; }
[JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
}
internal sealed class InfisicalCertificateSearchResponseDto
{
[JsonProperty("certificates")] public List<InfisicalCertificateResponseDto> Certificates { get; set; }
[JsonProperty("totalCount")] public int TotalCount { get; set; }
}
internal sealed class InfisicalCertificateSingleResponseDto
{
[JsonProperty("certificate")] public InfisicalCertificateResponseDto Certificate { get; set; }
}
internal sealed class InfisicalCertificateSearchRequestDto
{
[JsonProperty("friendlyName", NullValueHandling = NullValueHandling.Ignore)] public string FriendlyName { get; set; }
[JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; }
[JsonProperty("search", NullValueHandling = NullValueHandling.Ignore)] public string Search { get; set; }
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public string Status { get; set; }
[JsonProperty("offset", NullValueHandling = NullValueHandling.Ignore)] public int? Offset { get; set; }
[JsonProperty("limit", NullValueHandling = NullValueHandling.Ignore)] public int? Limit { get; set; }
[JsonProperty("caIds", NullValueHandling = NullValueHandling.Ignore)] public string[] CaIds { get; set; }
[JsonProperty("profileIds", NullValueHandling = NullValueHandling.Ignore)] public string[] ProfileIds { get; set; }
[JsonProperty("applicationIds", NullValueHandling = NullValueHandling.Ignore)] public string[] ApplicationIds { get; set; }
[JsonProperty("enrollmentTypes", NullValueHandling = NullValueHandling.Ignore)] public string[] EnrollmentTypes { get; set; }
[JsonProperty("extendedKeyUsage", NullValueHandling = NullValueHandling.Ignore)] public string ExtendedKeyUsage { get; set; }
[JsonProperty("keyAlgorithm", NullValueHandling = NullValueHandling.Ignore)] public string[] KeyAlgorithm { get; set; }
[JsonProperty("signatureAlgorithm", NullValueHandling = NullValueHandling.Ignore)] public string SignatureAlgorithm { get; set; }
[JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)] public string[] Source { get; set; }
[JsonProperty("fromDate", NullValueHandling = NullValueHandling.Ignore)] public string FromDate { get; set; }
[JsonProperty("toDate", NullValueHandling = NullValueHandling.Ignore)] public string ToDate { get; set; }
[JsonProperty("notAfterFrom", NullValueHandling = NullValueHandling.Ignore)] public string NotAfterFrom { get; set; }
[JsonProperty("notAfterTo", NullValueHandling = NullValueHandling.Ignore)] public string NotAfterTo { get; set; }
[JsonProperty("notBeforeFrom", NullValueHandling = NullValueHandling.Ignore)] public string NotBeforeFrom { get; set; }
[JsonProperty("notBeforeTo", NullValueHandling = NullValueHandling.Ignore)] public string NotBeforeTo { get; set; }
[JsonProperty("sortBy", NullValueHandling = NullValueHandling.Ignore)] public string SortBy { get; set; }
[JsonProperty("sortOrder", NullValueHandling = NullValueHandling.Ignore)] public string SortOrder { get; set; }
[JsonProperty("forPkiSync", NullValueHandling = NullValueHandling.Ignore)] public bool? ForPkiSync { get; set; }
}
internal sealed class InfisicalCertificateBundleResponseDto
{
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
[JsonProperty("certificate")] public string Certificate { get; set; }
[JsonProperty("certificateChain")] public string CertificateChain { get; set; }
[JsonProperty("privateKey")] public string PrivateKey { get; set; }
}
}
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using PSInfisicalAPI.Models;
namespace PSInfisicalAPI.Pki
{
internal static class InfisicalCertificateMapper
{
public static InfisicalCertificate Map(InfisicalCertificateResponseDto dto, string fallbackProjectId)
{
if (dto == null)
{
return null;
}
return new InfisicalCertificate
{
Id = dto.Id,
ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId,
CaId = dto.CaId,
CaName = dto.CaName,
CaCertId = dto.CaCertId,
CertificateTemplateId = dto.CertificateTemplateId,
ProfileId = dto.ProfileId,
ProfileName = dto.ProfileName,
ApplicationId = dto.ApplicationId,
ApplicationName = dto.ApplicationName,
PkiSubscriberId = dto.PkiSubscriberId,
Status = dto.Status,
SerialNumber = dto.SerialNumber,
FriendlyName = dto.FriendlyName,
CommonName = dto.CommonName,
AltNames = dto.AltNames,
KeyUsages = dto.KeyUsages != null ? dto.KeyUsages.ToArray() : null,
ExtendedKeyUsages = dto.ExtendedKeyUsages != null ? dto.ExtendedKeyUsages.ToArray() : null,
KeyAlgorithm = dto.KeyAlgorithm,
SignatureAlgorithm = dto.SignatureAlgorithm,
SubjectOrganization = dto.SubjectOrganization,
SubjectOrganizationalUnit = dto.SubjectOrganizationalUnit,
SubjectCountry = dto.SubjectCountry,
SubjectState = dto.SubjectState,
SubjectLocality = dto.SubjectLocality,
FingerprintSha256 = dto.FingerprintSha256,
FingerprintSha1 = dto.FingerprintSha1,
IsCA = dto.IsCA,
PathLength = dto.PathLength,
Source = dto.Source,
EnrollmentType = dto.EnrollmentType,
HasPrivateKey = dto.HasPrivateKey,
RevocationReason = dto.RevocationReason,
RenewalError = dto.RenewalError,
RenewBeforeDays = dto.RenewBeforeDays,
RenewedFromCertificateId = dto.RenewedFromCertificateId,
RenewedByCertificateId = dto.RenewedByCertificateId,
NotBeforeUtc = ParseTimestamp(dto.NotBefore),
NotAfterUtc = ParseTimestamp(dto.NotAfter),
RevokedAtUtc = ParseTimestamp(dto.RevokedAt),
CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
};
}
public static InfisicalCertificate[] MapMany(IEnumerable<InfisicalCertificateResponseDto> items, string fallbackProjectId)
{
if (items == null)
{
return Array.Empty<InfisicalCertificate>();
}
List<InfisicalCertificate> results = new List<InfisicalCertificate>();
foreach (InfisicalCertificateResponseDto dto in items)
{
InfisicalCertificate mapped = Map(dto, fallbackProjectId);
if (mapped != null)
{
results.Add(mapped);
}
}
return results.ToArray();
}
public static InfisicalCertificateBundle MapBundle(InfisicalCertificateBundleResponseDto dto)
{
if (dto == null)
{
return null;
}
return new InfisicalCertificateBundle
{
SerialNumber = dto.SerialNumber,
CertificatePem = dto.Certificate,
CertificateChainPem = dto.CertificateChain,
PrivateKeyPem = dto.PrivateKey
};
}
private static DateTimeOffset? ParseTimestamp(string value)
{
if (string.IsNullOrEmpty(value))
{
return null;
}
DateTimeOffset parsed;
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed))
{
return parsed;
}
return null;
}
}
}
@@ -0,0 +1,32 @@
using System;
namespace PSInfisicalAPI.Pki
{
public sealed class InfisicalCertificateSearchQuery
{
public string ProjectId { get; set; }
public string FriendlyName { get; set; }
public string CommonName { get; set; }
public string Search { get; set; }
public string Status { get; set; }
public int? Offset { get; set; }
public int? Limit { get; set; }
public string[] CaIds { get; set; }
public string[] ProfileIds { get; set; }
public string[] ApplicationIds { get; set; }
public string[] EnrollmentTypes { get; set; }
public string ExtendedKeyUsage { get; set; }
public string[] KeyAlgorithm { get; set; }
public string SignatureAlgorithm { get; set; }
public string[] Source { get; set; }
public DateTimeOffset? FromDate { get; set; }
public DateTimeOffset? ToDate { get; set; }
public DateTimeOffset? NotAfterFrom { get; set; }
public DateTimeOffset? NotAfterTo { get; set; }
public DateTimeOffset? NotBeforeFrom { get; set; }
public DateTimeOffset? NotBeforeTo { get; set; }
public string SortBy { get; set; }
public string SortOrder { get; set; }
public bool? ForPkiSync { get; set; }
}
}
@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Endpoints;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Http;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Serialization;
namespace PSInfisicalAPI.Pki
{
public sealed class InfisicalPkiClient
{
private const string Component = "PkiClient";
private readonly IInfisicalLogger _logger;
private readonly JsonInfisicalSerializer _serializer;
private readonly InfisicalApiInvoker _invoker;
public InfisicalPkiClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
{
if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); }
_logger = logger ?? NullInfisicalLogger.Instance;
_serializer = new JsonInfisicalSerializer();
_invoker = new InfisicalApiInvoker(httpClient);
}
public InfisicalCertificateAuthority[] ListInternalCertificateAuthorities(InfisicalConnection connection, string projectId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId);
List<KeyValuePair<string, string>> query = null;
if (!string.IsNullOrEmpty(resolvedProjectId))
{
query = new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("projectId", resolvedProjectId) };
}
try
{
_logger.Information(Component, "Attempting to list Infisical internal certificate authorities. Please Wait...");
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListInternalCertificateAuthorities, "ListInternalCertificateAuthorities", null, query, null);
InfisicalInternalCaListResponseDto dto = _serializer.Deserialize<InfisicalInternalCaListResponseDto>(response.Body);
response.Clear();
List<InfisicalInternalCaResponseDto> source = dto != null ? (dto.CertificateAuthorities ?? dto.Cas) : null;
InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, resolvedProjectId);
_logger.Information(Component, "Infisical internal certificate authority list retrieval was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical internal certificate authority list retrieval failed.");
throw;
}
}
public InfisicalCertificateAuthority GetInternalCertificateAuthority(InfisicalConnection connection, string caId, string projectId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "caId", caId } };
try
{
_logger.Information(Component, string.Concat("Attempting to retrieve Infisical internal certificate authority '", caId, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveInternalCertificateAuthority, "RetrieveInternalCertificateAuthority", pathParameters, null, null);
InfisicalInternalCaSingleResponseDto dto = _serializer.Deserialize<InfisicalInternalCaSingleResponseDto>(response.Body);
response.Clear();
InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null;
if (inner == null)
{
inner = _serializer.Deserialize<InfisicalInternalCaResponseDto>(response.Body);
}
InfisicalCertificateAuthority mapped = InfisicalCaMapper.Map(inner, FirstNonEmpty(projectId, connection.ProjectId));
_logger.Information(Component, "Infisical internal certificate authority retrieval was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical internal certificate authority retrieval failed.");
throw;
}
}
public InfisicalCertificateSearchResult SearchCertificates(InfisicalConnection connection, InfisicalCertificateSearchQuery query)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (query == null) { throw new ArgumentNullException(nameof(query)); }
string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId);
if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "projectId", resolvedProjectId } };
InfisicalCertificateSearchRequestDto request = BuildSearchRequest(query);
string body = _serializer.Serialize(request);
try
{
_logger.Information(Component, "Attempting to search Infisical certificates. Please Wait...");
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.SearchCertificates, "SearchCertificates", pathParameters, null, body);
InfisicalCertificateSearchResponseDto dto = _serializer.Deserialize<InfisicalCertificateSearchResponseDto>(response.Body);
response.Clear();
InfisicalCertificate[] mapped = InfisicalCertificateMapper.MapMany(dto != null ? dto.Certificates : null, resolvedProjectId);
int total = dto != null ? dto.TotalCount : mapped.Length;
_logger.Information(Component, "Infisical certificate search was successful.");
return new InfisicalCertificateSearchResult { Certificates = mapped, TotalCount = total };
}
catch (Exception)
{
_logger.Error(Component, "Infisical certificate search failed.");
throw;
}
}
public InfisicalCertificateBundle GetCertificateBundle(InfisicalConnection connection, string serialNumber)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(serialNumber)) { throw new InfisicalConfigurationException("SerialNumber is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "serialNumber", serialNumber } };
try
{
_logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate bundle for '", serialNumber, "'. Please Wait..."));
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.GetCertificateBundle, "GetCertificateBundle", pathParameters, null, null);
InfisicalCertificateBundleResponseDto dto = _serializer.Deserialize<InfisicalCertificateBundleResponseDto>(response.Body);
response.Clear();
InfisicalCertificateBundle mapped = InfisicalCertificateMapper.MapBundle(dto);
_logger.Information(Component, "Infisical certificate bundle retrieval was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical certificate bundle retrieval failed.");
throw;
}
}
internal static InfisicalCertificateSearchRequestDto BuildSearchRequest(InfisicalCertificateSearchQuery query)
{
return new InfisicalCertificateSearchRequestDto
{
FriendlyName = query.FriendlyName,
CommonName = query.CommonName,
Search = query.Search,
Status = query.Status,
Offset = query.Offset,
Limit = query.Limit,
CaIds = query.CaIds,
ProfileIds = query.ProfileIds,
ApplicationIds = query.ApplicationIds,
EnrollmentTypes = query.EnrollmentTypes,
ExtendedKeyUsage = query.ExtendedKeyUsage,
KeyAlgorithm = query.KeyAlgorithm,
SignatureAlgorithm = query.SignatureAlgorithm,
Source = query.Source,
FromDate = FormatTimestamp(query.FromDate),
ToDate = FormatTimestamp(query.ToDate),
NotAfterFrom = FormatTimestamp(query.NotAfterFrom),
NotAfterTo = FormatTimestamp(query.NotAfterTo),
NotBeforeFrom = FormatTimestamp(query.NotBeforeFrom),
NotBeforeTo = FormatTimestamp(query.NotBeforeTo),
SortBy = query.SortBy,
SortOrder = query.SortOrder,
ForPkiSync = query.ForPkiSync
};
}
private static string FormatTimestamp(DateTimeOffset? value)
{
if (!value.HasValue) { return null; }
return value.Value.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
}
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;
}
}
public sealed class InfisicalCertificateSearchResult
{
public InfisicalCertificate[] Certificates { get; set; }
public int TotalCount { get; set; }
}
}
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using BcX509Certificate = Org.BouncyCastle.X509.X509Certificate;
namespace PSInfisicalAPI.Pki
{
public static class PemCertificateBuilder
{
public static X509Certificate2 Build(string certificatePem, string privateKeyPem, string chainPem, X509KeyStorageFlags storageFlags)
{
if (string.IsNullOrEmpty(certificatePem))
{
throw new ArgumentException("Certificate PEM is required.", nameof(certificatePem));
}
BcX509Certificate leaf = ReadFirstCertificate(certificatePem);
List<BcX509Certificate> chain = ReadAllCertificates(chainPem);
if (string.IsNullOrEmpty(privateKeyPem))
{
return new X509Certificate2(leaf.GetEncoded());
}
AsymmetricKeyParameter privateKey = ReadPrivateKey(privateKeyPem);
Pkcs12StoreBuilder builder = new Pkcs12StoreBuilder();
Pkcs12Store store = builder.Build();
const string alias = "infisical-cert";
List<X509CertificateEntry> entries = new List<X509CertificateEntry>();
entries.Add(new X509CertificateEntry(leaf));
foreach (BcX509Certificate intermediate in chain)
{
entries.Add(new X509CertificateEntry(intermediate));
}
store.SetKeyEntry(alias, new AsymmetricKeyEntry(privateKey), entries.ToArray());
char[] password = GenerateRandomPassword();
using (MemoryStream ms = new MemoryStream())
{
store.Save(ms, password, new SecureRandom());
byte[] pfxBytes = ms.ToArray();
return new X509Certificate2(pfxBytes, new string(password), storageFlags);
}
}
public static List<X509Certificate2> ReadCertificateChain(string chainPem)
{
List<X509Certificate2> results = new List<X509Certificate2>();
if (string.IsNullOrEmpty(chainPem))
{
return results;
}
foreach (BcX509Certificate cert in ReadAllCertificates(chainPem))
{
results.Add(new X509Certificate2(cert.GetEncoded()));
}
return results;
}
private static BcX509Certificate ReadFirstCertificate(string pem)
{
using (StringReader reader = new StringReader(pem))
{
PemReader pemReader = new PemReader(reader);
object obj = pemReader.ReadObject();
while (obj != null)
{
BcX509Certificate cert = obj as BcX509Certificate;
if (cert != null)
{
return cert;
}
obj = pemReader.ReadObject();
}
}
throw new InvalidOperationException("No certificate found in PEM input.");
}
private static List<BcX509Certificate> ReadAllCertificates(string pem)
{
List<BcX509Certificate> results = new List<BcX509Certificate>();
if (string.IsNullOrEmpty(pem))
{
return results;
}
using (StringReader reader = new StringReader(pem))
{
PemReader pemReader = new PemReader(reader);
object obj = pemReader.ReadObject();
while (obj != null)
{
BcX509Certificate cert = obj as BcX509Certificate;
if (cert != null)
{
results.Add(cert);
}
obj = pemReader.ReadObject();
}
}
return results;
}
private static AsymmetricKeyParameter ReadPrivateKey(string pem)
{
using (StringReader reader = new StringReader(pem))
{
PemReader pemReader = new PemReader(reader);
object obj = pemReader.ReadObject();
while (obj != null)
{
AsymmetricKeyParameter key = obj as AsymmetricKeyParameter;
if (key != null && key.IsPrivate)
{
return key;
}
AsymmetricCipherKeyPair pair = obj as AsymmetricCipherKeyPair;
if (pair != null)
{
return pair.Private;
}
obj = pemReader.ReadObject();
}
}
throw new InvalidOperationException("No private key found in PEM input.");
}
private static char[] GenerateRandomPassword()
{
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[24];
random.NextBytes(bytes);
return Convert.ToBase64String(bytes).ToCharArray();
}
}
}