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
@@ -41,7 +41,7 @@ jobs:
|
|||||||
Write-Host "Manifest OK: $($manifest.Name) $($manifest.Version)"
|
Write-Host "Manifest OK: $($manifest.Name) $($manifest.Version)"
|
||||||
|
|
||||||
- name: Upload module artifact
|
- name: Upload module artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: christopherhx/gitea-upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: PSInfisicalAPI-module
|
name: PSInfisicalAPI-module
|
||||||
path: Module/PSInfisicalAPI
|
path: Module/PSInfisicalAPI
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
|
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
|
||||||
|
|
||||||
- name: Download module artifact
|
- name: Download module artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: christopherhx/gitea-download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: PSInfisicalAPI-module
|
name: PSInfisicalAPI-module
|
||||||
path: Module/PSInfisicalAPI
|
path: Module/PSInfisicalAPI
|
||||||
@@ -213,7 +213,7 @@ jobs:
|
|||||||
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
|
Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'))
|
||||||
|
|
||||||
- name: Download module artifact
|
- name: Download module artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: christopherhx/gitea-download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: PSInfisicalAPI-module
|
name: PSInfisicalAPI-module
|
||||||
path: Module/PSInfisicalAPI
|
path: Module/PSInfisicalAPI
|
||||||
|
|||||||
+31
-3
@@ -6,17 +6,45 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
|
|||||||
|
|
||||||
## Unreleased
|
## 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
|
## 2026.06.04.0020
|
||||||
|
|
||||||
- Build produced from commit 211fbcf34dbb.
|
- Build produced from commit 211fbcf34dbb.
|
||||||
|
|
||||||
## Unreleased (carried forward)
|
## Unreleased (carried forward)
|
||||||
|
|
||||||
## 2026.06.04.0005
|
## 2026.06.04.0005
|
||||||
|
|
||||||
- Build produced from commit e0a6ef02df3e.
|
- Build produced from commit e0a6ef02df3e.
|
||||||
|
|
||||||
## Unreleased (carried forward)
|
## Unreleased (carried forward)
|
||||||
|
|
||||||
- **Bulk v4 batch routes**: Endpoint registry now registers `POST|PATCH|DELETE /api/v4/secrets/batch` as the preferred candidates for `BulkCreateSecret`/`BulkUpdateSecret`/`BulkDeleteSecret`; the existing v3 raw routes (`/api/v3/secrets/batch/raw`) remain as automatic fallback. Batch request DTOs serialize both `projectId` (required by v4) and `workspaceId` (accepted by v3) when populated.
|
- **Bulk v4 batch routes**: Endpoint registry now registers `POST|PATCH|DELETE /api/v4/secrets/batch` as the preferred candidates for `BulkCreateSecret`/`BulkUpdateSecret`/`BulkDeleteSecret`; the existing v3 raw routes (`/api/v3/secrets/batch/raw`) remain as automatic fallback. Batch request DTOs serialize both `projectId` (required by v4) and `workspaceId` (accepted by v3) when populated.
|
||||||
- **Strongly-typed bulk input**: `-Secrets` on `New-InfisicalSecret` and `Update-InfisicalSecret` is now `IDictionary<string, string>[]` instead of `Hashtable[]`. `InfisicalBulkSecretConverter` accepts `IEnumerable<IDictionary<string, string>>` and parses `TagIds` from a comma-separated string. Nested `Metadata`/`SecretMetadata` dictionaries are no longer accepted in the bulk hashtable surface (set `SecretMetadata` programmatically on `InfisicalBulkCreateSecretItem`/`InfisicalBulkUpdateSecretItem` if needed).
|
- **Strongly-typed bulk input**: `-Secrets` on `New-InfisicalSecret` and `Update-InfisicalSecret` is now `IDictionary<string, string>[]` instead of `Hashtable[]`. `InfisicalBulkSecretConverter` accepts `IEnumerable<IDictionary<string, string>>` and parses `TagIds` from a comma-separated string. Nested `Metadata`/`SecretMetadata` dictionaries are no longer accepted in the bulk hashtable surface (set `SecretMetadata` programmatically on `InfisicalBulkCreateSecretItem`/`InfisicalBulkUpdateSecretItem` if needed).
|
||||||
@@ -24,7 +52,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
|
|||||||
## 2026.06.03.2207
|
## 2026.06.03.2207
|
||||||
|
|
||||||
- Build produced from commit 09c3d5c68bbc.
|
- 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`.
|
- **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.
|
- **`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.
|
- **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>
|
</TableRowEntries>
|
||||||
</TableControl>
|
</TableControl>
|
||||||
</View>
|
</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>
|
</ViewDefinitions>
|
||||||
</Configuration>
|
</Configuration>
|
||||||
|
|||||||
@@ -46,4 +46,64 @@
|
|||||||
</MemberSet>
|
</MemberSet>
|
||||||
</Members>
|
</Members>
|
||||||
</Type>
|
</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>
|
</Types>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@{
|
@{
|
||||||
RootModule = 'PSInfisicalAPI.psm1'
|
RootModule = 'PSInfisicalAPI.psm1'
|
||||||
ModuleVersion = '2026.06.04.0020'
|
ModuleVersion = '2026.06.04.0123'
|
||||||
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||||
Author = 'Grace Solutions'
|
Author = 'Grace Solutions'
|
||||||
CompanyName = 'Grace Solutions'
|
CompanyName = 'Grace Solutions'
|
||||||
@@ -39,7 +39,13 @@
|
|||||||
'Get-InfisicalTag',
|
'Get-InfisicalTag',
|
||||||
'New-InfisicalTag',
|
'New-InfisicalTag',
|
||||||
'Update-InfisicalTag',
|
'Update-InfisicalTag',
|
||||||
'Remove-InfisicalTag'
|
'Remove-InfisicalTag',
|
||||||
|
'Get-InfisicalCertificateAuthority',
|
||||||
|
'Search-InfisicalCertificate',
|
||||||
|
'ConvertTo-InfisicalCertificate',
|
||||||
|
'Install-InfisicalCertificate',
|
||||||
|
'Uninstall-InfisicalCertificate',
|
||||||
|
'Export-InfisicalCertificate'
|
||||||
)
|
)
|
||||||
AliasesToExport = @()
|
AliasesToExport = @()
|
||||||
VariablesToExport = @()
|
VariablesToExport = @()
|
||||||
@@ -51,7 +57,7 @@
|
|||||||
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
||||||
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
||||||
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
||||||
CommitHash = '211fbcf34dbb'
|
CommitHash = '2cbd5c2008f5'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -127,7 +127,13 @@ function Write-Manifest {
|
|||||||
'Get-InfisicalTag',
|
'Get-InfisicalTag',
|
||||||
'New-InfisicalTag',
|
'New-InfisicalTag',
|
||||||
'Update-InfisicalTag',
|
'Update-InfisicalTag',
|
||||||
'Remove-InfisicalTag'
|
'Remove-InfisicalTag',
|
||||||
|
'Get-InfisicalCertificateAuthority',
|
||||||
|
'Search-InfisicalCertificate',
|
||||||
|
'ConvertTo-InfisicalCertificate',
|
||||||
|
'Install-InfisicalCertificate',
|
||||||
|
'Uninstall-InfisicalCertificate',
|
||||||
|
'Export-InfisicalCertificate'
|
||||||
)
|
)
|
||||||
AliasesToExport = @()
|
AliasesToExport = @()
|
||||||
VariablesToExport = @()
|
VariablesToExport = @()
|
||||||
@@ -187,7 +193,7 @@ if (`$null -eq `$manifest) {
|
|||||||
|
|
||||||
Import-Module -Name '$($ModuleDirectory.FullName)' -Force
|
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) {
|
foreach (`$c in `$cmds) {
|
||||||
if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
|
||||||
throw "Cmdlet not found: `$c"
|
throw "Cmdlet not found: `$c"
|
||||||
@@ -283,7 +289,7 @@ $publishArgs = @(
|
|||||||
Invoke-DotNet -Arguments $publishArgs
|
Invoke-DotNet -Arguments $publishArgs
|
||||||
|
|
||||||
Clear-Directory -Directory $ModuleBinDir
|
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) {
|
foreach ($assembly in $desiredAssemblies) {
|
||||||
$source = [System.IO.FileInfo][System.IO.Path]::Combine($publishOutput.FullName, $assembly)
|
$source = [System.IO.FileInfo][System.IO.Path]::Combine($publishOutput.FullName, $assembly)
|
||||||
if ($source.Exists) {
|
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 CreateTag = "CreateTag";
|
||||||
public const string UpdateTag = "UpdateTag";
|
public const string UpdateTag = "UpdateTag";
|
||||||
public const string DeleteTag = "DeleteTag";
|
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);
|
RegisterEnvironments(Candidates);
|
||||||
RegisterFolders(Candidates);
|
RegisterFolders(Candidates);
|
||||||
RegisterTags(Candidates);
|
RegisterTags(Candidates);
|
||||||
|
RegisterPki(Candidates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Add(Dictionary<string, List<InfisicalEndpointDefinition>> map, InfisicalEndpointDefinition definition)
|
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)
|
public static InfisicalEndpointDefinition Get(string name)
|
||||||
{
|
{
|
||||||
List<InfisicalEndpointDefinition> list = GetCandidatesInternal(name);
|
List<InfisicalEndpointDefinition> list = GetCandidatesInternal(name);
|
||||||
|
|||||||
@@ -43,6 +43,53 @@ namespace PSInfisicalAPI.Http
|
|||||||
throw exception;
|
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(
|
private InfisicalHttpResponse ExecuteAuthorized(
|
||||||
InfisicalConnection connection,
|
InfisicalConnection connection,
|
||||||
InfisicalEndpointDefinition definition,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||||
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
|
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="YamlDotNet" Version="15.1.6" />
|
<PackageReference Include="YamlDotNet" Version="15.1.6" />
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user