diff --git a/CHANGELOG.md b/CHANGELOG.md index da75758..6703ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,105 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.06.04.1825 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1820 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- `Install-InfisicalCertificate` now routes chain certificates by self-signed status instead of dumping every chain entry into the Intermediate Certification Authorities store. Self-signed roots are installed into `StoreName.Root` (Trusted Root Certification Authorities) and non-self-signed intermediates are installed into `StoreName.CertificateAuthority` (Intermediate Certification Authorities). The leaf continues to use the user-specified `-StoreName`/`-StoreLocation` (default `My`/`CurrentUser`). `Request-InfisicalCertificate` already routed chain certs correctly; the same routing helper is now shared by both cmdlets. +- `InfisicalCertificateRequestHelpers` exposes a new public `GetChainCertificateTargetStore(X509Certificate2)` classifier and a new `InstallChain(IEnumerable, StoreLocation, bool, IInfisicalLogger, string)` overload. The existing `InstallChain(InfisicalSignedCertificate, ...)` overload now delegates to the new collection-based overload, so PKI chain-installation routing is centralized in one place. + +## 2026.06.04.1810 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- Authored MAML help (`Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`) covering all 39 exported cmdlets. Every entry includes a synopsis, description, notes section, and two examples: a one-liner and an `OrderedDictionary` splat (with `OrdinalIgnoreCase`) that includes preceding `Get-` resolver commands wherever IDs or slugs are required. +- `build.ps1` now stages the cmdlet help XML next to the deployed binary. After the publish step, every culture directory under `Module/PSInfisicalAPI/` (matching `xx` or `xx-XX`) that contains `PSInfisicalAPI.dll-Help.xml` is mirrored into `bin//`. The script hard-fails if `bin/en-US/PSInfisicalAPI.dll-Help.xml` is missing or contains zero `` entries. +- `Test-ModuleImports` in `build.ps1` now dynamically enumerates exported cmdlets via `Get-Command -Module PSInfisicalAPI -CommandType Cmdlet`, cross-checks the result against an expected list of 39 cmdlet names (including the previously-missing `Copy-InfisicalSecret`), and for each cmdlet asserts that `Get-Help -Full` returns a non-empty synopsis (rejecting PowerShell's auto-generated cmdlet-name fallback), a non-empty description, and that `Get-Help -Examples` returns at least one example node whose `` block is non-empty. + +## 2026.06.04.1808 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1658 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- `Request-InfisicalCertificate` reuse path now falls back to the Infisical certificate-bundle endpoint when the local trust stores do not contain the issuing intermediates or root. The cmdlet builds the local chain first; if the result has no intermediates and no root, it fetches `GetCertificateBundle(serialNumber)` and rebuilds the result with the bundle's chain PEM merged in. A new `-LocalChainOnly` switch opts out of the bundle fetch for strict offline behavior. Bundle-fetch failures are logged at verbose level and the cmdlet returns the local-only result. +- `InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal` adds a second overload that accepts an `InfisicalCertificateBundle`; when supplied, chain certs from the bundle are deduplicated by thumbprint and merged with the locally-resolved chain before classification. + +## 2026.06.04.1652 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1651 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1634 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1631 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1622 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- **PKI contract fixes and cmdlet expansion**: + - `InfisicalPkiClient` no longer auto-injects `connection.ProjectId` into PKI CA list/retrieve calls; only the caller's explicit `-ProjectId` is forwarded so that cert-manager primary routes (which do not accept the query parameter) succeed. + - List/single CA and single certificate response parsing now tolerate raw arrays, wrapper objects (`{certificate: {...}}`, `{certificates: [...]}`), and nested `configuration` blocks. `InfisicalCaMapper` reads CA detail fields from `configuration` first, falling back to top-level. + - `RetrieveCertificate(connection, identifier)` added on `InfisicalPkiClient`. +- **New cmdlets**: + - **`Get-InfisicalCertificate`** — single-record retrieval by `-SerialNumber`/`-Id` (mandatory positional). + - **`Get-InfisicalCertificates`** — listing with light filtering (`-CommonName`, `-FriendlyName`, `-Status`, `-CaId`, `-Limit`, `-Offset`, `-NoAutoPage`). Auto-paginates by default. + - **`Request-InfisicalCertificate`** — generates a keypair locally (private key never leaves the device), submits a PKCS#10 CSR to either `pki-subscribers/{name}/sign-certificate` (`-PkiSubscriberSlug`) or `ca/{caId}/sign-certificate` (`-CertificateAuthorityId`), and returns a single `InfisicalCertificateResult` object with the leaf and chain pre-classified. The result exposes `Leaf : X509Certificate2`, `Intermediates : X509Certificate2[]`, `Root : X509Certificate2` (nullable), `Chain : X509Certificate2[]` (ordered leaf → intermediates → root, deduplicated by thumbprint), plus pass-through `SerialNumber`, `CertificatePem`, `CertificateChainPem`, and `PrivateKeyPem`. Supports `-Subject` (`IDictionary` with `CN`/`C`/`ST`/`L`/`O`/`OU`/`E` keys) merged with individual `-CommonName`/`-Country`/etc. parameters (individual params win), `-DnsName`/`-IpAddress` SANs (auto-populated from local FQDN when omitted). Idempotency: scans the local `X509Store` for an existing certificate matching `CN` and an Infisical-known serial number; returns the existing certificate wrapped in an `InfisicalCertificateResult` whose `Intermediates`/`Root`/`Chain` are populated by walking the local trust stores via `X509Chain` (no network calls, revocation checks disabled), and whose `CertificatePem`/`CertificateChainPem` are reconstructed from the resolved certs. Reuse is short-circuited unless `-Force` or `-AllowRenewal` (with optional `-RenewalThresholdDays`, default 30) requests a new one. Installation: `-Install` adds the leaf to `-StoreName`/`-StoreLocation` (default `My`/`CurrentUser`); `-InstallChain` additionally places intermediates into `CertificateAuthority` and self-signed roots into `Root` for the same `-StoreLocation`. `-KeyStorageFlags` is passed through to `X509Certificate2` import. + - **Multi-algorithm CSR support** on `Request-InfisicalCertificate` via split parameters: `-KeyAlgorithm` (`Rsa`/`Ecdsa`/`Ed25519`, default `Rsa`), `-KeySize` (`2048`/`3072`/`4096`, default `2048`, applies to RSA only), `-Curve` (`P256`/`P384`, default `P256`, applies to ECDSA only). Signature algorithms are picked automatically: SHA256WITHRSA for RSA, SHA256WITHECDSA / SHA384WITHECDSA for ECDSA P-256/P-384, and Ed25519 (pure-EdDSA) for Ed25519. The underlying `InfisicalCsrBuilder.Build(subject, dns, ip, options)` API was updated to take an `InfisicalCsrOptions` object in place of the prior `keySize` int. + - **Sign-certificate endpoint registrations**: `SignCertificateBySubscriber` and `SignCertificateByCa` registered with both `/api/v1/pki/...` and `/api/v1/cert-manager/...` candidate paths and marked `ContainsSecretMaterialInResponse = true`. + +## 2026.06.04.1554 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1512 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1508 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + - **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 diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index e0de7f0..5ffa120 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.06.04.0123' + ModuleVersion = '2026.06.04.1825' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -41,7 +41,10 @@ 'Update-InfisicalTag', 'Remove-InfisicalTag', 'Get-InfisicalCertificateAuthority', + 'Get-InfisicalCertificate', + 'Get-InfisicalCertificates', 'Search-InfisicalCertificate', + 'Request-InfisicalCertificate', 'ConvertTo-InfisicalCertificate', 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', @@ -57,7 +60,7 @@ LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html' ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI' ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.' - CommitHash = '2cbd5c2008f5' + CommitHash = '19615363e356' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index e4308f8..634d6d8 100644 Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ diff --git a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml new file mode 100644 index 0000000..dc9fb90 --- /dev/null +++ b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml @@ -0,0 +1,1531 @@ + + + + + + Connect-Infisical + Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. + Connect + Infisical + + + Authenticates against an Infisical instance using one of the supported auth providers (UniversalAuth, Token, JWT, OIDC, LDAP, Azure, GCP IAM) and stores the resulting connection in the module-level session manager. Subsequent cmdlets pick up the connection automatically. If parameters such as BaseUri, OrganizationId, ProjectId, Environment, ClientId, or ClientSecret are not supplied, the cmdlet attempts to resolve them from a curated list of environment-variable name patterns across Process, User, and Machine scopes. + + + Notes + + Use -PassThru to emit the resulting InfisicalConnection object; by default the connection is stored silently. SecureString-typed parameters such as ClientSecret, AccessToken, Jwt, and Password are never logged. + The cmdlet pins the API version to the bound value when -ApiVersion is supplied explicitly; otherwise the default 'v4' is used and remains overridable per-call. + + + + + EXAMPLE 1 + Connect-Infisical -BaseUri 'https://app.infisical.com' -ClientId $ClientId -ClientSecret $ClientSecret -OrganizationId $OrgId -ProjectId $ProjectId -Environment 'dev' + Performs a Universal-Auth machine-identity login and stores the resulting session for subsequent cmdlets. + + + EXAMPLE 2 + $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com' +$ConnectInfisicalParameters.OrganizationId = $OrganizationId +$ConnectInfisicalParameters.ProjectId = $ProjectId +$ConnectInfisicalParameters.Environment = 'dev' +$ConnectInfisicalParameters.ClientId = $ClientId +$ConnectInfisicalParameters.ClientSecret = $ClientSecret +$ConnectInfisicalParameters.SecretPath = '/' +$ConnectInfisicalParameters.ApiVersion = 'v4' +$ConnectInfisicalParameters.PassThru = $True +$ConnectInfisicalParameters.Verbose = $True + +$ConnectInfisicalResult = Connect-Infisical @ConnectInfisicalParameters + Builds an ordered parameter dictionary, splats it onto Connect-Infisical, and captures the returned InfisicalConnection for later reuse. + + + + + + + Disconnect-Infisical + Clears the current Infisical session from the module-level session manager. + Disconnect + Infisical + + + Removes the cached InfisicalConnection so subsequent cmdlets that require an active session will fail until Connect-Infisical is invoked again. The cmdlet does not contact the Infisical server. + + + Notes + + Use -PassThru to receive a status object that includes the disconnect timestamp; by default the cmdlet returns no output. + + + + + EXAMPLE 1 + Disconnect-Infisical + Clears the active Infisical session silently. + + + EXAMPLE 2 + $DisconnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$DisconnectInfisicalParameters.PassThru = $True +$DisconnectInfisicalParameters.Verbose = $True + +$DisconnectInfisicalResult = Disconnect-Infisical @DisconnectInfisicalParameters + Disconnects and captures a status object that includes IsConnected and DisconnectedAtUtc for logging. + + + + + + + Get-InfisicalSecret + Retrieves a single Infisical secret by name from the active session's project and environment. + Get + InfisicalSecret + + + Fetches a single secret by name. Project, Environment, SecretPath, and ApiVersion default to the values pinned on the active InfisicalConnection but can be overridden per call. Optional flags request reference-expansion, import inclusion, or a specific historical version. + + + Notes + + The returned InfisicalSecret stores the value as SecureString; call .GetPlainTextValue() to materialize the cleartext value only when strictly required. + + + + + EXAMPLE 1 + Get-InfisicalSecret -SecretName 'DATABASE_URL' + Retrieves the DATABASE_URL secret from the project and environment pinned by Connect-Infisical. + + + EXAMPLE 2 + $GetInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretParameters.SecretName = 'DATABASE_URL' +$GetInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretParameters.ExpandSecretReferences = $True +$GetInfisicalSecretParameters.IncludeImports = $True +$GetInfisicalSecretParameters.Verbose = $True + +$GetInfisicalSecretResult = Get-InfisicalSecret @GetInfisicalSecretParameters + Retrieves a single secret from a script-specific subpath with secret-reference expansion and folder imports enabled. + + + + + + + Get-InfisicalSecrets + Lists Infisical secrets within a project, environment, and optional folder path. + Get + InfisicalSecrets + + + Enumerates secrets under the active session's project and environment, optionally recursing through subfolders. Supports metadata-based filtering, tag-slug filtering, secret-reference expansion, and personal-override inclusion. + + + Notes + + Use -Recursive together with -SecretPath to walk an entire folder subtree. Pipe the result into ConvertTo-InfisicalSecretDictionary for hashtable-style lookup. + + + + + EXAMPLE 1 + Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + Lists every secret under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretsParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretsParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretsParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretsParameters.Recursive = $True +$GetInfisicalSecretsParameters.ExpandSecretReferences = $True +$GetInfisicalSecretsParameters.IncludeImports = $True +$GetInfisicalSecretsParameters.IncludePersonalOverrides = $True +$GetInfisicalSecretsParameters.Verbose = $True + +$GetInfisicalSecretsResult = Get-InfisicalSecrets @GetInfisicalSecretsParameters + Lists secrets under a script-specific subpath with imports, personal overrides, and reference expansion enabled. + + + + + + + New-InfisicalSecret + Creates a new Infisical secret, with support for SecureString values and bulk creation. + New + InfisicalSecret + + + Creates one or many secrets. Three parameter sets are supported: PlainText (SecretName + SecretValue), SecureString (SecretName + SecureSecretValue), and Bulk (an array of hashtables piped or supplied via -Secrets). Honors -WhatIf and -Confirm. + + + Notes + + Pass -SkipMultilineEncoding when the value already contains literal newlines that the server should preserve verbatim. Use -TagIds to attach tag references at creation time. + + + + + EXAMPLE 1 + New-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'super-secret-value' + Creates a single shared secret in the active project/environment. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags -ProjectId $ConnectInfisicalParameters.ProjectId + +$NewInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalSecretParameters.SecretName = 'API_KEY' +$NewInfisicalSecretParameters.SecretValue = 'super-secret-value' +$NewInfisicalSecretParameters.SecretComment = 'Issued by deployment pipeline' +$NewInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$NewInfisicalSecretParameters.TagIds = @($GetInfisicalTagsResult[0].Id) +$NewInfisicalSecretParameters.Verbose = $True + +$NewInfisicalSecretResult = New-InfisicalSecret @NewInfisicalSecretParameters + Looks up tags to attach, then creates a single secret with a comment and tag association under a script-specific subpath. + + + + + + + Update-InfisicalSecret + Updates an existing Infisical secret value, comment, name, or tags. + Update + InfisicalSecret + + + Updates one or many secrets. Supports PlainText, SecureString, and Bulk parameter sets. Use -NewSecretName to rename a secret, -SecretComment to update its comment, and -TagIds to replace tag associations. Honors -WhatIf and -Confirm. + + + Notes + + Only the parameters you bind are sent; omitted scalar parameters are not modified server-side. The Bulk parameter set accepts pipeline input of hashtables containing SecretName/SecretValue/etc. + + + + + EXAMPLE 1 + Update-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'rotated-value' + Rotates the API_KEY secret in the active project/environment. + + + EXAMPLE 2 + $UpdateInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalSecretParameters.SecretName = 'API_KEY' +$UpdateInfisicalSecretParameters.NewSecretName = 'API_KEY_V2' +$UpdateInfisicalSecretParameters.SecretValue = 'rotated-value' +$UpdateInfisicalSecretParameters.SecretComment = 'Rotated by scheduled job' +$UpdateInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$UpdateInfisicalSecretParameters.Verbose = $True + +$UpdateInfisicalSecretResult = Update-InfisicalSecret @UpdateInfisicalSecretParameters + Rotates the value, renames the secret, and updates its comment in a single call. + + + + + + + Remove-InfisicalSecret + Deletes one or many Infisical secrets by name. + Remove + InfisicalSecret + + + Deletes a single secret (Single parameter set) or a batch of secrets by name (Bulk parameter set). High ConfirmImpact triggers prompts by default. -PassThru emits the removed secret names. + + + Notes + + Removal is irreversible from this cmdlet's perspective; rely on Infisical's audit log or secret-version history for forensics. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalSecret -SecretName 'API_KEY_V1' -Confirm:$False + Deletes a single secret without prompting. + + + EXAMPLE 2 + $RemoveInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalSecretParameters.SecretNames = @('LEGACY_KEY_1','LEGACY_KEY_2','LEGACY_KEY_3') +$RemoveInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$RemoveInfisicalSecretParameters.PassThru = $True +$RemoveInfisicalSecretParameters.Confirm = $False +$RemoveInfisicalSecretParameters.Verbose = $True + +$RemoveInfisicalSecretResult = Remove-InfisicalSecret @RemoveInfisicalSecretParameters + Bulk-deletes three legacy secrets and returns the removed names for audit logging. + + + + + + + Copy-InfisicalSecret + Duplicates one or more secrets into a different environment or secret path. + Copy + InfisicalSecret + + + Server-side duplicates an array of secret IDs into a destination environment (and optional destination path), with switches that control whether the value, comment, tags, and metadata are copied. Use Get-InfisicalSecrets followed by selection of the desired Id values to feed -SecretId. + + + Notes + + Set -OverwriteExisting to replace same-named secrets at the destination. Without -CopySecretValue, the destination secrets are created with empty values, preserving only metadata. + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Select-Object -ExpandProperty Id | Copy-InfisicalSecret -DestinationEnvironment 'staging' -CopySecretValue + Copies all secrets from the active environment into 'staging', including their values. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + +$CopyInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$CopyInfisicalSecretParameters.SecretId = $GetInfisicalSecretsResult.Id +$CopyInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$CopyInfisicalSecretParameters.SourceEnvironment = $ConnectInfisicalParameters.Environment +$CopyInfisicalSecretParameters.SourceSecretPath = '/Windows' +$CopyInfisicalSecretParameters.DestinationEnvironment = 'staging' +$CopyInfisicalSecretParameters.DestinationSecretPath = '/Windows' +$CopyInfisicalSecretParameters.OverwriteExisting = $True +$CopyInfisicalSecretParameters.CopySecretValue = $True +$CopyInfisicalSecretParameters.CopySecretComment = $True +$CopyInfisicalSecretParameters.CopyTags = $True +$CopyInfisicalSecretParameters.CopyMetadata = $True +$CopyInfisicalSecretParameters.Verbose = $True + +$CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters + Promotes every Windows secret from the active environment into staging with full value/comment/tag/metadata propagation. + + + + + + + ConvertTo-InfisicalSecretDictionary + Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. + ConvertTo + InfisicalSecretDictionary + + + Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). + + + Notes + + Use this conversion before splatting secrets into another process (-AsPlainText) or before passing them to libraries that expect SecureString-keyed lookups (default). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | ConvertTo-InfisicalSecretDictionary -AsPlainText + Builds a plain-text dictionary of every secret in the active environment. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ConvertToInfisicalSecretDictionaryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalSecretDictionaryParameters.InputObject = $GetInfisicalSecretsResult +$ConvertToInfisicalSecretDictionaryParameters.DuplicateKeyBehavior = 'LastWins' +$ConvertToInfisicalSecretDictionaryParameters.AsPlainText = $True +$ConvertToInfisicalSecretDictionaryParameters.Verbose = $True + +$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters + Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + + + + + + Export-InfisicalSecrets + Exports InfisicalSecret objects to disk or environment variables in a chosen file format. + Export + InfisicalSecrets + + + Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. + + + Notes + + EnvironmentVariables format does not require -Path; all other formats do. User/Machine scopes require appropriate privileges (Machine scope requires elevation on Windows). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Export-InfisicalSecrets -Format DotEnv -Path '.\.env' -Force + Writes the active environment's secrets to a .env file. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ExportInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalSecretsParameters.InputObject = $GetInfisicalSecretsResult +$ExportInfisicalSecretsParameters.Format = 'EnvironmentVariables' +$ExportInfisicalSecretsParameters.Scope = 'Process' +$ExportInfisicalSecretsParameters.Force = $True +$ExportInfisicalSecretsParameters.Verbose = $True + +$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters + Projects the recursive secret result into Process-scope environment variables for the current PowerShell session. + + + + + + + Get-InfisicalProjects + Lists Infisical projects accessible to the current identity. + Get + InfisicalProjects + + + Returns every project the active session can see. The cmdlet requires an active InfisicalConnection but takes no parameters; project visibility is governed by Infisical's role assignments. + + + Notes + + The result is an array of InfisicalProject objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. + + + + + EXAMPLE 1 + Get-InfisicalProjects + Lists every project the current session can see. + + + EXAMPLE 2 + $GetInfisicalProjectsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectsParameters.Verbose = $True + +$GetInfisicalProjectsResult = Get-InfisicalProjects @GetInfisicalProjectsParameters | Where-Object { $_.Slug -ilike 'platform-*' } + Lists projects and filters down to those whose slug begins with 'platform-'. + + + + + + + Get-InfisicalProject + Retrieves a single Infisical project by its identifier. + Get + InfisicalProject + + + Retrieves one project by Id. If -ProjectId is not supplied, the cmdlet falls back to the ProjectId pinned on the active InfisicalConnection. + + + Notes + + The cmdlet accepts pipeline input by property name; objects emitted by Get-InfisicalProjects can be piped in directly to refresh a single record. + + + + + EXAMPLE 1 + Get-InfisicalProject + Retrieves the project pinned by the active session. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects + +$GetInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult[0].Id +$GetInfisicalProjectParameters.Verbose = $True + +$GetInfisicalProjectResult = Get-InfisicalProject @GetInfisicalProjectParameters + Looks up the first project in the list and retrieves its full record. + + + + + + + New-InfisicalProject + Creates a new Infisical project in the active organization. + New + InfisicalProject + + + Creates a project with the supplied name and optional slug, description, type, and organization id. If -OrganizationId is not supplied, the active session's organization is used. Honors -WhatIf and -Confirm. + + + Notes + + Slug must be unique within the organization; if not supplied, the server derives one from the project name. + + + + + EXAMPLE 1 + New-InfisicalProject -ProjectName 'Platform Telemetry' + Creates a new project named 'Platform Telemetry' in the active organization. + + + EXAMPLE 2 + $NewInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalProjectParameters.ProjectName = 'Platform Telemetry' +$NewInfisicalProjectParameters.Slug = 'platform-telemetry' +$NewInfisicalProjectParameters.Description = 'Secrets for platform telemetry pipeline' +$NewInfisicalProjectParameters.Type = 'secret-manager' +$NewInfisicalProjectParameters.OrganizationId = $ConnectInfisicalParameters.OrganizationId +$NewInfisicalProjectParameters.Verbose = $True + +$NewInfisicalProjectResult = New-InfisicalProject @NewInfisicalProjectParameters + Creates a project with an explicit slug, description, and type bound to a specific organization id. + + + + + + + Update-InfisicalProject + Updates the name, description, or auto-capitalization flag on an existing project. + Update + InfisicalProject + + + Updates mutable attributes on a project. -ProjectId defaults to the pinned session project when omitted. Only parameters that are bound are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + AutoCapitalization controls whether secret names submitted in mixed case are stored uppercase server-side; setting it false preserves the literal case supplied by clients. + + + + + EXAMPLE 1 + Update-InfisicalProject -Name 'Platform Telemetry (v2)' + Renames the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$UpdateInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$UpdateInfisicalProjectParameters.Name = 'Platform Telemetry (v2)' +$UpdateInfisicalProjectParameters.Description = 'Migrated to v2 pipeline' +$UpdateInfisicalProjectParameters.AutoCapitalization = $False +$UpdateInfisicalProjectParameters.Verbose = $True + +$UpdateInfisicalProjectResult = Update-InfisicalProject @UpdateInfisicalProjectParameters + Locates the project by slug, renames it, updates the description, and disables auto-capitalization. + + + + + + + Remove-InfisicalProject + Deletes an Infisical project. + Remove + InfisicalProject + + + Deletes a project by Id. Defaults to the session-pinned project when -ProjectId is omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed project id. + + + Notes + + This is destructive and removes all secrets, environments, folders, and tags within the project. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalProject -Confirm:$False + Deletes the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'sandbox-temp' } + +$RemoveInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$RemoveInfisicalProjectParameters.PassThru = $True +$RemoveInfisicalProjectParameters.Confirm = $False +$RemoveInfisicalProjectParameters.Verbose = $True + +$RemoveInfisicalProjectResult = Remove-InfisicalProject @RemoveInfisicalProjectParameters + Finds the sandbox project by slug, removes it without confirmation, and emits the removed project id for logging. + + + + + + + Get-InfisicalEnvironments + Lists environments defined on an Infisical project. + Get + InfisicalEnvironments + + + Returns all environments configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Each InfisicalEnvironment carries both Id and Slug; downstream cmdlets accept either form on -Environment-like parameters. + + + + + EXAMPLE 1 + Get-InfisicalEnvironments + Lists environments for the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalEnvironmentsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalEnvironmentsParameters.Verbose = $True + +$GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments @GetInfisicalEnvironmentsParameters + Resolves a project by slug and lists every environment defined on it. + + + + + + + Get-InfisicalEnvironment + Retrieves a single Infisical environment by slug or id. + Get + InfisicalEnvironment + + + Returns one environment record by slug or id (-EnvironmentSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalEnvironment objects from Get-InfisicalEnvironments can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalEnvironment -EnvironmentSlugOrId 'dev' + Retrieves the 'dev' environment from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'dev' } + +$GetInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentParameters.EnvironmentSlugOrId = $GetInfisicalEnvironmentsResult.Slug +$GetInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalEnvironmentParameters.Verbose = $True + +$GetInfisicalEnvironmentResult = Get-InfisicalEnvironment @GetInfisicalEnvironmentParameters + Looks up the dev environment by slug and re-fetches the canonical record by slug or id. + + + + + + + New-InfisicalEnvironment + Creates a new environment on an Infisical project. + New + InfisicalEnvironment + + + Creates an environment with the supplied display name and slug, optionally setting its sort -Position. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Slugs must be unique within the project and are used as the canonical -Environment value across all other cmdlets. + + + + + EXAMPLE 1 + New-InfisicalEnvironment -Name 'Staging' -Slug 'staging' + Adds a Staging environment to the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$NewInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectsResult.Id +$NewInfisicalEnvironmentParameters.Name = 'Staging' +$NewInfisicalEnvironmentParameters.Slug = 'staging' +$NewInfisicalEnvironmentParameters.Position = 20 +$NewInfisicalEnvironmentParameters.Verbose = $True + +$NewInfisicalEnvironmentResult = New-InfisicalEnvironment @NewInfisicalEnvironmentParameters + Adds a Staging environment at sort position 20 on the resolved project. + + + + + + + Update-InfisicalEnvironment + Updates the name, slug, or sort order of an existing Infisical environment. + Update + InfisicalEnvironment + + + Updates an environment identified by -EnvironmentId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug can break downstream automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalEnvironment -EnvironmentId $EnvId -Name 'Pre-Production' + Renames an environment in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'staging' } + +$UpdateInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$UpdateInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalEnvironmentParameters.Name = 'Pre-Production' +$UpdateInfisicalEnvironmentParameters.Slug = 'preprod' +$UpdateInfisicalEnvironmentParameters.Position = 25 +$UpdateInfisicalEnvironmentParameters.Verbose = $True + +$UpdateInfisicalEnvironmentResult = Update-InfisicalEnvironment @UpdateInfisicalEnvironmentParameters + Locates the staging environment, renames it to Pre-Production, rotates its slug, and updates its sort order. + + + + + + + Remove-InfisicalEnvironment + Deletes an Infisical environment from a project. + Remove + InfisicalEnvironment + + + Removes an environment by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed environment id. + + + Notes + + Removing an environment deletes every secret and folder scoped to it. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalEnvironment -EnvironmentId $EnvId -Confirm:$False + Deletes an environment without prompting. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'sandbox' } + +$RemoveInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$RemoveInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalEnvironmentParameters.PassThru = $True +$RemoveInfisicalEnvironmentParameters.Confirm = $False +$RemoveInfisicalEnvironmentParameters.Verbose = $True + +$RemoveInfisicalEnvironmentResult = Remove-InfisicalEnvironment @RemoveInfisicalEnvironmentParameters + Removes the sandbox environment without prompting and emits its id for the audit trail. + + + + + + + Get-InfisicalFolders + Lists Infisical folders at a given secret path. + Get + InfisicalFolders + + + Enumerates folders directly under the supplied -Path within the active project and environment. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + This is a non-recursive listing of immediate subfolders. To enumerate secrets across a folder subtree use Get-InfisicalSecrets -Recursive. + + + + + EXAMPLE 1 + Get-InfisicalFolders -Path '/Windows' + Lists every folder directly under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFoldersParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFoldersParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFoldersParameters.Path = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalFoldersParameters.Verbose = $True + +$GetInfisicalFoldersResult = Get-InfisicalFolders @GetInfisicalFoldersParameters + Lists folders under a script-specific subpath using the session-pinned project and environment. + + + + + + + Get-InfisicalFolder + Retrieves a single Infisical folder by name or id. + Get + InfisicalFolder + + + Returns one folder record by name or id (-FolderNameOrId) under the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalFolder objects from Get-InfisicalFolders can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalFolder -FolderNameOrId 'Deployments' -Path '/Windows' + Retrieves the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$GetInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFolderParameters.FolderNameOrId = $GetInfisicalFoldersResult.Id +$GetInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFolderParameters.Path = '/Windows' +$GetInfisicalFolderParameters.Verbose = $True + +$GetInfisicalFolderResult = Get-InfisicalFolder @GetInfisicalFolderParameters + Locates the folder by name first, then re-fetches it by id to refresh the canonical record. + + + + + + + New-InfisicalFolder + Creates a new Infisical folder under the supplied parent path. + New + InfisicalFolder + + + Creates a folder with the supplied -Name beneath the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Folder names are case-sensitive and must be unique within a parent path; the cmdlet does not create intermediate folders. + + + + + EXAMPLE 1 + New-InfisicalFolder -Name 'Deployments' -Path '/Windows' + Creates the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $NewInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalFolderParameters.Name = $CallingScriptPath.BaseName +$NewInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalFolderParameters.Path = '/Windows' +$NewInfisicalFolderParameters.Verbose = $True + +$NewInfisicalFolderResult = New-InfisicalFolder @NewInfisicalFolderParameters + Creates a script-named folder under /Windows using the session-pinned project and environment. + + + + + + + Update-InfisicalFolder + Renames an existing Infisical folder. + Update + InfisicalFolder + + + Renames a folder identified by -FolderId to the supplied -Name. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Renaming a folder rewrites the path component for every secret beneath it; coordinate with consumers that pin to the previous path. + + + + + EXAMPLE 1 + Update-InfisicalFolder -FolderId $FolderId -Name 'Deployments-Archive' + Renames a folder in the session-pinned project/environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$UpdateInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$UpdateInfisicalFolderParameters.Name = 'Deployments-Archive' +$UpdateInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalFolderParameters.Path = '/Windows' +$UpdateInfisicalFolderParameters.Verbose = $True + +$UpdateInfisicalFolderResult = Update-InfisicalFolder @UpdateInfisicalFolderParameters + Resolves the folder by name and renames it to Deployments-Archive. + + + + + + + Remove-InfisicalFolder + Deletes an Infisical folder and all secrets it contains. + Remove + InfisicalFolder + + + Removes a folder by Id from the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed folder id. + + + Notes + + This is destructive and removes every secret and subfolder under the target folder. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalFolder -FolderId $FolderId -Confirm:$False + Deletes a folder from the session-pinned project/environment without prompting. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq $CallingScriptPath.BaseName } + +$RemoveInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$RemoveInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalFolderParameters.Path = '/Windows' +$RemoveInfisicalFolderParameters.PassThru = $True +$RemoveInfisicalFolderParameters.Confirm = $False +$RemoveInfisicalFolderParameters.Verbose = $True + +$RemoveInfisicalFolderResult = Remove-InfisicalFolder @RemoveInfisicalFolderParameters + Resolves the script-named folder under /Windows and removes it without prompting, returning its id for logging. + + + + + + + Get-InfisicalTags + Lists Infisical tags defined on a project. + Get + InfisicalTags + + + Returns every tag configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Tag Ids returned here are the values to pass on -TagIds when creating or updating secrets. + + + + + EXAMPLE 1 + Get-InfisicalTags + Lists every tag defined on the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalTagsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalTagsParameters.Verbose = $True + +$GetInfisicalTagsResult = Get-InfisicalTags @GetInfisicalTagsParameters + Resolves a project by slug and lists every tag defined on it. + + + + + + + Get-InfisicalTag + Retrieves a single Infisical tag by slug or id. + Get + InfisicalTag + + + Returns one tag record by slug or id (-TagSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalTag objects from Get-InfisicalTags can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalTag -TagSlugOrId 'critical' + Retrieves the 'critical' tag from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$GetInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagParameters.TagSlugOrId = $GetInfisicalTagsResult.Slug +$GetInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalTagParameters.Verbose = $True + +$GetInfisicalTagResult = Get-InfisicalTag @GetInfisicalTagParameters + Filters tags to the critical slug and refetches the canonical record. + + + + + + + New-InfisicalTag + Creates a new Infisical tag on a project. + New + InfisicalTag + + + Creates a tag with the supplied -Slug, optional -Name and -Color. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Tag slugs must be unique within the project and are the canonical reference used by tag-filtered secret lookups. + + + + + EXAMPLE 1 + New-InfisicalTag -Slug 'critical' -Name 'Critical' -Color '#FF0000' + Creates a red Critical tag in the session-pinned project. + + + EXAMPLE 2 + $NewInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalTagParameters.Slug = 'critical' +$NewInfisicalTagParameters.Name = 'Critical' +$NewInfisicalTagParameters.Color = '#FF0000' +$NewInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalTagParameters.Verbose = $True + +$NewInfisicalTagResult = New-InfisicalTag @NewInfisicalTagParameters + Creates a red Critical tag against an explicitly supplied project id. + + + + + + + Update-InfisicalTag + Updates the slug, name, or color of an existing Infisical tag. + Update + InfisicalTag + + + Updates a tag identified by -TagId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug breaks tag-filtered automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalTag -TagId $TagId -Color '#FFA500' + Changes the display color of a tag in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$UpdateInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$UpdateInfisicalTagParameters.Slug = 'critical-v2' +$UpdateInfisicalTagParameters.Name = 'Critical (v2)' +$UpdateInfisicalTagParameters.Color = '#FFA500' +$UpdateInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalTagParameters.Verbose = $True + +$UpdateInfisicalTagResult = Update-InfisicalTag @UpdateInfisicalTagParameters + Locates the critical tag and rotates its slug, display name, and color. + + + + + + + Remove-InfisicalTag + Deletes an Infisical tag from a project. + Remove + InfisicalTag + + + Removes a tag by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed tag id. + + + Notes + + Removing a tag detaches it from every secret it was applied to but does not delete the secrets themselves. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalTag -TagId $TagId -Confirm:$False + Deletes a tag from the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical-v2' } + +$RemoveInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$RemoveInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalTagParameters.PassThru = $True +$RemoveInfisicalTagParameters.Confirm = $False +$RemoveInfisicalTagParameters.Verbose = $True + +$RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters + Resolves a tag by slug and removes it without prompting, returning its id for the audit trail. + + + + + + + Get-InfisicalCertificateAuthority + Lists or retrieves Infisical internal Certificate Authorities. + Get + InfisicalCertificateAuthority + + + When -CaId is supplied (ById parameter set) returns a single CA. Otherwise (List parameter set) returns every internal CA visible in the project. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Only internal CAs are surfaced; external/ACME issuers are not enumerated by this cmdlet. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificateAuthority + Lists every internal CA visible in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateAuthorityParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateAuthorityParameters.CaId = $GetInfisicalCertificateAuthorityListResult.Id +$GetInfisicalCertificateAuthorityParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificateAuthorityParameters.Verbose = $True + +$GetInfisicalCertificateAuthorityResult = Get-InfisicalCertificateAuthority @GetInfisicalCertificateAuthorityParameters + Filters the CA list by friendly name and then re-fetches the canonical CA record by id. + + + + + + + Get-InfisicalCertificates + Lists Infisical certificates in a project, with optional filters and automatic paging. + Get + InfisicalCertificates + + + Enumerates certificates with optional filters for -CommonName, -FriendlyName, -Status, and -CaId. -Limit and -Offset drive a single page; pages are walked automatically until exhausted unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + For advanced filtering (validity window, key algorithm, extended key usage, etc.) use Search-InfisicalCertificate instead. + + + + + EXAMPLE 1 + Get-InfisicalCertificates -Status 'active' + Lists every active certificate in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificatesParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificatesParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificatesParameters.CommonName = $env:COMPUTERNAME +$GetInfisicalCertificatesParameters.FriendlyName = 'web-tier' +$GetInfisicalCertificatesParameters.Status = 'active' +$GetInfisicalCertificatesParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$GetInfisicalCertificatesParameters.Limit = 100 +$GetInfisicalCertificatesParameters.Verbose = $True + +$GetInfisicalCertificatesResult = Get-InfisicalCertificates @GetInfisicalCertificatesParameters + Resolves the issuing CA, then lists active certificates scoped to that CA, the local hostname, and the 'web-tier' friendly name. + + + + + + + Get-InfisicalCertificate + Retrieves a single Infisical certificate by serial number. + Get + InfisicalCertificate + + + Returns one certificate record by -SerialNumber. Accepts pipeline input by property name so InfisicalCertificate objects from list/search cmdlets can be re-fetched directly. + + + Notes + + This returns metadata only. To obtain certificate and chain PEM material use ConvertTo-InfisicalCertificate or Export-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber '7A:F2:1B:...:9E' + Retrieves the certificate record for the supplied serial number. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$GetInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$GetInfisicalCertificateParameters.Verbose = $True + +$GetInfisicalCertificateResult = Get-InfisicalCertificate @GetInfisicalCertificateParameters + Selects the active certificate whose common name matches the host and refetches its canonical record. + + + + + + + Search-InfisicalCertificate + Searches Infisical certificates with advanced filters and automatic paging. + Search + InfisicalCertificate + + + Performs a server-side search across certificates with filters for friendly name, common name, free-text search, status, CA/profile/application/enrollment scope, key/signature algorithm, source, and validity window (-NotBeforeFrom/-NotBeforeTo/-NotAfterFrom/-NotAfterTo). Results are paged automatically unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Use -SortBy together with -SortOrder ('asc'/'desc') to control result ordering. Pair with Get-InfisicalCertificate or Export-InfisicalCertificate to drill into specific hits. + + + + + EXAMPLE 1 + Search-InfisicalCertificate -Search $env:COMPUTERNAME -Status 'active' + Finds active certificates whose searchable fields contain the local hostname. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$SearchInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$SearchInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$SearchInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$SearchInfisicalCertificateParameters.Status = 'active' +$SearchInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$SearchInfisicalCertificateParameters.KeyAlgorithm = @('RSA') +$SearchInfisicalCertificateParameters.NotAfterTo = (Get-Date).AddDays(30) +$SearchInfisicalCertificateParameters.SortBy = 'notAfter' +$SearchInfisicalCertificateParameters.SortOrder = 'asc' +$SearchInfisicalCertificateParameters.Limit = 100 +$SearchInfisicalCertificateParameters.Verbose = $True + +$SearchInfisicalCertificateResult = Search-InfisicalCertificate @SearchInfisicalCertificateParameters + Searches for RSA certificates from a specific CA, scoped to the local hostname, that expire within the next 30 days, sorted soonest-first. + + + + + + + Request-InfisicalCertificate + Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. + Request + InfisicalCertificate + + + Generates a keypair locally, builds a CSR, and submits it for signing either via a PKI subscriber (-PkiSubscriberSlug, default parameter set) or by direct CA signing (-CertificateAuthorityId). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm. + + + Notes + + Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely. + + + + + EXAMPLE 1 + Request-InfisicalCertificate -PkiSubscriberSlug 'web-tier' -Install + Requests (or reuses) a certificate for the 'web-tier' subscriber and installs it into CurrentUser\My. + + + EXAMPLE 2 + $RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RequestInfisicalCertificateParameters.PkiSubscriberSlug = 'web-tier' +$RequestInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RequestInfisicalCertificateParameters.CommonName = ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName +$RequestInfisicalCertificateParameters.DnsName = @(([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName, $env:COMPUTERNAME) +$RequestInfisicalCertificateParameters.KeyAlgorithm = 'Rsa' +$RequestInfisicalCertificateParameters.KeySize = 3072 +$RequestInfisicalCertificateParameters.Install = $True +$RequestInfisicalCertificateParameters.InstallChain = $True +$RequestInfisicalCertificateParameters.StoreName = 'My' +$RequestInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$RequestInfisicalCertificateParameters.PrivateKeyProtection = 'NonExportable' +$RequestInfisicalCertificateParameters.MachineKey = $True +$RequestInfisicalCertificateParameters.PersistKey = $True +$RequestInfisicalCertificateParameters.AllowRenewal = $True +$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30 +$RequestInfisicalCertificateParameters.Verbose = $True + +$RequestInfisicalCertificateResult = Request-InfisicalCertificate @RequestInfisicalCertificateParameters + Issues (or renews within 30 days) a 3072-bit RSA certificate for the local FQDN, installs the leaf and chain into LocalMachine\My with a non-exportable machine-bound persistent key. + + + + + + + ConvertTo-InfisicalCertificate + Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. + ConvertTo + InfisicalCertificate + + + Fetches the certificate bundle (when given an InfisicalCertificate or -SerialNumber), or accepts an already-fetched -Bundle, and constructs an X509Certificate2 from the PEM material. Use -NoPrivateKey to omit the private key, -KeyStorageFlags to control how the key is loaded, and -IncludeChain to additionally emit each chain certificate as a separate X509Certificate2 in the pipeline. + + + Notes + + The bundle for any given certificate is typically retrievable only once after issuance; -SerialNumber and pipeline modes will fail with a bundle-not-available error for older certificates. Use -KeyStorageFlags Exportable when callers need to re-export the resulting cert as PFX. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber $Serial | ConvertTo-InfisicalCertificate -IncludeChain + Materializes the certificate and emits each chain element individually. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ConvertToInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ConvertToInfisicalCertificateParameters.NoPrivateKey = $False +$ConvertToInfisicalCertificateParameters.IncludeChain = $True +$ConvertToInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable +$ConvertToInfisicalCertificateParameters.Verbose = $True + +$ConvertToInfisicalCertificateResult = ConvertTo-InfisicalCertificate @ConvertToInfisicalCertificateParameters + Selects the active certificate whose CN matches the host and materializes it (with private key and chain) as exportable X509Certificate2 objects. + + + + + + + Export-InfisicalCertificate + Exports an Infisical certificate to disk in PEM, PFX, or CER format. + Export + InfisicalCertificate + + + Writes a certificate to -Path in the supplied -Format. Accepts an X509Certificate2, an InfisicalCertificateBundle, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -Password (SecureString) supplies the PFX password. -IncludeChain appends chain certificates (PEM only). -NoPrivateKey omits the private key. -Force overwrites an existing file. Honors -WhatIf and -Confirm. + + + Notes + + PFX export requires the cert to have been loaded with X509KeyStorageFlags.Exportable; bundle/serial modes import with Exportable automatically. CER and PFX formats ignore -IncludeChain. + + + + + EXAMPLE 1 + Export-InfisicalCertificate -Path 'C:\Temp\web-tier.pem' -Format Pem -SerialNumber $Serial -IncludeChain + Exports a certificate, its chain, and private key (when available) as a single PEM bundle. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ExportInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ExportInfisicalCertificateParameters.Path = "C:\Temp\$($env:COMPUTERNAME).pfx" +$ExportInfisicalCertificateParameters.Format = 'Pfx' +$ExportInfisicalCertificateParameters.Password = (Read-Host -AsSecureString -Prompt 'PFX password') +$ExportInfisicalCertificateParameters.Force = $True +$ExportInfisicalCertificateParameters.PassThru = $True +$ExportInfisicalCertificateParameters.Verbose = $True + +$ExportInfisicalCertificateResult = Export-InfisicalCertificate @ExportInfisicalCertificateParameters + Resolves the active host certificate by serial and exports it as a password-protected PFX, overwriting any existing file and emitting a FileInfo for downstream use. + + + + + + + Install-InfisicalCertificate + Installs an Infisical certificate (and optional chain) into a Windows certificate store. + Install + InfisicalCertificate + + + Adds a certificate to the supplied -StoreName and -StoreLocation. Accepts an X509Certificate2, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -KeyStorageFlags controls private-key loading. -IncludeChain installs each chain certificate to the CertificateAuthority store of the same -StoreLocation. -Force replaces an existing thumbprint. -PassThru emits the installed certificate. Honors -WhatIf and -Confirm. + + + Notes + + Installing into LocalMachine stores typically requires elevation. -IncludeChain only fires for serial/InfisicalCertificate inputs because the X509Certificate2 input has no associated bundle to walk. + + + + + EXAMPLE 1 + Install-InfisicalCertificate -SerialNumber $Serial -StoreLocation LocalMachine -IncludeChain + Installs the leaf into LocalMachine\My and each chain element into LocalMachine\CertificateAuthority. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$InstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$InstallInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$InstallInfisicalCertificateParameters.StoreName = 'My' +$InstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$InstallInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet +$InstallInfisicalCertificateParameters.IncludeChain = $True +$InstallInfisicalCertificateParameters.Force = $True +$InstallInfisicalCertificateParameters.PassThru = $True +$InstallInfisicalCertificateParameters.Verbose = $True + +$InstallInfisicalCertificateResult = Install-InfisicalCertificate @InstallInfisicalCertificateParameters + Resolves the active host certificate and installs the leaf (with a machine-bound persistent key) plus its chain into LocalMachine, replacing any existing thumbprint match. + + + + + + + Uninstall-InfisicalCertificate + Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. + Uninstall + InfisicalCertificate + + + Removes matching certificates from the supplied -StoreName and -StoreLocation. Accepts -Thumbprint, -Subject, an X509Certificate2 (-Certificate), or an InfisicalCertificate (-InfisicalCertificate, uses FingerprintSha1). -Force allows removing multiple matches in one call; -PassThru emits each removed certificate. Honors -WhatIf and -Confirm. + + + Notes + + When more than one certificate matches -Subject and -Force is not supplied the cmdlet throws to prevent accidental bulk removal. Uninstalling from LocalMachine stores typically requires elevation. + + + + + EXAMPLE 1 + Uninstall-InfisicalCertificate -Thumbprint $Thumbprint -StoreLocation LocalMachine + Removes the certificate with the supplied thumbprint from LocalMachine\My. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'revoked' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$UninstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UninstallInfisicalCertificateParameters.InfisicalCertificate = $GetInfisicalCertificatesResult[0] +$UninstallInfisicalCertificateParameters.StoreName = 'My' +$UninstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$UninstallInfisicalCertificateParameters.Force = $True +$UninstallInfisicalCertificateParameters.PassThru = $True +$UninstallInfisicalCertificateParameters.Verbose = $True + +$UninstallInfisicalCertificateResult = Uninstall-InfisicalCertificate @UninstallInfisicalCertificateParameters + Picks the revoked host certificate and removes it from LocalMachine\My using its SHA1 fingerprint, emitting the removed object for the audit trail. + + + + + + + diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml new file mode 100644 index 0000000..dc9fb90 --- /dev/null +++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml @@ -0,0 +1,1531 @@ + + + + + + Connect-Infisical + Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. + Connect + Infisical + + + Authenticates against an Infisical instance using one of the supported auth providers (UniversalAuth, Token, JWT, OIDC, LDAP, Azure, GCP IAM) and stores the resulting connection in the module-level session manager. Subsequent cmdlets pick up the connection automatically. If parameters such as BaseUri, OrganizationId, ProjectId, Environment, ClientId, or ClientSecret are not supplied, the cmdlet attempts to resolve them from a curated list of environment-variable name patterns across Process, User, and Machine scopes. + + + Notes + + Use -PassThru to emit the resulting InfisicalConnection object; by default the connection is stored silently. SecureString-typed parameters such as ClientSecret, AccessToken, Jwt, and Password are never logged. + The cmdlet pins the API version to the bound value when -ApiVersion is supplied explicitly; otherwise the default 'v4' is used and remains overridable per-call. + + + + + EXAMPLE 1 + Connect-Infisical -BaseUri 'https://app.infisical.com' -ClientId $ClientId -ClientSecret $ClientSecret -OrganizationId $OrgId -ProjectId $ProjectId -Environment 'dev' + Performs a Universal-Auth machine-identity login and stores the resulting session for subsequent cmdlets. + + + EXAMPLE 2 + $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com' +$ConnectInfisicalParameters.OrganizationId = $OrganizationId +$ConnectInfisicalParameters.ProjectId = $ProjectId +$ConnectInfisicalParameters.Environment = 'dev' +$ConnectInfisicalParameters.ClientId = $ClientId +$ConnectInfisicalParameters.ClientSecret = $ClientSecret +$ConnectInfisicalParameters.SecretPath = '/' +$ConnectInfisicalParameters.ApiVersion = 'v4' +$ConnectInfisicalParameters.PassThru = $True +$ConnectInfisicalParameters.Verbose = $True + +$ConnectInfisicalResult = Connect-Infisical @ConnectInfisicalParameters + Builds an ordered parameter dictionary, splats it onto Connect-Infisical, and captures the returned InfisicalConnection for later reuse. + + + + + + + Disconnect-Infisical + Clears the current Infisical session from the module-level session manager. + Disconnect + Infisical + + + Removes the cached InfisicalConnection so subsequent cmdlets that require an active session will fail until Connect-Infisical is invoked again. The cmdlet does not contact the Infisical server. + + + Notes + + Use -PassThru to receive a status object that includes the disconnect timestamp; by default the cmdlet returns no output. + + + + + EXAMPLE 1 + Disconnect-Infisical + Clears the active Infisical session silently. + + + EXAMPLE 2 + $DisconnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$DisconnectInfisicalParameters.PassThru = $True +$DisconnectInfisicalParameters.Verbose = $True + +$DisconnectInfisicalResult = Disconnect-Infisical @DisconnectInfisicalParameters + Disconnects and captures a status object that includes IsConnected and DisconnectedAtUtc for logging. + + + + + + + Get-InfisicalSecret + Retrieves a single Infisical secret by name from the active session's project and environment. + Get + InfisicalSecret + + + Fetches a single secret by name. Project, Environment, SecretPath, and ApiVersion default to the values pinned on the active InfisicalConnection but can be overridden per call. Optional flags request reference-expansion, import inclusion, or a specific historical version. + + + Notes + + The returned InfisicalSecret stores the value as SecureString; call .GetPlainTextValue() to materialize the cleartext value only when strictly required. + + + + + EXAMPLE 1 + Get-InfisicalSecret -SecretName 'DATABASE_URL' + Retrieves the DATABASE_URL secret from the project and environment pinned by Connect-Infisical. + + + EXAMPLE 2 + $GetInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretParameters.SecretName = 'DATABASE_URL' +$GetInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretParameters.ExpandSecretReferences = $True +$GetInfisicalSecretParameters.IncludeImports = $True +$GetInfisicalSecretParameters.Verbose = $True + +$GetInfisicalSecretResult = Get-InfisicalSecret @GetInfisicalSecretParameters + Retrieves a single secret from a script-specific subpath with secret-reference expansion and folder imports enabled. + + + + + + + Get-InfisicalSecrets + Lists Infisical secrets within a project, environment, and optional folder path. + Get + InfisicalSecrets + + + Enumerates secrets under the active session's project and environment, optionally recursing through subfolders. Supports metadata-based filtering, tag-slug filtering, secret-reference expansion, and personal-override inclusion. + + + Notes + + Use -Recursive together with -SecretPath to walk an entire folder subtree. Pipe the result into ConvertTo-InfisicalSecretDictionary for hashtable-style lookup. + + + + + EXAMPLE 1 + Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + Lists every secret under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretsParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretsParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretsParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretsParameters.Recursive = $True +$GetInfisicalSecretsParameters.ExpandSecretReferences = $True +$GetInfisicalSecretsParameters.IncludeImports = $True +$GetInfisicalSecretsParameters.IncludePersonalOverrides = $True +$GetInfisicalSecretsParameters.Verbose = $True + +$GetInfisicalSecretsResult = Get-InfisicalSecrets @GetInfisicalSecretsParameters + Lists secrets under a script-specific subpath with imports, personal overrides, and reference expansion enabled. + + + + + + + New-InfisicalSecret + Creates a new Infisical secret, with support for SecureString values and bulk creation. + New + InfisicalSecret + + + Creates one or many secrets. Three parameter sets are supported: PlainText (SecretName + SecretValue), SecureString (SecretName + SecureSecretValue), and Bulk (an array of hashtables piped or supplied via -Secrets). Honors -WhatIf and -Confirm. + + + Notes + + Pass -SkipMultilineEncoding when the value already contains literal newlines that the server should preserve verbatim. Use -TagIds to attach tag references at creation time. + + + + + EXAMPLE 1 + New-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'super-secret-value' + Creates a single shared secret in the active project/environment. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags -ProjectId $ConnectInfisicalParameters.ProjectId + +$NewInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalSecretParameters.SecretName = 'API_KEY' +$NewInfisicalSecretParameters.SecretValue = 'super-secret-value' +$NewInfisicalSecretParameters.SecretComment = 'Issued by deployment pipeline' +$NewInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$NewInfisicalSecretParameters.TagIds = @($GetInfisicalTagsResult[0].Id) +$NewInfisicalSecretParameters.Verbose = $True + +$NewInfisicalSecretResult = New-InfisicalSecret @NewInfisicalSecretParameters + Looks up tags to attach, then creates a single secret with a comment and tag association under a script-specific subpath. + + + + + + + Update-InfisicalSecret + Updates an existing Infisical secret value, comment, name, or tags. + Update + InfisicalSecret + + + Updates one or many secrets. Supports PlainText, SecureString, and Bulk parameter sets. Use -NewSecretName to rename a secret, -SecretComment to update its comment, and -TagIds to replace tag associations. Honors -WhatIf and -Confirm. + + + Notes + + Only the parameters you bind are sent; omitted scalar parameters are not modified server-side. The Bulk parameter set accepts pipeline input of hashtables containing SecretName/SecretValue/etc. + + + + + EXAMPLE 1 + Update-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'rotated-value' + Rotates the API_KEY secret in the active project/environment. + + + EXAMPLE 2 + $UpdateInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalSecretParameters.SecretName = 'API_KEY' +$UpdateInfisicalSecretParameters.NewSecretName = 'API_KEY_V2' +$UpdateInfisicalSecretParameters.SecretValue = 'rotated-value' +$UpdateInfisicalSecretParameters.SecretComment = 'Rotated by scheduled job' +$UpdateInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$UpdateInfisicalSecretParameters.Verbose = $True + +$UpdateInfisicalSecretResult = Update-InfisicalSecret @UpdateInfisicalSecretParameters + Rotates the value, renames the secret, and updates its comment in a single call. + + + + + + + Remove-InfisicalSecret + Deletes one or many Infisical secrets by name. + Remove + InfisicalSecret + + + Deletes a single secret (Single parameter set) or a batch of secrets by name (Bulk parameter set). High ConfirmImpact triggers prompts by default. -PassThru emits the removed secret names. + + + Notes + + Removal is irreversible from this cmdlet's perspective; rely on Infisical's audit log or secret-version history for forensics. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalSecret -SecretName 'API_KEY_V1' -Confirm:$False + Deletes a single secret without prompting. + + + EXAMPLE 2 + $RemoveInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalSecretParameters.SecretNames = @('LEGACY_KEY_1','LEGACY_KEY_2','LEGACY_KEY_3') +$RemoveInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$RemoveInfisicalSecretParameters.PassThru = $True +$RemoveInfisicalSecretParameters.Confirm = $False +$RemoveInfisicalSecretParameters.Verbose = $True + +$RemoveInfisicalSecretResult = Remove-InfisicalSecret @RemoveInfisicalSecretParameters + Bulk-deletes three legacy secrets and returns the removed names for audit logging. + + + + + + + Copy-InfisicalSecret + Duplicates one or more secrets into a different environment or secret path. + Copy + InfisicalSecret + + + Server-side duplicates an array of secret IDs into a destination environment (and optional destination path), with switches that control whether the value, comment, tags, and metadata are copied. Use Get-InfisicalSecrets followed by selection of the desired Id values to feed -SecretId. + + + Notes + + Set -OverwriteExisting to replace same-named secrets at the destination. Without -CopySecretValue, the destination secrets are created with empty values, preserving only metadata. + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Select-Object -ExpandProperty Id | Copy-InfisicalSecret -DestinationEnvironment 'staging' -CopySecretValue + Copies all secrets from the active environment into 'staging', including their values. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + +$CopyInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$CopyInfisicalSecretParameters.SecretId = $GetInfisicalSecretsResult.Id +$CopyInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$CopyInfisicalSecretParameters.SourceEnvironment = $ConnectInfisicalParameters.Environment +$CopyInfisicalSecretParameters.SourceSecretPath = '/Windows' +$CopyInfisicalSecretParameters.DestinationEnvironment = 'staging' +$CopyInfisicalSecretParameters.DestinationSecretPath = '/Windows' +$CopyInfisicalSecretParameters.OverwriteExisting = $True +$CopyInfisicalSecretParameters.CopySecretValue = $True +$CopyInfisicalSecretParameters.CopySecretComment = $True +$CopyInfisicalSecretParameters.CopyTags = $True +$CopyInfisicalSecretParameters.CopyMetadata = $True +$CopyInfisicalSecretParameters.Verbose = $True + +$CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters + Promotes every Windows secret from the active environment into staging with full value/comment/tag/metadata propagation. + + + + + + + ConvertTo-InfisicalSecretDictionary + Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. + ConvertTo + InfisicalSecretDictionary + + + Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). + + + Notes + + Use this conversion before splatting secrets into another process (-AsPlainText) or before passing them to libraries that expect SecureString-keyed lookups (default). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | ConvertTo-InfisicalSecretDictionary -AsPlainText + Builds a plain-text dictionary of every secret in the active environment. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ConvertToInfisicalSecretDictionaryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalSecretDictionaryParameters.InputObject = $GetInfisicalSecretsResult +$ConvertToInfisicalSecretDictionaryParameters.DuplicateKeyBehavior = 'LastWins' +$ConvertToInfisicalSecretDictionaryParameters.AsPlainText = $True +$ConvertToInfisicalSecretDictionaryParameters.Verbose = $True + +$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters + Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + + + + + + Export-InfisicalSecrets + Exports InfisicalSecret objects to disk or environment variables in a chosen file format. + Export + InfisicalSecrets + + + Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. + + + Notes + + EnvironmentVariables format does not require -Path; all other formats do. User/Machine scopes require appropriate privileges (Machine scope requires elevation on Windows). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Export-InfisicalSecrets -Format DotEnv -Path '.\.env' -Force + Writes the active environment's secrets to a .env file. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ExportInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalSecretsParameters.InputObject = $GetInfisicalSecretsResult +$ExportInfisicalSecretsParameters.Format = 'EnvironmentVariables' +$ExportInfisicalSecretsParameters.Scope = 'Process' +$ExportInfisicalSecretsParameters.Force = $True +$ExportInfisicalSecretsParameters.Verbose = $True + +$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters + Projects the recursive secret result into Process-scope environment variables for the current PowerShell session. + + + + + + + Get-InfisicalProjects + Lists Infisical projects accessible to the current identity. + Get + InfisicalProjects + + + Returns every project the active session can see. The cmdlet requires an active InfisicalConnection but takes no parameters; project visibility is governed by Infisical's role assignments. + + + Notes + + The result is an array of InfisicalProject objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. + + + + + EXAMPLE 1 + Get-InfisicalProjects + Lists every project the current session can see. + + + EXAMPLE 2 + $GetInfisicalProjectsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectsParameters.Verbose = $True + +$GetInfisicalProjectsResult = Get-InfisicalProjects @GetInfisicalProjectsParameters | Where-Object { $_.Slug -ilike 'platform-*' } + Lists projects and filters down to those whose slug begins with 'platform-'. + + + + + + + Get-InfisicalProject + Retrieves a single Infisical project by its identifier. + Get + InfisicalProject + + + Retrieves one project by Id. If -ProjectId is not supplied, the cmdlet falls back to the ProjectId pinned on the active InfisicalConnection. + + + Notes + + The cmdlet accepts pipeline input by property name; objects emitted by Get-InfisicalProjects can be piped in directly to refresh a single record. + + + + + EXAMPLE 1 + Get-InfisicalProject + Retrieves the project pinned by the active session. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects + +$GetInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult[0].Id +$GetInfisicalProjectParameters.Verbose = $True + +$GetInfisicalProjectResult = Get-InfisicalProject @GetInfisicalProjectParameters + Looks up the first project in the list and retrieves its full record. + + + + + + + New-InfisicalProject + Creates a new Infisical project in the active organization. + New + InfisicalProject + + + Creates a project with the supplied name and optional slug, description, type, and organization id. If -OrganizationId is not supplied, the active session's organization is used. Honors -WhatIf and -Confirm. + + + Notes + + Slug must be unique within the organization; if not supplied, the server derives one from the project name. + + + + + EXAMPLE 1 + New-InfisicalProject -ProjectName 'Platform Telemetry' + Creates a new project named 'Platform Telemetry' in the active organization. + + + EXAMPLE 2 + $NewInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalProjectParameters.ProjectName = 'Platform Telemetry' +$NewInfisicalProjectParameters.Slug = 'platform-telemetry' +$NewInfisicalProjectParameters.Description = 'Secrets for platform telemetry pipeline' +$NewInfisicalProjectParameters.Type = 'secret-manager' +$NewInfisicalProjectParameters.OrganizationId = $ConnectInfisicalParameters.OrganizationId +$NewInfisicalProjectParameters.Verbose = $True + +$NewInfisicalProjectResult = New-InfisicalProject @NewInfisicalProjectParameters + Creates a project with an explicit slug, description, and type bound to a specific organization id. + + + + + + + Update-InfisicalProject + Updates the name, description, or auto-capitalization flag on an existing project. + Update + InfisicalProject + + + Updates mutable attributes on a project. -ProjectId defaults to the pinned session project when omitted. Only parameters that are bound are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + AutoCapitalization controls whether secret names submitted in mixed case are stored uppercase server-side; setting it false preserves the literal case supplied by clients. + + + + + EXAMPLE 1 + Update-InfisicalProject -Name 'Platform Telemetry (v2)' + Renames the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$UpdateInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$UpdateInfisicalProjectParameters.Name = 'Platform Telemetry (v2)' +$UpdateInfisicalProjectParameters.Description = 'Migrated to v2 pipeline' +$UpdateInfisicalProjectParameters.AutoCapitalization = $False +$UpdateInfisicalProjectParameters.Verbose = $True + +$UpdateInfisicalProjectResult = Update-InfisicalProject @UpdateInfisicalProjectParameters + Locates the project by slug, renames it, updates the description, and disables auto-capitalization. + + + + + + + Remove-InfisicalProject + Deletes an Infisical project. + Remove + InfisicalProject + + + Deletes a project by Id. Defaults to the session-pinned project when -ProjectId is omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed project id. + + + Notes + + This is destructive and removes all secrets, environments, folders, and tags within the project. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalProject -Confirm:$False + Deletes the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'sandbox-temp' } + +$RemoveInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$RemoveInfisicalProjectParameters.PassThru = $True +$RemoveInfisicalProjectParameters.Confirm = $False +$RemoveInfisicalProjectParameters.Verbose = $True + +$RemoveInfisicalProjectResult = Remove-InfisicalProject @RemoveInfisicalProjectParameters + Finds the sandbox project by slug, removes it without confirmation, and emits the removed project id for logging. + + + + + + + Get-InfisicalEnvironments + Lists environments defined on an Infisical project. + Get + InfisicalEnvironments + + + Returns all environments configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Each InfisicalEnvironment carries both Id and Slug; downstream cmdlets accept either form on -Environment-like parameters. + + + + + EXAMPLE 1 + Get-InfisicalEnvironments + Lists environments for the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalEnvironmentsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalEnvironmentsParameters.Verbose = $True + +$GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments @GetInfisicalEnvironmentsParameters + Resolves a project by slug and lists every environment defined on it. + + + + + + + Get-InfisicalEnvironment + Retrieves a single Infisical environment by slug or id. + Get + InfisicalEnvironment + + + Returns one environment record by slug or id (-EnvironmentSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalEnvironment objects from Get-InfisicalEnvironments can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalEnvironment -EnvironmentSlugOrId 'dev' + Retrieves the 'dev' environment from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'dev' } + +$GetInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentParameters.EnvironmentSlugOrId = $GetInfisicalEnvironmentsResult.Slug +$GetInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalEnvironmentParameters.Verbose = $True + +$GetInfisicalEnvironmentResult = Get-InfisicalEnvironment @GetInfisicalEnvironmentParameters + Looks up the dev environment by slug and re-fetches the canonical record by slug or id. + + + + + + + New-InfisicalEnvironment + Creates a new environment on an Infisical project. + New + InfisicalEnvironment + + + Creates an environment with the supplied display name and slug, optionally setting its sort -Position. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Slugs must be unique within the project and are used as the canonical -Environment value across all other cmdlets. + + + + + EXAMPLE 1 + New-InfisicalEnvironment -Name 'Staging' -Slug 'staging' + Adds a Staging environment to the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$NewInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectsResult.Id +$NewInfisicalEnvironmentParameters.Name = 'Staging' +$NewInfisicalEnvironmentParameters.Slug = 'staging' +$NewInfisicalEnvironmentParameters.Position = 20 +$NewInfisicalEnvironmentParameters.Verbose = $True + +$NewInfisicalEnvironmentResult = New-InfisicalEnvironment @NewInfisicalEnvironmentParameters + Adds a Staging environment at sort position 20 on the resolved project. + + + + + + + Update-InfisicalEnvironment + Updates the name, slug, or sort order of an existing Infisical environment. + Update + InfisicalEnvironment + + + Updates an environment identified by -EnvironmentId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug can break downstream automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalEnvironment -EnvironmentId $EnvId -Name 'Pre-Production' + Renames an environment in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'staging' } + +$UpdateInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$UpdateInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalEnvironmentParameters.Name = 'Pre-Production' +$UpdateInfisicalEnvironmentParameters.Slug = 'preprod' +$UpdateInfisicalEnvironmentParameters.Position = 25 +$UpdateInfisicalEnvironmentParameters.Verbose = $True + +$UpdateInfisicalEnvironmentResult = Update-InfisicalEnvironment @UpdateInfisicalEnvironmentParameters + Locates the staging environment, renames it to Pre-Production, rotates its slug, and updates its sort order. + + + + + + + Remove-InfisicalEnvironment + Deletes an Infisical environment from a project. + Remove + InfisicalEnvironment + + + Removes an environment by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed environment id. + + + Notes + + Removing an environment deletes every secret and folder scoped to it. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalEnvironment -EnvironmentId $EnvId -Confirm:$False + Deletes an environment without prompting. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'sandbox' } + +$RemoveInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$RemoveInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalEnvironmentParameters.PassThru = $True +$RemoveInfisicalEnvironmentParameters.Confirm = $False +$RemoveInfisicalEnvironmentParameters.Verbose = $True + +$RemoveInfisicalEnvironmentResult = Remove-InfisicalEnvironment @RemoveInfisicalEnvironmentParameters + Removes the sandbox environment without prompting and emits its id for the audit trail. + + + + + + + Get-InfisicalFolders + Lists Infisical folders at a given secret path. + Get + InfisicalFolders + + + Enumerates folders directly under the supplied -Path within the active project and environment. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + This is a non-recursive listing of immediate subfolders. To enumerate secrets across a folder subtree use Get-InfisicalSecrets -Recursive. + + + + + EXAMPLE 1 + Get-InfisicalFolders -Path '/Windows' + Lists every folder directly under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFoldersParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFoldersParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFoldersParameters.Path = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalFoldersParameters.Verbose = $True + +$GetInfisicalFoldersResult = Get-InfisicalFolders @GetInfisicalFoldersParameters + Lists folders under a script-specific subpath using the session-pinned project and environment. + + + + + + + Get-InfisicalFolder + Retrieves a single Infisical folder by name or id. + Get + InfisicalFolder + + + Returns one folder record by name or id (-FolderNameOrId) under the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalFolder objects from Get-InfisicalFolders can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalFolder -FolderNameOrId 'Deployments' -Path '/Windows' + Retrieves the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$GetInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFolderParameters.FolderNameOrId = $GetInfisicalFoldersResult.Id +$GetInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFolderParameters.Path = '/Windows' +$GetInfisicalFolderParameters.Verbose = $True + +$GetInfisicalFolderResult = Get-InfisicalFolder @GetInfisicalFolderParameters + Locates the folder by name first, then re-fetches it by id to refresh the canonical record. + + + + + + + New-InfisicalFolder + Creates a new Infisical folder under the supplied parent path. + New + InfisicalFolder + + + Creates a folder with the supplied -Name beneath the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Folder names are case-sensitive and must be unique within a parent path; the cmdlet does not create intermediate folders. + + + + + EXAMPLE 1 + New-InfisicalFolder -Name 'Deployments' -Path '/Windows' + Creates the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $NewInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalFolderParameters.Name = $CallingScriptPath.BaseName +$NewInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalFolderParameters.Path = '/Windows' +$NewInfisicalFolderParameters.Verbose = $True + +$NewInfisicalFolderResult = New-InfisicalFolder @NewInfisicalFolderParameters + Creates a script-named folder under /Windows using the session-pinned project and environment. + + + + + + + Update-InfisicalFolder + Renames an existing Infisical folder. + Update + InfisicalFolder + + + Renames a folder identified by -FolderId to the supplied -Name. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Renaming a folder rewrites the path component for every secret beneath it; coordinate with consumers that pin to the previous path. + + + + + EXAMPLE 1 + Update-InfisicalFolder -FolderId $FolderId -Name 'Deployments-Archive' + Renames a folder in the session-pinned project/environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$UpdateInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$UpdateInfisicalFolderParameters.Name = 'Deployments-Archive' +$UpdateInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalFolderParameters.Path = '/Windows' +$UpdateInfisicalFolderParameters.Verbose = $True + +$UpdateInfisicalFolderResult = Update-InfisicalFolder @UpdateInfisicalFolderParameters + Resolves the folder by name and renames it to Deployments-Archive. + + + + + + + Remove-InfisicalFolder + Deletes an Infisical folder and all secrets it contains. + Remove + InfisicalFolder + + + Removes a folder by Id from the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed folder id. + + + Notes + + This is destructive and removes every secret and subfolder under the target folder. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalFolder -FolderId $FolderId -Confirm:$False + Deletes a folder from the session-pinned project/environment without prompting. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq $CallingScriptPath.BaseName } + +$RemoveInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$RemoveInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalFolderParameters.Path = '/Windows' +$RemoveInfisicalFolderParameters.PassThru = $True +$RemoveInfisicalFolderParameters.Confirm = $False +$RemoveInfisicalFolderParameters.Verbose = $True + +$RemoveInfisicalFolderResult = Remove-InfisicalFolder @RemoveInfisicalFolderParameters + Resolves the script-named folder under /Windows and removes it without prompting, returning its id for logging. + + + + + + + Get-InfisicalTags + Lists Infisical tags defined on a project. + Get + InfisicalTags + + + Returns every tag configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Tag Ids returned here are the values to pass on -TagIds when creating or updating secrets. + + + + + EXAMPLE 1 + Get-InfisicalTags + Lists every tag defined on the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalTagsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalTagsParameters.Verbose = $True + +$GetInfisicalTagsResult = Get-InfisicalTags @GetInfisicalTagsParameters + Resolves a project by slug and lists every tag defined on it. + + + + + + + Get-InfisicalTag + Retrieves a single Infisical tag by slug or id. + Get + InfisicalTag + + + Returns one tag record by slug or id (-TagSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalTag objects from Get-InfisicalTags can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalTag -TagSlugOrId 'critical' + Retrieves the 'critical' tag from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$GetInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagParameters.TagSlugOrId = $GetInfisicalTagsResult.Slug +$GetInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalTagParameters.Verbose = $True + +$GetInfisicalTagResult = Get-InfisicalTag @GetInfisicalTagParameters + Filters tags to the critical slug and refetches the canonical record. + + + + + + + New-InfisicalTag + Creates a new Infisical tag on a project. + New + InfisicalTag + + + Creates a tag with the supplied -Slug, optional -Name and -Color. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Tag slugs must be unique within the project and are the canonical reference used by tag-filtered secret lookups. + + + + + EXAMPLE 1 + New-InfisicalTag -Slug 'critical' -Name 'Critical' -Color '#FF0000' + Creates a red Critical tag in the session-pinned project. + + + EXAMPLE 2 + $NewInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalTagParameters.Slug = 'critical' +$NewInfisicalTagParameters.Name = 'Critical' +$NewInfisicalTagParameters.Color = '#FF0000' +$NewInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalTagParameters.Verbose = $True + +$NewInfisicalTagResult = New-InfisicalTag @NewInfisicalTagParameters + Creates a red Critical tag against an explicitly supplied project id. + + + + + + + Update-InfisicalTag + Updates the slug, name, or color of an existing Infisical tag. + Update + InfisicalTag + + + Updates a tag identified by -TagId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug breaks tag-filtered automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalTag -TagId $TagId -Color '#FFA500' + Changes the display color of a tag in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$UpdateInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$UpdateInfisicalTagParameters.Slug = 'critical-v2' +$UpdateInfisicalTagParameters.Name = 'Critical (v2)' +$UpdateInfisicalTagParameters.Color = '#FFA500' +$UpdateInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalTagParameters.Verbose = $True + +$UpdateInfisicalTagResult = Update-InfisicalTag @UpdateInfisicalTagParameters + Locates the critical tag and rotates its slug, display name, and color. + + + + + + + Remove-InfisicalTag + Deletes an Infisical tag from a project. + Remove + InfisicalTag + + + Removes a tag by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed tag id. + + + Notes + + Removing a tag detaches it from every secret it was applied to but does not delete the secrets themselves. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalTag -TagId $TagId -Confirm:$False + Deletes a tag from the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical-v2' } + +$RemoveInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$RemoveInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalTagParameters.PassThru = $True +$RemoveInfisicalTagParameters.Confirm = $False +$RemoveInfisicalTagParameters.Verbose = $True + +$RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters + Resolves a tag by slug and removes it without prompting, returning its id for the audit trail. + + + + + + + Get-InfisicalCertificateAuthority + Lists or retrieves Infisical internal Certificate Authorities. + Get + InfisicalCertificateAuthority + + + When -CaId is supplied (ById parameter set) returns a single CA. Otherwise (List parameter set) returns every internal CA visible in the project. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Only internal CAs are surfaced; external/ACME issuers are not enumerated by this cmdlet. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificateAuthority + Lists every internal CA visible in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateAuthorityParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateAuthorityParameters.CaId = $GetInfisicalCertificateAuthorityListResult.Id +$GetInfisicalCertificateAuthorityParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificateAuthorityParameters.Verbose = $True + +$GetInfisicalCertificateAuthorityResult = Get-InfisicalCertificateAuthority @GetInfisicalCertificateAuthorityParameters + Filters the CA list by friendly name and then re-fetches the canonical CA record by id. + + + + + + + Get-InfisicalCertificates + Lists Infisical certificates in a project, with optional filters and automatic paging. + Get + InfisicalCertificates + + + Enumerates certificates with optional filters for -CommonName, -FriendlyName, -Status, and -CaId. -Limit and -Offset drive a single page; pages are walked automatically until exhausted unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + For advanced filtering (validity window, key algorithm, extended key usage, etc.) use Search-InfisicalCertificate instead. + + + + + EXAMPLE 1 + Get-InfisicalCertificates -Status 'active' + Lists every active certificate in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificatesParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificatesParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificatesParameters.CommonName = $env:COMPUTERNAME +$GetInfisicalCertificatesParameters.FriendlyName = 'web-tier' +$GetInfisicalCertificatesParameters.Status = 'active' +$GetInfisicalCertificatesParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$GetInfisicalCertificatesParameters.Limit = 100 +$GetInfisicalCertificatesParameters.Verbose = $True + +$GetInfisicalCertificatesResult = Get-InfisicalCertificates @GetInfisicalCertificatesParameters + Resolves the issuing CA, then lists active certificates scoped to that CA, the local hostname, and the 'web-tier' friendly name. + + + + + + + Get-InfisicalCertificate + Retrieves a single Infisical certificate by serial number. + Get + InfisicalCertificate + + + Returns one certificate record by -SerialNumber. Accepts pipeline input by property name so InfisicalCertificate objects from list/search cmdlets can be re-fetched directly. + + + Notes + + This returns metadata only. To obtain certificate and chain PEM material use ConvertTo-InfisicalCertificate or Export-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber '7A:F2:1B:...:9E' + Retrieves the certificate record for the supplied serial number. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$GetInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$GetInfisicalCertificateParameters.Verbose = $True + +$GetInfisicalCertificateResult = Get-InfisicalCertificate @GetInfisicalCertificateParameters + Selects the active certificate whose common name matches the host and refetches its canonical record. + + + + + + + Search-InfisicalCertificate + Searches Infisical certificates with advanced filters and automatic paging. + Search + InfisicalCertificate + + + Performs a server-side search across certificates with filters for friendly name, common name, free-text search, status, CA/profile/application/enrollment scope, key/signature algorithm, source, and validity window (-NotBeforeFrom/-NotBeforeTo/-NotAfterFrom/-NotAfterTo). Results are paged automatically unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Use -SortBy together with -SortOrder ('asc'/'desc') to control result ordering. Pair with Get-InfisicalCertificate or Export-InfisicalCertificate to drill into specific hits. + + + + + EXAMPLE 1 + Search-InfisicalCertificate -Search $env:COMPUTERNAME -Status 'active' + Finds active certificates whose searchable fields contain the local hostname. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$SearchInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$SearchInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$SearchInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$SearchInfisicalCertificateParameters.Status = 'active' +$SearchInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$SearchInfisicalCertificateParameters.KeyAlgorithm = @('RSA') +$SearchInfisicalCertificateParameters.NotAfterTo = (Get-Date).AddDays(30) +$SearchInfisicalCertificateParameters.SortBy = 'notAfter' +$SearchInfisicalCertificateParameters.SortOrder = 'asc' +$SearchInfisicalCertificateParameters.Limit = 100 +$SearchInfisicalCertificateParameters.Verbose = $True + +$SearchInfisicalCertificateResult = Search-InfisicalCertificate @SearchInfisicalCertificateParameters + Searches for RSA certificates from a specific CA, scoped to the local hostname, that expire within the next 30 days, sorted soonest-first. + + + + + + + Request-InfisicalCertificate + Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. + Request + InfisicalCertificate + + + Generates a keypair locally, builds a CSR, and submits it for signing either via a PKI subscriber (-PkiSubscriberSlug, default parameter set) or by direct CA signing (-CertificateAuthorityId). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm. + + + Notes + + Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely. + + + + + EXAMPLE 1 + Request-InfisicalCertificate -PkiSubscriberSlug 'web-tier' -Install + Requests (or reuses) a certificate for the 'web-tier' subscriber and installs it into CurrentUser\My. + + + EXAMPLE 2 + $RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RequestInfisicalCertificateParameters.PkiSubscriberSlug = 'web-tier' +$RequestInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RequestInfisicalCertificateParameters.CommonName = ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName +$RequestInfisicalCertificateParameters.DnsName = @(([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName, $env:COMPUTERNAME) +$RequestInfisicalCertificateParameters.KeyAlgorithm = 'Rsa' +$RequestInfisicalCertificateParameters.KeySize = 3072 +$RequestInfisicalCertificateParameters.Install = $True +$RequestInfisicalCertificateParameters.InstallChain = $True +$RequestInfisicalCertificateParameters.StoreName = 'My' +$RequestInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$RequestInfisicalCertificateParameters.PrivateKeyProtection = 'NonExportable' +$RequestInfisicalCertificateParameters.MachineKey = $True +$RequestInfisicalCertificateParameters.PersistKey = $True +$RequestInfisicalCertificateParameters.AllowRenewal = $True +$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30 +$RequestInfisicalCertificateParameters.Verbose = $True + +$RequestInfisicalCertificateResult = Request-InfisicalCertificate @RequestInfisicalCertificateParameters + Issues (or renews within 30 days) a 3072-bit RSA certificate for the local FQDN, installs the leaf and chain into LocalMachine\My with a non-exportable machine-bound persistent key. + + + + + + + ConvertTo-InfisicalCertificate + Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. + ConvertTo + InfisicalCertificate + + + Fetches the certificate bundle (when given an InfisicalCertificate or -SerialNumber), or accepts an already-fetched -Bundle, and constructs an X509Certificate2 from the PEM material. Use -NoPrivateKey to omit the private key, -KeyStorageFlags to control how the key is loaded, and -IncludeChain to additionally emit each chain certificate as a separate X509Certificate2 in the pipeline. + + + Notes + + The bundle for any given certificate is typically retrievable only once after issuance; -SerialNumber and pipeline modes will fail with a bundle-not-available error for older certificates. Use -KeyStorageFlags Exportable when callers need to re-export the resulting cert as PFX. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber $Serial | ConvertTo-InfisicalCertificate -IncludeChain + Materializes the certificate and emits each chain element individually. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ConvertToInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ConvertToInfisicalCertificateParameters.NoPrivateKey = $False +$ConvertToInfisicalCertificateParameters.IncludeChain = $True +$ConvertToInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable +$ConvertToInfisicalCertificateParameters.Verbose = $True + +$ConvertToInfisicalCertificateResult = ConvertTo-InfisicalCertificate @ConvertToInfisicalCertificateParameters + Selects the active certificate whose CN matches the host and materializes it (with private key and chain) as exportable X509Certificate2 objects. + + + + + + + Export-InfisicalCertificate + Exports an Infisical certificate to disk in PEM, PFX, or CER format. + Export + InfisicalCertificate + + + Writes a certificate to -Path in the supplied -Format. Accepts an X509Certificate2, an InfisicalCertificateBundle, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -Password (SecureString) supplies the PFX password. -IncludeChain appends chain certificates (PEM only). -NoPrivateKey omits the private key. -Force overwrites an existing file. Honors -WhatIf and -Confirm. + + + Notes + + PFX export requires the cert to have been loaded with X509KeyStorageFlags.Exportable; bundle/serial modes import with Exportable automatically. CER and PFX formats ignore -IncludeChain. + + + + + EXAMPLE 1 + Export-InfisicalCertificate -Path 'C:\Temp\web-tier.pem' -Format Pem -SerialNumber $Serial -IncludeChain + Exports a certificate, its chain, and private key (when available) as a single PEM bundle. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ExportInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ExportInfisicalCertificateParameters.Path = "C:\Temp\$($env:COMPUTERNAME).pfx" +$ExportInfisicalCertificateParameters.Format = 'Pfx' +$ExportInfisicalCertificateParameters.Password = (Read-Host -AsSecureString -Prompt 'PFX password') +$ExportInfisicalCertificateParameters.Force = $True +$ExportInfisicalCertificateParameters.PassThru = $True +$ExportInfisicalCertificateParameters.Verbose = $True + +$ExportInfisicalCertificateResult = Export-InfisicalCertificate @ExportInfisicalCertificateParameters + Resolves the active host certificate by serial and exports it as a password-protected PFX, overwriting any existing file and emitting a FileInfo for downstream use. + + + + + + + Install-InfisicalCertificate + Installs an Infisical certificate (and optional chain) into a Windows certificate store. + Install + InfisicalCertificate + + + Adds a certificate to the supplied -StoreName and -StoreLocation. Accepts an X509Certificate2, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -KeyStorageFlags controls private-key loading. -IncludeChain installs each chain certificate to the CertificateAuthority store of the same -StoreLocation. -Force replaces an existing thumbprint. -PassThru emits the installed certificate. Honors -WhatIf and -Confirm. + + + Notes + + Installing into LocalMachine stores typically requires elevation. -IncludeChain only fires for serial/InfisicalCertificate inputs because the X509Certificate2 input has no associated bundle to walk. + + + + + EXAMPLE 1 + Install-InfisicalCertificate -SerialNumber $Serial -StoreLocation LocalMachine -IncludeChain + Installs the leaf into LocalMachine\My and each chain element into LocalMachine\CertificateAuthority. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$InstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$InstallInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$InstallInfisicalCertificateParameters.StoreName = 'My' +$InstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$InstallInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet +$InstallInfisicalCertificateParameters.IncludeChain = $True +$InstallInfisicalCertificateParameters.Force = $True +$InstallInfisicalCertificateParameters.PassThru = $True +$InstallInfisicalCertificateParameters.Verbose = $True + +$InstallInfisicalCertificateResult = Install-InfisicalCertificate @InstallInfisicalCertificateParameters + Resolves the active host certificate and installs the leaf (with a machine-bound persistent key) plus its chain into LocalMachine, replacing any existing thumbprint match. + + + + + + + Uninstall-InfisicalCertificate + Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. + Uninstall + InfisicalCertificate + + + Removes matching certificates from the supplied -StoreName and -StoreLocation. Accepts -Thumbprint, -Subject, an X509Certificate2 (-Certificate), or an InfisicalCertificate (-InfisicalCertificate, uses FingerprintSha1). -Force allows removing multiple matches in one call; -PassThru emits each removed certificate. Honors -WhatIf and -Confirm. + + + Notes + + When more than one certificate matches -Subject and -Force is not supplied the cmdlet throws to prevent accidental bulk removal. Uninstalling from LocalMachine stores typically requires elevation. + + + + + EXAMPLE 1 + Uninstall-InfisicalCertificate -Thumbprint $Thumbprint -StoreLocation LocalMachine + Removes the certificate with the supplied thumbprint from LocalMachine\My. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'revoked' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$UninstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UninstallInfisicalCertificateParameters.InfisicalCertificate = $GetInfisicalCertificatesResult[0] +$UninstallInfisicalCertificateParameters.StoreName = 'My' +$UninstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$UninstallInfisicalCertificateParameters.Force = $True +$UninstallInfisicalCertificateParameters.PassThru = $True +$UninstallInfisicalCertificateParameters.Verbose = $True + +$UninstallInfisicalCertificateResult = Uninstall-InfisicalCertificate @UninstallInfisicalCertificateParameters + Picks the revoked host certificate and removes it from LocalMachine\My using its SHA1 fingerprint, emitting the removed object for the audit trail. + + + + + + + diff --git a/build.ps1 b/build.ps1 index ae27921..b928622 100644 --- a/build.ps1 +++ b/build.ps1 @@ -129,7 +129,10 @@ function Write-Manifest { 'Update-InfisicalTag', 'Remove-InfisicalTag', 'Get-InfisicalCertificateAuthority', + 'Get-InfisicalCertificate', + 'Get-InfisicalCertificates', 'Search-InfisicalCertificate', + 'Request-InfisicalCertificate', 'ConvertTo-InfisicalCertificate', 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', @@ -193,15 +196,50 @@ if (`$null -eq `$manifest) { Import-Module -Name '$($ModuleDirectory.FullName)' -Force -`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Search-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate') -foreach (`$c in `$cmds) { - if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { - throw "Cmdlet not found: `$c" +`$cmds = @(Get-Command -Module PSInfisicalAPI -CommandType Cmdlet) +if (`$cmds.Count -eq 0) { + throw "No cmdlets were exported by the PSInfisicalAPI module." +} + +`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-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','Get-InfisicalCertificate','Get-InfisicalCertificates','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate') +foreach (`$expected in `$expectedCmds) { + if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { + throw "Cmdlet not found: `$expected" + } +} + +foreach (`$cmd in `$cmds) { + `$name = `$cmd.Name + `$help = Get-Help -Name `$name -Full -ErrorAction SilentlyContinue + if (`$null -eq `$help) { + throw "Get-Help returned nothing for cmdlet: `$name" } - `$help = Get-Help -Name `$c -ErrorAction SilentlyContinue - if (`$null -eq `$help) { - throw "Get-Help returned nothing for cmdlet: `$c" + `$synopsis = (`$help.Synopsis | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$synopsis) -or `$synopsis.StartsWith(`$name, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Get-Help synopsis is missing or auto-generated for cmdlet: `$name" + } + + `$description = (`$help.description | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$description)) { + throw "Get-Help description is empty for cmdlet: `$name" + } + + `$examples = Get-Help -Name `$name -Examples -ErrorAction SilentlyContinue + if (`$null -eq `$examples -or `$null -eq `$examples.examples -or `$null -eq `$examples.examples.example) { + throw "Get-Help -Examples returned no examples for cmdlet: `$name" + } + + `$exampleNodes = @(`$examples.examples.example) + if (`$exampleNodes.Count -lt 1) { + throw "Get-Help -Examples returned zero examples for cmdlet: `$name" + } + + foreach (`$example in `$exampleNodes) { + `$code = (`$example.code | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$code)) { + throw "Example with empty code block found for cmdlet: `$name" + } } } @@ -297,6 +335,35 @@ foreach ($assembly in $desiredAssemblies) { } } +Write-Step "Staging cmdlet help XML next to module binary" +$moduleCultureDirs = Get-ChildItem -LiteralPath $ModuleRoot.FullName -Directory -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^[a-z]{2}(-[A-Za-z0-9]+)*$' } +foreach ($cultureDir in $moduleCultureDirs) { + $helpXmlSource = [System.IO.FileInfo][System.IO.Path]::Combine($cultureDir.FullName, 'PSInfisicalAPI.dll-Help.xml') + if (-not $helpXmlSource.Exists) { continue } + + $binCultureDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, $cultureDir.Name) + Ensure-Directory -Directory $binCultureDir + Copy-Item -LiteralPath $helpXmlSource.FullName -Destination $binCultureDir.FullName -Force +} + +$primaryHelpXml = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, 'en-US', 'PSInfisicalAPI.dll-Help.xml') +if (-not $primaryHelpXml.Exists) { + throw "Help XML not found at '$($primaryHelpXml.FullName)'. Ensure Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml exists." +} + +try { + [xml]$helpDocument = Get-Content -LiteralPath $primaryHelpXml.FullName -Raw +} catch { + throw "Help XML at '$($primaryHelpXml.FullName)' failed to parse as XML: $_" +} + +$helpCommandCount = @($helpDocument.helpItems.command).Count +if ($helpCommandCount -lt 1) { + throw "Help XML at '$($primaryHelpXml.FullName)' contains no entries." +} +Write-Step "Help XML contains $helpCommandCount cmdlet entries." + $manifestPath = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleRoot.FullName, 'PSInfisicalAPI.psd1') Write-Manifest -Path $manifestPath -ModuleVersion $buildVersion -CommitHash $commitHash diff --git a/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs b/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs index a9653b3..13d21f4 100644 --- a/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs +++ b/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs @@ -12,6 +12,7 @@ namespace PSInfisicalAPI.Tests 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 CaConfigDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalInternalCaConfigurationDto", true); private static readonly Type BundleDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateBundleResponseDto", true); private static InfisicalCertificate InvokeCertMap(object dto, string fallbackProjectId) @@ -90,6 +91,43 @@ namespace PSInfisicalAPI.Tests Assert.Equal("proj-fallback", mapped.ProjectId); } + [Fact] + public void CaMap_Prefers_Configuration_Fields_Over_TopLevel() + { + object cfg = Activator.CreateInstance(CaConfigDtoType); + CaConfigDtoType.GetProperty("FriendlyName").SetValue(cfg, "C=US, CN=GSPA Intermediate"); + CaConfigDtoType.GetProperty("CommonName").SetValue(cfg, "GSPA Intermediate"); + CaConfigDtoType.GetProperty("OrganizationName").SetValue(cfg, "GSPA"); + CaConfigDtoType.GetProperty("OrganizationUnit").SetValue(cfg, "MECM"); + CaConfigDtoType.GetProperty("Country").SetValue(cfg, "US"); + CaConfigDtoType.GetProperty("KeyAlgorithm").SetValue(cfg, "RSA_2048"); + CaConfigDtoType.GetProperty("DistinguishedName").SetValue(cfg, "CN=GSPA Intermediate"); + CaConfigDtoType.GetProperty("SerialNumber").SetValue(cfg, "74a4b62197ad"); + CaConfigDtoType.GetProperty("MaxPathLength").SetValue(cfg, 0); + CaConfigDtoType.GetProperty("Type").SetValue(cfg, "intermediate"); + + object dto = Activator.CreateInstance(CaDtoType); + CaDtoType.GetProperty("Id").SetValue(dto, "ca-9"); + CaDtoType.GetProperty("Name").SetValue(dto, "intermediate-ca"); + CaDtoType.GetProperty("Type").SetValue(dto, "internal"); + CaDtoType.GetProperty("Status").SetValue(dto, "active"); + CaDtoType.GetProperty("Configuration").SetValue(dto, cfg); + + InfisicalCertificateAuthority mapped = InvokeCaMap(dto, "proj-fallback"); + Assert.Equal("ca-9", mapped.Id); + Assert.Equal("intermediate-ca", mapped.Name); + Assert.Equal("internal", mapped.Type); + Assert.Equal("C=US, CN=GSPA Intermediate", mapped.FriendlyName); + Assert.Equal("GSPA Intermediate", mapped.CommonName); + Assert.Equal("GSPA", mapped.OrganizationName); + Assert.Equal("MECM", mapped.OrganizationUnit); + Assert.Equal("US", mapped.Country); + Assert.Equal("RSA_2048", mapped.KeyAlgorithm); + Assert.Equal("CN=GSPA Intermediate", mapped.DistinguishedName); + Assert.Equal("74a4b62197ad", mapped.SerialNumber); + Assert.Equal(0, mapped.MaxPathLength); + } + [Fact] public void BundleMap_Maps_All_Pem_Fields() { diff --git a/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs new file mode 100644 index 0000000..e048a6a --- /dev/null +++ b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; +using PSInfisicalAPI.Endpoints; +using PSInfisicalAPI.Pki; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class CsrAndRequestCmdletTests + { + private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly; + + [Fact] + public void CsrBuilder_Rsa2048_Produces_Pem_Csr_And_PrivateKey_With_Subject_And_Sans() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject + { + CommonName = "test.contoso.local", + Organization = "Contoso", + Country = "US" + }; + + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Rsa, RsaKeySize = 2048 }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "test.contoso.local", "alt.contoso.local" }, new[] { "10.0.0.5" }, options); + + Assert.NotNull(result); + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("END CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("BEGIN RSA PRIVATE KEY", result.PrivateKeyPem); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters rsa = Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + Assert.Equal(2048, rsa.Modulus.BitLength); + } + + [Theory] + [InlineData(InfisicalEcCurve.P256, "1.2.840.10045.3.1.7")] + [InlineData(InfisicalEcCurve.P384, "1.3.132.0.34")] + public void CsrBuilder_Ecdsa_Produces_Verifiable_Csr(InfisicalEcCurve curve, string expectedCurveOid) + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "ec.contoso.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Ecdsa, EcCurve = curve }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "ec.contoso.local" }, null, options); + + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.True(result.PrivateKeyPem.Contains("BEGIN EC PRIVATE KEY") || result.PrivateKeyPem.Contains("BEGIN PRIVATE KEY")); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Org.BouncyCastle.Crypto.Parameters.ECPublicKeyParameters ec = Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + Assert.Equal(expectedCurveOid, ec.PublicKeyParamSet.Id); + } + + [Fact] + public void CsrBuilder_Ed25519_Produces_Verifiable_Csr() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "ed.contoso.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Ed25519 }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "ed.contoso.local" }, null, options); + + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("BEGIN PRIVATE KEY", result.PrivateKeyPem); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + } + + [Fact] + public void CsrBuilder_Rsa_Rejects_Invalid_KeySize() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "test.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Rsa, RsaKeySize = 1024 }; + Assert.Throws(() => InfisicalCsrBuilder.Build(subject, null, null, options)); + } + + [Fact] + public void CsrBuilder_Throws_When_CommonName_Missing() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { Organization = "Contoso" }; + Assert.Throws(() => InfisicalCsrBuilder.Build(subject, null, null, new InfisicalCsrOptions())); + } + + private static Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest ReadCsr(string pem) + { + using (System.IO.StringReader reader = new System.IO.StringReader(pem)) + { + Org.BouncyCastle.OpenSsl.PemReader pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader); + object obj = pemReader.ReadObject(); + return Assert.IsType(obj); + } + } + + [Fact] + public void MergeSubject_Hashtable_Then_Individual_Params_Override() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo merge = helperType.GetMethod("MergeSubject", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(merge); + + Hashtable subject = new Hashtable { { "CN", "fallback.local" }, { "O", "FallbackOrg" }, { "C", "DE" } }; + object result = merge.Invoke(null, new object[] { subject, "explicit.local", null, null, null, "ExplicitOrg", null, null }); + + PropertyInfo commonNameProp = result.GetType().GetProperty("CommonName"); + PropertyInfo organizationProp = result.GetType().GetProperty("Organization"); + PropertyInfo countryProp = result.GetType().GetProperty("Country"); + + Assert.Equal("explicit.local", commonNameProp.GetValue(result)); + Assert.Equal("ExplicitOrg", organizationProp.GetValue(result)); + Assert.Equal("DE", countryProp.GetValue(result)); + } + + [Fact] + public void Candidates_For_SignCertificateBySubscriber_Include_Pki_And_CertManager() + { + IReadOnlyList candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.SignCertificateBySubscriber); + Assert.Contains(candidates, c => c.Template == "/api/v1/pki/pki-subscribers/{subscriberName}/sign-certificate"); + Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/pki-subscribers/{subscriberName}/sign-certificate"); + foreach (InfisicalEndpointDefinition candidate in candidates) + { + Assert.Equal("POST", candidate.Method); + Assert.True(candidate.RequiresAuthorization); + Assert.True(candidate.ContainsSecretMaterialInResponse); + } + } + + [Fact] + public void Candidates_For_SignCertificateByCa_Include_Pki_And_CertManager() + { + IReadOnlyList candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.SignCertificateByCa); + Assert.Contains(candidates, c => c.Template == "/api/v1/pki/ca/{caId}/sign-certificate"); + Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/ca/{caId}/sign-certificate"); + } + + [Fact] + public void RequestInfisicalCertificate_Cmdlet_Has_Both_Parameter_Sets() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(VerbsLifecycle.Request, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificate", cmdletData.ConstructorArguments[1].Value); + + string defaultParameterSetName = null; + foreach (CustomAttributeNamedArgument named in cmdletData.NamedArguments) + { + if (named.MemberName == "DefaultParameterSetName") { defaultParameterSetName = (string)named.TypedValue.Value; break; } + } + Assert.Equal("BySubscriber", defaultParameterSetName); + + Assert.NotNull(cmdletType.GetProperty("PkiSubscriberSlug")); + Assert.NotNull(cmdletType.GetProperty("CertificateAuthorityId")); + Assert.NotNull(cmdletType.GetProperty("Subject")); + Assert.NotNull(cmdletType.GetProperty("CommonName")); + Assert.NotNull(cmdletType.GetProperty("DnsName")); + Assert.NotNull(cmdletType.GetProperty("IpAddress")); + Assert.NotNull(cmdletType.GetProperty("Install")); + Assert.NotNull(cmdletType.GetProperty("StoreName")); + Assert.NotNull(cmdletType.GetProperty("StoreLocation")); + Assert.NotNull(cmdletType.GetProperty("AllowRenewal")); + Assert.NotNull(cmdletType.GetProperty("RenewalThresholdDays")); + Assert.NotNull(cmdletType.GetProperty("Force")); + Assert.NotNull(cmdletType.GetProperty("InstallChain")); + + PropertyInfo keyAlgorithmProp = cmdletType.GetProperty("KeyAlgorithm"); + PropertyInfo curveProp = cmdletType.GetProperty("Curve"); + Assert.NotNull(keyAlgorithmProp); + Assert.NotNull(curveProp); + Assert.Equal(typeof(InfisicalKeyAlgorithm), keyAlgorithmProp.PropertyType); + Assert.Equal(typeof(InfisicalEcCurve), curveProp.PropertyType); + + PropertyInfo protectionProp = cmdletType.GetProperty("PrivateKeyProtection"); + Assert.NotNull(protectionProp); + Assert.Equal(typeof(InfisicalPrivateKeyProtection), protectionProp.PropertyType); + Assert.NotNull(cmdletType.GetProperty("PersistKey")); + Assert.NotNull(cmdletType.GetProperty("MachineKey")); + Assert.NotNull(cmdletType.GetProperty("PrivateKeyPath")); + Assert.NotNull(cmdletType.GetProperty("LocalChainOnly")); + + CustomAttributeData outputTypeData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(OutputTypeAttribute)) { outputTypeData = candidate; break; } + } + Assert.NotNull(outputTypeData); + IList outputTypeArgs = (IList)outputTypeData.ConstructorArguments[0].Value; + Assert.Contains(outputTypeArgs, a => (Type)a.Value == typeof(PSInfisicalAPI.Models.InfisicalCertificateResult)); + } + + [Fact] + public void BuildResult_Splits_Chain_Into_Leaf_Intermediates_And_Root() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Leaf"); + (string intermediatePem, _, string intermediateThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Intermediate"); + (string rootPem, _, string rootThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Root"); + + PSInfisicalAPI.Models.InfisicalSignedCertificate signed = new PSInfisicalAPI.Models.InfisicalSignedCertificate + { + SerialNumber = "ABC123", + CertificatePem = leafPem, + CertificateChainPem = intermediatePem + rootPem, + IssuingCaCertificatePem = rootPem + }; + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo buildResult = helperType.GetMethod("BuildResult", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(buildResult); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)buildResult.Invoke(null, new object[] { leaf, signed }); + + Assert.Same(leaf, result.Leaf); + Assert.Equal("ABC123", result.SerialNumber); + Assert.Empty(result.Intermediates); + Assert.NotNull(result.Root); + Assert.Equal(2, result.Chain.Length); + Assert.Same(leaf, result.Chain[0]); + } + } + + [Theory] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable)] + [InlineData(InfisicalPrivateKeyProtection.NonExportable, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, true, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, true, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.MachineKeySet)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, true, true, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable | System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.MachineKeySet | System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet)] + public void ResolveKeyStorageFlags_Maps_Protection_And_Switches(InfisicalPrivateKeyProtection protection, bool persist, bool machine, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags expected) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("ResolveKeyStorageFlags", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + System.Security.Cryptography.X509Certificates.X509KeyStorageFlags actual = (System.Security.Cryptography.X509Certificates.X509KeyStorageFlags)method.Invoke(null, new object[] { protection, persist, machine }); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, false)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, false, false)] + [InlineData(InfisicalPrivateKeyProtection.NonExportable, false, true)] + [InlineData(InfisicalPrivateKeyProtection.Ephemeral, false, true)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, true, true)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, true, true)] + public void ShouldScrubPrivateKeyPem_Returns_Expected(InfisicalPrivateKeyProtection protection, bool hasPath, bool expected) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("ShouldScrubPrivateKeyPem", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + bool actual = (bool)method.Invoke(null, new object[] { protection, hasPath }); + Assert.Equal(expected, actual); + } + + [Fact] + public void WritePrivateKeyPem_Writes_File_And_Creates_Directory() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("WritePrivateKeyPem", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + string tempRoot = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PSInfisicalAPI_PemWrite_" + Guid.NewGuid().ToString("N")); + string nested = System.IO.Path.Combine(tempRoot, "nested", "key.pem"); + const string pem = "-----BEGIN PRIVATE KEY-----\nMIIBVgIBADANBgkqhkiG9w0BAQEFAA==\n-----END PRIVATE KEY-----\n"; + try + { + method.Invoke(null, new object[] { pem, nested }); + Assert.True(System.IO.File.Exists(nested)); + Assert.Equal(pem, System.IO.File.ReadAllText(nested)); + } + finally + { + if (System.IO.Directory.Exists(tempRoot)) { System.IO.Directory.Delete(tempRoot, true); } + } + } + + [Fact] + public void BuildResultFromExistingLocal_Populates_Leaf_And_Pem_For_Selfsigned() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Leaf"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo build = helperType.GetMethod("BuildResultFromExistingLocal", BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2) }, null); + Assert.NotNull(build); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)build.Invoke(null, new object[] { leaf }); + + Assert.Same(leaf, result.Leaf); + Assert.Equal(leaf.SerialNumber, result.SerialNumber); + Assert.Contains("BEGIN CERTIFICATE", result.CertificatePem); + Assert.NotNull(result.Chain); + Assert.NotEmpty(result.Chain); + Assert.Same(leaf, result.Chain[0]); + Assert.Empty(result.Intermediates); + } + } + + [Fact] + public void BuildResultFromExistingLocal_Has_Bundle_Fallback_Overload() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + Assert.NotNull(overload); + Assert.Equal(typeof(PSInfisicalAPI.Models.InfisicalCertificateResult), overload.ReturnType); + } + + [Fact] + public void BuildResultFromExistingLocal_With_Null_Bundle_Matches_LocalOnly_Behavior() + { + (string leafPem, _, _) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Null.Leaf"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)overload.Invoke(null, new object[] { leaf, null }); + + Assert.Same(leaf, result.Leaf); + Assert.Empty(result.Intermediates); + Assert.Single(result.Chain); + } + } + + [Fact] + public void BuildResultFromExistingLocal_With_Bundle_Merges_Chain_From_Bundle() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Leaf"); + (string caPem, _, string caThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Ca"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + PSInfisicalAPI.Models.InfisicalCertificateBundle bundle = new PSInfisicalAPI.Models.InfisicalCertificateBundle + { + SerialNumber = leaf.SerialNumber, + CertificatePem = leafPem, + CertificateChainPem = caPem + }; + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)overload.Invoke(null, new object[] { leaf, bundle }); + + Assert.Same(leaf, result.Leaf); + Assert.NotNull(result.Root); + Assert.Equal(caThumb, result.Root.Thumbprint); + Assert.Equal(2, result.Chain.Length); + Assert.Same(leaf, result.Chain[0]); + Assert.Equal(caThumb, result.Chain[1].Thumbprint); + + Assert.NotNull(result.CertificateChainPem); + Assert.Contains("BEGIN CERTIFICATE", result.CertificateChainPem); + } + } + + [Fact] + public void GetChainCertificateTargetStore_SelfSigned_Returns_Root() + { + using (System.Security.Cryptography.RSA rsa = System.Security.Cryptography.RSA.Create(2048)) + { + System.Security.Cryptography.X509Certificates.CertificateRequest request = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.SelfSigned", + rsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + using (System.Security.Cryptography.X509Certificates.X509Certificate2 selfSigned = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(classify); + + object result = classify.Invoke(null, new object[] { selfSigned }); + Assert.Equal(System.Security.Cryptography.X509Certificates.StoreName.Root, result); + } + } + } + + [Fact] + public void GetChainCertificateTargetStore_NonSelfSigned_Returns_CertificateAuthority() + { + using (System.Security.Cryptography.RSA rootRsa = System.Security.Cryptography.RSA.Create(2048)) + using (System.Security.Cryptography.RSA intermediateRsa = System.Security.Cryptography.RSA.Create(2048)) + { + System.Security.Cryptography.X509Certificates.CertificateRequest rootRequest = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.Root", + rootRsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + rootRequest.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true)); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 rootCert = rootRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1))) + { + System.Security.Cryptography.X509Certificates.CertificateRequest intermediateRequest = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.Intermediate", + intermediateRsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + intermediateRequest.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true)); + + byte[] serial = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + using (System.Security.Cryptography.X509Certificates.X509Certificate2 intermediate = intermediateRequest.Create(rootCert, DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1), serial)) + { + Assert.NotEqual(intermediate.Subject, intermediate.Issuer); + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + + object result = classify.Invoke(null, new object[] { intermediate }); + Assert.Equal(System.Security.Cryptography.X509Certificates.StoreName.CertificateAuthority, result); + } + } + } + } + + [Fact] + public void InstallChain_Has_X509Collection_Overload() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + Type loggerType = ModuleAssembly.GetType("PSInfisicalAPI.Logging.IInfisicalLogger", true); + + MethodInfo overload = helperType.GetMethod( + "InstallChain", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] + { + typeof(System.Collections.Generic.IEnumerable), + typeof(System.Security.Cryptography.X509Certificates.StoreLocation), + typeof(bool), + loggerType, + typeof(string) + }, + null); + + Assert.NotNull(overload); + Assert.Equal(typeof(void), overload.ReturnType); + } + + [Fact] + public void InstallInfisicalCertificateCmdlet_Uses_ChainRouting_Helper() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.InstallInfisicalCertificateCmdlet", true); + Assert.NotNull(cmdletType); + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(classify); + } + } +} diff --git a/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs b/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs index b147725..17d3d01 100644 --- a/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs +++ b/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs @@ -8,6 +8,11 @@ namespace PSInfisicalAPI.Tests { public class PemCertificateBuilderTests { + public static (string CertPem, string KeyPem, string Thumbprint) CreateSelfSignedExposed(string commonName) + { + return CreateSelfSigned(commonName); + } + private static (string CertPem, string KeyPem, string Thumbprint) CreateSelfSigned(string commonName) { using (RSA rsa = RSA.Create(2048)) diff --git a/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs b/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs new file mode 100644 index 0000000..8ccbf5d --- /dev/null +++ b/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections; +using System.Reflection; +using PSInfisicalAPI.Http; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Pki; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class PkiClientParseTests + { + private sealed class NoopHttpClient : IInfisicalHttpClient + { + public InfisicalHttpResponse Send(InfisicalHttpRequest request) { throw new NotImplementedException(); } + } + + private static InfisicalPkiClient CreateClient() + { + return new InfisicalPkiClient(new NoopHttpClient(), NullInfisicalLogger.Instance); + } + + private static object InvokeNonPublic(InfisicalPkiClient client, string methodName, string body) + { + MethodInfo method = typeof(InfisicalPkiClient).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); + return method.Invoke(client, new object[] { body }); + } + + [Fact] + public void ParseCaListBody_Reads_Raw_Json_Array() + { + string body = "[{\"id\":\"ca-1\",\"name\":\"intermediate\",\"projectId\":\"p1\",\"configuration\":{\"commonName\":\"Intermediate CA\",\"keyAlgorithm\":\"RSA_2048\"}}]"; + object result = InvokeNonPublic(CreateClient(), "ParseCaListBody", body); + IList list = (IList)result; + Assert.Single(list); + object dto = list[0]; + Assert.Equal("ca-1", dto.GetType().GetProperty("Id").GetValue(dto)); + object cfg = dto.GetType().GetProperty("Configuration").GetValue(dto); + Assert.NotNull(cfg); + Assert.Equal("Intermediate CA", cfg.GetType().GetProperty("CommonName").GetValue(cfg)); + } + + [Fact] + public void ParseCaListBody_Reads_CertificateAuthorities_Wrapper() + { + string body = "{\"certificateAuthorities\":[{\"id\":\"ca-2\",\"name\":\"root\"}]}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaListBody", body); + IList list = (IList)result; + Assert.Single(list); + object dto = list[0]; + Assert.Equal("ca-2", dto.GetType().GetProperty("Id").GetValue(dto)); + } + + [Fact] + public void ParseCaSingleBody_Reads_Raw_Object_With_Configuration() + { + string body = "{\"id\":\"ca-9\",\"name\":\"intermediate-ca\",\"status\":\"active\",\"configuration\":{\"commonName\":\"GSPA Intermediate\",\"organization\":\"GSPA\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaSingleBody", body); + Assert.NotNull(result); + Assert.Equal("ca-9", result.GetType().GetProperty("Id").GetValue(result)); + object cfg = result.GetType().GetProperty("Configuration").GetValue(result); + Assert.NotNull(cfg); + Assert.Equal("GSPA Intermediate", cfg.GetType().GetProperty("CommonName").GetValue(cfg)); + Assert.Equal("GSPA", cfg.GetType().GetProperty("OrganizationName").GetValue(cfg)); + } + + [Fact] + public void ParseCaSingleBody_Reads_CertificateAuthority_Wrapper() + { + string body = "{\"certificateAuthority\":{\"id\":\"ca-7\",\"name\":\"root\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaSingleBody", body); + Assert.NotNull(result); + Assert.Equal("ca-7", result.GetType().GetProperty("Id").GetValue(result)); + } + + [Fact] + public void ParseCertificateSingleBody_Reads_Certificate_Wrapper() + { + string body = "{\"certificate\":{\"id\":\"cert-1\",\"serialNumber\":\"ABCD\",\"commonName\":\"host.example\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCertificateSingleBody", body); + Assert.NotNull(result); + Assert.Equal("cert-1", result.GetType().GetProperty("Id").GetValue(result)); + Assert.Equal("ABCD", result.GetType().GetProperty("SerialNumber").GetValue(result)); + } + } +} diff --git a/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs b/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs index 70c3ca2..9ecd5c9 100644 --- a/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs +++ b/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; using PSInfisicalAPI.Endpoints; using Xunit; @@ -6,6 +9,64 @@ namespace PSInfisicalAPI.Tests { public class PkiEndpointRegistryTests { + private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly; + + [Fact] + public void GetInfisicalCertificate_Cmdlet_Is_Singular_With_Mandatory_SerialNumber() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(2, cmdletData.ConstructorArguments.Count); + Assert.Equal(VerbsCommon.Get, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificate", cmdletData.ConstructorArguments[1].Value); + + PropertyInfo serialProp = cmdletType.GetProperty("SerialNumber"); + Assert.NotNull(serialProp); + + CustomAttributeData parameterAttr = null; + foreach (CustomAttributeData candidate in serialProp.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(ParameterAttribute)) { parameterAttr = candidate; break; } + } + Assert.NotNull(parameterAttr); + + bool mandatory = false; + foreach (CustomAttributeNamedArgument named in parameterAttr.NamedArguments) + { + if (named.MemberName == "Mandatory") { mandatory = (bool)named.TypedValue.Value; break; } + } + Assert.True(mandatory); + } + + [Fact] + public void GetInfisicalCertificates_Cmdlet_Is_Registered_For_Listing() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificatesCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(VerbsCommon.Get, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificates", cmdletData.ConstructorArguments[1].Value); + + Assert.NotNull(cmdletType.GetProperty("CommonName")); + Assert.NotNull(cmdletType.GetProperty("FriendlyName")); + Assert.NotNull(cmdletType.GetProperty("CaId")); + Assert.NotNull(cmdletType.GetProperty("Limit")); + Assert.NotNull(cmdletType.GetProperty("Offset")); + } + [Fact] public void Get_ListInternalCertificateAuthorities_Returns_CertManager_Primary() { diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs index d80992e..bd90777 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs @@ -21,12 +21,11 @@ namespace PSInfisicalAPI.Cmdlets 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); + InfisicalCertificateAuthority ca = client.GetInternalCertificateAuthority(connection, CaId, ProjectId); if (ca != null) { WriteObject(ca); @@ -35,7 +34,7 @@ namespace PSInfisicalAPI.Cmdlets return; } - InfisicalCertificateAuthority[] all = client.ListInternalCertificateAuthorities(connection, resolvedProjectId); + InfisicalCertificateAuthority[] all = client.ListInternalCertificateAuthorities(connection, ProjectId); foreach (InfisicalCertificateAuthority ca in all) { WriteObject(ca); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs new file mode 100644 index 0000000..df5970c --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs @@ -0,0 +1,36 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificate")] + [OutputType(typeof(InfisicalCertificate))] + public sealed class GetInfisicalCertificateCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id", "Identifier")] + public string SerialNumber { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + InfisicalCertificate cert = client.RetrieveCertificate(connection, SerialNumber); + if (cert != null) + { + WriteObject(cert); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificateCmdlet", "GetCertificate", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatesCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatesCmdlet.cs new file mode 100644 index 0000000..7c90278 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatesCmdlet.cs @@ -0,0 +1,76 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificates")] + [OutputType(typeof(InfisicalCertificate))] + public sealed class GetInfisicalCertificatesCmdlet : InfisicalCmdletBase + { + [Parameter] public string ProjectId { get; set; } + [Parameter] public string CommonName { get; set; } + [Parameter] public string FriendlyName { get; set; } + [Parameter] public string Status { get; set; } + [Parameter] public string[] CaId { 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(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + string resolvedProjectId = ResolveProjectId(connection, ProjectId); + + InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery + { + ProjectId = resolvedProjectId, + CommonName = CommonName, + FriendlyName = FriendlyName, + Status = Status, + CaIds = CaId, + Limit = Limit ?? 100, + Offset = Offset ?? 0 + }; + + int requestedLimit = query.Limit ?? 100; + 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("GetInfisicalCertificatesCmdlet", "GetCertificates", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs index af32a3e..03b635a 100644 --- a/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs @@ -44,7 +44,8 @@ namespace PSInfisicalAPI.Cmdlets { foreach (X509Certificate2 chainCert in ResolveChain()) { - InstallCertificate(chainCert, StoreName.CertificateAuthority, StoreLocation); + StoreName chainStore = InfisicalCertificateRequestHelpers.GetChainCertificateTargetStore(chainCert); + InstallCertificate(chainCert, chainStore, StoreLocation); } } diff --git a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs new file mode 100644 index 0000000..1fc3b09 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsLifecycle.Request, "InfisicalCertificate", SupportsShouldProcess = true, DefaultParameterSetName = "BySubscriber")] + [OutputType(typeof(InfisicalCertificateResult))] + public sealed class RequestInfisicalCertificateCmdlet : InfisicalCmdletBase + { + private const string Component = "RequestInfisicalCertificateCmdlet"; + + [Parameter(ParameterSetName = "BySubscriber", Mandatory = true, Position = 0)] + [Alias("Subscriber")] + public string PkiSubscriberSlug { get; set; } + + [Parameter(ParameterSetName = "ByCa", Mandatory = true, Position = 0)] + [Alias("CaId")] + public string CertificateAuthorityId { get; set; } + + [Parameter] public string ProjectId { get; set; } + [Parameter] public IDictionary Subject { get; set; } + [Parameter] public string CommonName { get; set; } + [Parameter] public string Country { get; set; } + [Parameter] public string State { get; set; } + [Parameter] public string Locality { get; set; } + [Parameter] public string Organization { get; set; } + [Parameter] public string OrganizationalUnit { get; set; } + [Parameter] public string EmailAddress { get; set; } + [Parameter] public string[] DnsName { get; set; } + [Parameter] public string[] IpAddress { get; set; } + [Parameter] public InfisicalKeyAlgorithm KeyAlgorithm { get; set; } = InfisicalKeyAlgorithm.Rsa; + [Parameter] public int KeySize { get; set; } = 2048; + [Parameter] public InfisicalEcCurve Curve { get; set; } = InfisicalEcCurve.P256; + + [Parameter(ParameterSetName = "ByCa")] public string Ttl { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string NotBefore { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string NotAfter { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string FriendlyName { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string PkiCollectionId { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string[] KeyUsage { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string[] ExtendedKeyUsage { get; set; } + + [Parameter] public SwitchParameter Install { 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 InstallChain { get; set; } + + [Parameter] public InfisicalPrivateKeyProtection PrivateKeyProtection { get; set; } = InfisicalPrivateKeyProtection.LocalOnly; + [Parameter] public SwitchParameter PersistKey { get; set; } + [Parameter] public SwitchParameter MachineKey { get; set; } + [Parameter] public string PrivateKeyPath { get; set; } + + [Parameter] public SwitchParameter AllowRenewal { get; set; } + [Parameter] public int RenewalThresholdDays { get; set; } = 30; + [Parameter] public SwitchParameter Force { get; set; } + [Parameter] public SwitchParameter LocalChainOnly { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + string resolvedProjectId = ResolveProjectId(connection, ProjectId); + + InfisicalCsrSubject csrSubject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress); + List dnsNames = BuildDnsNames(csrSubject); + if (string.IsNullOrEmpty(csrSubject.CommonName) && dnsNames.Count > 0) { csrSubject.CommonName = dnsNames[0]; } + if (string.IsNullOrEmpty(csrSubject.CommonName)) { throw new InvalidOperationException("Subject CommonName could not be determined and no DnsName was provided."); } + + X509Certificate2 existing = TryFindExisting(client, connection, resolvedProjectId, csrSubject.CommonName); + if (existing != null && !Force.IsPresent && !(AllowRenewal.IsPresent && InfisicalLocalCertificateLookup.IsRenewable(existing, RenewalThresholdDays))) + { + Logger.Information(Component, string.Concat("Reusing existing certificate (Thumbprint=", existing.Thumbprint, ", NotAfter=", existing.NotAfter.ToString("u"), ").")); + InfisicalCertificateResult reuseResult = InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal(existing); + + if (!LocalChainOnly.IsPresent + && (reuseResult.Root == null || reuseResult.Intermediates == null || reuseResult.Intermediates.Length == 0) + && !string.IsNullOrEmpty(existing.SerialNumber)) + { + try + { + InfisicalCertificateBundle bundle = client.GetCertificateBundle(connection, existing.SerialNumber); + if (bundle != null && !string.IsNullOrEmpty(bundle.CertificateChainPem)) + { + reuseResult = InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal(existing, bundle); + Logger.Information(Component, "Reused certificate chain completed from Infisical bundle."); + } + } + catch (Exception bundleException) + { + Logger.Verbose(Component, string.Concat("Infisical bundle fetch for reuse path failed (continuing with local-only chain): ", bundleException.Message)); + } + } + + WriteObject(reuseResult); + return; + } + + string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "' or CA '", CertificateAuthorityId ?? "(n/a)", "' for CN=", csrSubject.CommonName); + if (!ShouldProcess(target, "Request new certificate")) { return; } + + InfisicalCsrOptions csrOptions = new InfisicalCsrOptions { KeyAlgorithm = KeyAlgorithm, RsaKeySize = KeySize, EcCurve = Curve }; + InfisicalCsrResult csr = InfisicalCsrBuilder.Build(csrSubject, dnsNames, IpAddress, csrOptions); + InfisicalSignedCertificate signed = SignCertificate(client, connection, resolvedProjectId, csr.CsrPem); + signed.PrivateKeyPem = csr.PrivateKeyPem; + + X509KeyStorageFlags resolvedFlags = ResolveEffectiveKeyStorageFlags(); + X509Certificate2 cert = PemCertificateBuilder.Build(signed.CertificatePem, signed.PrivateKeyPem, signed.CertificateChainPem, resolvedFlags); + + if (Install.IsPresent) + { + InfisicalCertificateRequestHelpers.InstallToStore(cert, StoreName, StoreLocation, Force.IsPresent, Logger, Component); + if (InstallChain.IsPresent) + { + InfisicalCertificateRequestHelpers.InstallChain(signed, StoreLocation, Force.IsPresent, Logger, Component); + } + } + + InfisicalCertificateResult resultObj = InfisicalCertificateRequestHelpers.BuildResult(cert, signed); + + bool hasExplicitPath = !string.IsNullOrEmpty(PrivateKeyPath); + if (hasExplicitPath && !string.IsNullOrEmpty(resultObj.PrivateKeyPem)) + { + InfisicalCertificateRequestHelpers.WritePrivateKeyPem(resultObj.PrivateKeyPem, PrivateKeyPath); + Logger.Information(Component, string.Concat("Wrote private key PEM to '", PrivateKeyPath, "'.")); + } + + if (!MyInvocation.BoundParameters.ContainsKey("KeyStorageFlags") + && InfisicalCertificateRequestHelpers.ShouldScrubPrivateKeyPem(PrivateKeyProtection, hasExplicitPath)) + { + resultObj.PrivateKeyPem = null; + } + + WriteObject(resultObj); + } + catch (Exception exception) + { + ThrowTerminatingForException(Component, "RequestCertificate", exception); + } + } + + private List BuildDnsNames(InfisicalCsrSubject subject) + { + List result = new List(); + if (DnsName != null) { foreach (string dns in DnsName) { if (!string.IsNullOrEmpty(dns)) { result.Add(dns); } } } + if (result.Count == 0) + { + string fqdn = InfisicalCertificateRequestHelpers.ResolveLocalFqdn(); + if (!string.IsNullOrEmpty(fqdn)) { result.Add(fqdn); } + } + + if (!string.IsNullOrEmpty(subject.CommonName) && !result.Contains(subject.CommonName)) { result.Insert(0, subject.CommonName); } + return result; + } + + private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName) + { + List candidateSerials = new List(); + try + { + InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery { ProjectId = projectId, CommonName = commonName, Status = "active", Limit = 50 }; + InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query); + if (page != null && page.Certificates != null) + { + foreach (InfisicalCertificate hit in page.Certificates) { if (!string.IsNullOrEmpty(hit.SerialNumber)) { candidateSerials.Add(hit.SerialNumber); } } + } + } + catch (Exception searchException) + { + Logger.Verbose(Component, string.Concat("Infisical search for idempotency check failed: ", searchException.Message)); + } + + return InfisicalLocalCertificateLookup.FindMatch(StoreName, StoreLocation, commonName, candidateSerials); + } + + private X509KeyStorageFlags ResolveEffectiveKeyStorageFlags() + { + if (MyInvocation.BoundParameters.ContainsKey("KeyStorageFlags")) + { + return KeyStorageFlags; + } + + return InfisicalCertificateRequestHelpers.ResolveKeyStorageFlags(PrivateKeyProtection, PersistKey.IsPresent, MachineKey.IsPresent); + } + + private InfisicalSignedCertificate SignCertificate(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string csrPem) + { + if (string.Equals(ParameterSetName, "BySubscriber", StringComparison.Ordinal)) + { + return client.SignCertificateBySubscriber(connection, PkiSubscriberSlug, projectId, csrPem); + } + + return client.SignCertificateByCa(connection, CertificateAuthorityId, csrPem, CommonName, null, Ttl, NotBefore, NotAfter, FriendlyName, PkiCollectionId, KeyUsage, ExtendedKeyUsage); + } + } +} diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs index 6d01d4a..22f08bf 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs @@ -49,5 +49,7 @@ namespace PSInfisicalAPI.Endpoints public const string SearchCertificates = "SearchCertificates"; public const string RetrieveCertificate = "RetrieveCertificate"; public const string GetCertificateBundle = "GetCertificateBundle"; + public const string SignCertificateBySubscriber = "SignCertificateBySubscriber"; + public const string SignCertificateByCa = "SignCertificateByCa"; } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs index 9d1e305..2bd0f58 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs @@ -589,6 +589,50 @@ namespace PSInfisicalAPI.Endpoints RequiresAuthorization = true, ContainsSecretMaterialInResponse = true }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateBySubscriber, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/pki/pki-subscribers/{subscriberName}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateBySubscriber, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/cert-manager/pki-subscribers/{subscriberName}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateByCa, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/pki/ca/{caId}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateByCa, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/cert-manager/ca/{caId}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); } public static InfisicalEndpointDefinition Get(string name) diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs new file mode 100644 index 0000000..38dd62a --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateResult + { + public X509Certificate2 Leaf { get; set; } + public X509Certificate2[] Intermediates { get; set; } + public X509Certificate2 Root { get; set; } + public X509Certificate2[] Chain { get; set; } + public string SerialNumber { get; set; } + public string CertificatePem { get; set; } + public string CertificateChainPem { get; set; } + public string PrivateKeyPem { get; set; } + + public override string ToString() + { + if (Leaf != null) { return Leaf.Subject; } + return SerialNumber; + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs new file mode 100644 index 0000000..e40d62f --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs @@ -0,0 +1,16 @@ +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalSignedCertificate + { + public string SerialNumber { get; set; } + public string CertificatePem { get; set; } + public string CertificateChainPem { get; set; } + public string IssuingCaCertificatePem { get; set; } + public string PrivateKeyPem { get; set; } + + public override string ToString() + { + return SerialNumber; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs index 01c45fc..553dd5e 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs @@ -3,6 +3,26 @@ using Newtonsoft.Json; namespace PSInfisicalAPI.Pki { + internal sealed class InfisicalInternalCaConfigurationDto + { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("friendlyName")] public string FriendlyName { get; set; } + [JsonProperty("commonName")] public string CommonName { 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("notBefore")] public string NotBefore { get; set; } + [JsonProperty("notAfter")] public string NotAfter { get; set; } + [JsonProperty("maxPathLength")] public int? MaxPathLength { get; set; } + [JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; } + [JsonProperty("dn")] public string DistinguishedName { get; set; } + [JsonProperty("parentCaId")] public string ParentCaId { get; set; } + [JsonProperty("serialNumber")] public string SerialNumber { get; set; } + [JsonProperty("activeCaCertId")] public string ActiveCaCertId { get; set; } + } + internal sealed class InfisicalInternalCaResponseDto { [JsonProperty("id")] public string Id { get; set; } @@ -28,6 +48,7 @@ namespace PSInfisicalAPI.Pki [JsonProperty("activeCaCertId")] public string ActiveCaCertId { get; set; } [JsonProperty("createdAt")] public string CreatedAt { get; set; } [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + [JsonProperty("configuration")] public InfisicalInternalCaConfigurationDto Configuration { get; set; } } internal sealed class InfisicalInternalCaListResponseDto diff --git a/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs index dc83822..aa1c3dd 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs @@ -14,34 +14,41 @@ namespace PSInfisicalAPI.Pki return null; } + InfisicalInternalCaConfigurationDto cfg = dto.Configuration; + return new InfisicalCertificateAuthority { Id = dto.Id, ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, Name = dto.Name, - FriendlyName = dto.FriendlyName, - Type = dto.Type, + FriendlyName = Coalesce(cfg != null ? cfg.FriendlyName : null, dto.FriendlyName), + Type = Coalesce(dto.Type, cfg != null ? cfg.Type : null), 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, + KeyAlgorithm = Coalesce(cfg != null ? cfg.KeyAlgorithm : null, dto.KeyAlgorithm), + DistinguishedName = Coalesce(cfg != null ? cfg.DistinguishedName : null, dto.DistinguishedName), + OrganizationName = Coalesce(cfg != null ? cfg.OrganizationName : null, dto.OrganizationName), + OrganizationUnit = Coalesce(cfg != null ? cfg.OrganizationUnit : null, dto.OrganizationUnit), + Country = Coalesce(cfg != null ? cfg.Country : null, dto.Country), + State = Coalesce(cfg != null ? cfg.State : null, dto.State), + Locality = Coalesce(cfg != null ? cfg.Locality : null, dto.Locality), + CommonName = Coalesce(cfg != null ? cfg.CommonName : null, dto.CommonName), + MaxPathLength = (cfg != null && cfg.MaxPathLength.HasValue) ? cfg.MaxPathLength : dto.MaxPathLength, + NotBefore = Coalesce(cfg != null ? cfg.NotBefore : null, dto.NotBefore), + NotAfter = Coalesce(cfg != null ? cfg.NotAfter : null, dto.NotAfter), + SerialNumber = Coalesce(cfg != null ? cfg.SerialNumber : null, dto.SerialNumber), + ParentCaId = Coalesce(cfg != null ? cfg.ParentCaId : null, dto.ParentCaId), + ActiveCaCertId = Coalesce(cfg != null ? cfg.ActiveCaCertId : null, dto.ActiveCaCertId), CreatedAtUtc = ParseTimestamp(dto.CreatedAt), UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) }; } + private static string Coalesce(string primary, string fallback) + { + return !string.IsNullOrEmpty(primary) ? primary : fallback; + } + public static InfisicalCertificateAuthority[] MapMany(IEnumerable items, string fallbackProjectId) { if (items == null) diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs new file mode 100644 index 0000000..15c296b --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificateRequestHelpers + { + public static InfisicalCsrSubject MergeSubject(IDictionary subject, string commonName, string country, string state, string locality, string organization, string organizationalUnit, string emailAddress) + { + InfisicalCsrSubject result = new InfisicalCsrSubject(); + if (subject != null) + { + result.CommonName = ReadString(subject, "CN", "CommonName"); + result.Country = ReadString(subject, "C", "Country"); + result.State = ReadString(subject, "ST", "S", "State"); + result.Locality = ReadString(subject, "L", "Locality"); + result.Organization = ReadString(subject, "O", "Organization"); + result.OrganizationalUnit = ReadString(subject, "OU", "OrganizationalUnit"); + result.EmailAddress = ReadString(subject, "E", "EMAIL", "EmailAddress"); + } + + if (!string.IsNullOrEmpty(commonName)) { result.CommonName = commonName; } + if (!string.IsNullOrEmpty(country)) { result.Country = country; } + if (!string.IsNullOrEmpty(state)) { result.State = state; } + if (!string.IsNullOrEmpty(locality)) { result.Locality = locality; } + if (!string.IsNullOrEmpty(organization)) { result.Organization = organization; } + if (!string.IsNullOrEmpty(organizationalUnit)) { result.OrganizationalUnit = organizationalUnit; } + if (!string.IsNullOrEmpty(emailAddress)) { result.EmailAddress = emailAddress; } + + return result; + } + + public static string ResolveLocalFqdn() + { + try + { + string host = System.Net.Dns.GetHostName(); + string domain = null; + try { domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; } + catch { domain = null; } + + if (!string.IsNullOrEmpty(domain) && !host.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase)) + { + return string.Concat(host, ".", domain); + } + + return host; + } + catch + { + return null; + } + } + + public static void InstallToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + X509Store store = new X509Store(storeName, storeLocation); + try + { + store.Open(OpenFlags.ReadWrite); + X509Certificate2Collection existing = store.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, false); + string target = string.Concat(storeLocation.ToString(), @"\", storeName.ToString(), " [", cert.Thumbprint, "]"); + if (existing.Count > 0) + { + if (!force) + { + logger.Information(component, string.Concat("Certificate already present in ", target, "; no action taken.")); + return; + } + + store.RemoveRange(existing); + } + + store.Add(cert); + logger.Information(component, string.Concat("Installed certificate to ", target, ".")); + } + finally + { + store.Close(); + } + } + + public static void InstallChain(InfisicalSignedCertificate signed, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + List chainCerts = CollectChainCertificates(signed); + InstallChain(chainCerts, storeLocation, force, logger, component); + } + + public static void InstallChain(IEnumerable chainCerts, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + if (chainCerts == null) { return; } + foreach (X509Certificate2 chainCert in chainCerts) + { + if (chainCert == null) { continue; } + StoreName targetStore = GetChainCertificateTargetStore(chainCert); + InstallToStore(chainCert, targetStore, storeLocation, force, logger, component); + } + } + + public static StoreName GetChainCertificateTargetStore(X509Certificate2 cert) + { + return IsSelfSigned(cert) ? StoreName.Root : StoreName.CertificateAuthority; + } + + public static X509KeyStorageFlags ResolveKeyStorageFlags(InfisicalPrivateKeyProtection protection, bool persistKey, bool machineKey) + { + X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet; + switch (protection) + { + case InfisicalPrivateKeyProtection.Exportable: + flags |= X509KeyStorageFlags.Exportable; + break; + case InfisicalPrivateKeyProtection.Ephemeral: + const int ephemeralValue = 32; + if (Enum.GetName(typeof(X509KeyStorageFlags), ephemeralValue) == null) + { + throw new PlatformNotSupportedException("InfisicalPrivateKeyProtection.Ephemeral requires .NET Core 3.0 or later (PowerShell 7+). Use LocalOnly or NonExportable on Windows PowerShell 5.1."); + } + flags |= (X509KeyStorageFlags)ephemeralValue; + break; + } + + if (machineKey) { flags |= X509KeyStorageFlags.MachineKeySet; } + if (persistKey) { flags |= X509KeyStorageFlags.PersistKeySet; } + + return flags; + } + + public static bool ShouldScrubPrivateKeyPem(InfisicalPrivateKeyProtection protection, bool hasExplicitPrivateKeyPath) + { + if (hasExplicitPrivateKeyPath) { return true; } + return protection == InfisicalPrivateKeyProtection.NonExportable + || protection == InfisicalPrivateKeyProtection.Ephemeral; + } + + public static void WritePrivateKeyPem(string privateKeyPem, string path) + { + if (string.IsNullOrEmpty(privateKeyPem)) { throw new ArgumentException("PrivateKeyPem is empty.", nameof(privateKeyPem)); } + if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path is required.", nameof(path)); } + + string fullPath = System.IO.Path.GetFullPath(path); + string directory = System.IO.Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(directory) && !System.IO.Directory.Exists(directory)) + { + System.IO.Directory.CreateDirectory(directory); + } + + System.IO.File.WriteAllText(fullPath, privateKeyPem); + } + + public static InfisicalCertificateResult BuildResultFromExistingLocal(X509Certificate2 leaf) + { + return BuildResultFromExistingLocal(leaf, null); + } + + public static InfisicalCertificateResult BuildResultFromExistingLocal(X509Certificate2 leaf, InfisicalCertificateBundle fallbackBundle) + { + if (leaf == null) { throw new ArgumentNullException(nameof(leaf)); } + + InfisicalCertificateResult result = new InfisicalCertificateResult + { + Leaf = leaf, + SerialNumber = leaf.SerialNumber, + CertificatePem = ExportCertificateToPem(leaf) + }; + + List chainElements = BuildLocalChain(leaf); + + if (fallbackBundle != null && !string.IsNullOrEmpty(fallbackBundle.CertificateChainPem)) + { + List bundleChain = PemCertificateBuilder.ReadCertificateChain(fallbackBundle.CertificateChainPem); + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (X509Certificate2 c in chainElements) { if (c != null) { seen.Add(c.Thumbprint); } } + foreach (X509Certificate2 c in bundleChain) + { + if (c == null) { continue; } + if (seen.Add(c.Thumbprint)) { chainElements.Add(c); } + } + } + + List intermediates = new List(); + X509Certificate2 root = null; + foreach (X509Certificate2 cert in chainElements) + { + if (string.Equals(cert.Thumbprint, leaf.Thumbprint, StringComparison.OrdinalIgnoreCase)) { continue; } + if (IsSelfSigned(cert)) { if (root == null) { root = cert; } } + else { intermediates.Add(cert); } + } + + result.Intermediates = intermediates.ToArray(); + result.Root = root; + + List ordered = new List { leaf }; + ordered.AddRange(intermediates); + if (root != null) { ordered.Add(root); } + result.Chain = ordered.ToArray(); + + if (intermediates.Count > 0 || root != null) + { + StringBuilder sb = new StringBuilder(); + foreach (X509Certificate2 c in intermediates) { sb.Append(ExportCertificateToPem(c)); } + if (root != null) { sb.Append(ExportCertificateToPem(root)); } + result.CertificateChainPem = sb.ToString(); + } + + return result; + } + + public static InfisicalCertificateResult BuildResult(X509Certificate2 leaf, InfisicalSignedCertificate signed) + { + InfisicalCertificateResult result = new InfisicalCertificateResult { Leaf = leaf }; + if (signed != null) + { + result.SerialNumber = signed.SerialNumber; + result.CertificatePem = signed.CertificatePem; + result.CertificateChainPem = signed.CertificateChainPem; + result.PrivateKeyPem = signed.PrivateKeyPem; + } + + List chainCerts = signed != null ? CollectChainCertificates(signed) : new List(); + List intermediates = new List(); + X509Certificate2 root = null; + foreach (X509Certificate2 cert in chainCerts) + { + if (IsSelfSigned(cert)) { if (root == null) { root = cert; } } + else { intermediates.Add(cert); } + } + + result.Intermediates = intermediates.ToArray(); + result.Root = root; + + List ordered = new List(); + if (leaf != null) { ordered.Add(leaf); } + ordered.AddRange(intermediates); + if (root != null) { ordered.Add(root); } + result.Chain = ordered.ToArray(); + return result; + } + + private static List CollectChainCertificates(InfisicalSignedCertificate signed) + { + List chainCerts = PemCertificateBuilder.ReadCertificateChain(signed.CertificateChainPem); + if (!string.IsNullOrEmpty(signed.IssuingCaCertificatePem)) + { + foreach (X509Certificate2 issuing in PemCertificateBuilder.ReadCertificateChain(signed.IssuingCaCertificatePem)) + { + chainCerts.Add(issuing); + } + } + + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + List deduped = new List(); + foreach (X509Certificate2 cert in chainCerts) + { + if (cert == null) { continue; } + if (seen.Add(cert.Thumbprint)) { deduped.Add(cert); } + } + + return deduped; + } + + private static bool IsSelfSigned(X509Certificate2 cert) + { + if (cert == null) { return false; } + return string.Equals(cert.Subject, cert.Issuer, StringComparison.OrdinalIgnoreCase); + } + + private static List BuildLocalChain(X509Certificate2 leaf) + { + List result = new List(); + using (X509Chain chain = new X509Chain()) + { + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = + X509VerificationFlags.IgnoreNotTimeValid | + X509VerificationFlags.IgnoreNotTimeNested | + X509VerificationFlags.IgnoreInvalidName | + X509VerificationFlags.IgnoreInvalidPolicy | + X509VerificationFlags.IgnoreEndRevocationUnknown | + X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown | + X509VerificationFlags.IgnoreRootRevocationUnknown | + X509VerificationFlags.IgnoreCtlNotTimeValid | + X509VerificationFlags.IgnoreCtlSignerRevocationUnknown | + X509VerificationFlags.IgnoreInvalidBasicConstraints | + X509VerificationFlags.IgnoreWrongUsage; + + try { chain.Build(leaf); } + catch { return result; } + + foreach (X509ChainElement element in chain.ChainElements) + { + if (element != null && element.Certificate != null) + { + result.Add(new X509Certificate2(element.Certificate.RawData)); + } + } + } + + return result; + } + + private static string ExportCertificateToPem(X509Certificate2 cert) + { + byte[] der = cert.Export(X509ContentType.Cert); + StringBuilder sb = new StringBuilder(); + sb.AppendLine("-----BEGIN CERTIFICATE-----"); + sb.AppendLine(Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks)); + sb.AppendLine("-----END CERTIFICATE-----"); + return sb.ToString(); + } + + private static string ReadString(IDictionary source, params string[] keys) + { + foreach (string key in keys) + { + if (source.Contains(key)) + { + object value = source[key]; + if (value != null) + { + string text = value.ToString(); + if (!string.IsNullOrEmpty(text)) + { + return text; + } + } + } + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs b/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs new file mode 100644 index 0000000..2aae813 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using BcAttribute = Org.BouncyCastle.Asn1.Cms.Attribute; + +namespace PSInfisicalAPI.Pki +{ + public enum InfisicalKeyAlgorithm + { + Rsa = 0, + Ecdsa = 1, + Ed25519 = 2 + } + + public enum InfisicalEcCurve + { + P256 = 0, + P384 = 1 + } + + public sealed class InfisicalCsrSubject + { + public string CommonName { get; set; } + public string Country { get; set; } + public string State { get; set; } + public string Locality { get; set; } + public string Organization { get; set; } + public string OrganizationalUnit { get; set; } + public string EmailAddress { get; set; } + } + + public sealed class InfisicalCsrOptions + { + public InfisicalKeyAlgorithm KeyAlgorithm { get; set; } = InfisicalKeyAlgorithm.Rsa; + public int RsaKeySize { get; set; } = 2048; + public InfisicalEcCurve EcCurve { get; set; } = InfisicalEcCurve.P256; + } + + public sealed class InfisicalCsrResult + { + public string CsrPem { get; set; } + public string PrivateKeyPem { get; set; } + } + + public static class InfisicalCsrBuilder + { + public static InfisicalCsrResult Build(InfisicalCsrSubject subject, IEnumerable dnsNames, IEnumerable ipAddresses, InfisicalCsrOptions options) + { + if (subject == null) { throw new ArgumentNullException(nameof(subject)); } + if (string.IsNullOrEmpty(subject.CommonName)) { throw new ArgumentException("Subject.CommonName is required.", nameof(subject)); } + if (options == null) { options = new InfisicalCsrOptions(); } + + SecureRandom random = new SecureRandom(); + AsymmetricCipherKeyPair keyPair = GenerateKeyPair(options, random); + string signatureAlgorithm = ResolveSignatureAlgorithm(options); + + X509Name x509Name = BuildX509Name(subject); + Asn1Set attributes = BuildSanAttributes(dnsNames, ipAddresses); + + Pkcs10CertificationRequest pkcs10 = new Pkcs10CertificationRequest(signatureAlgorithm, x509Name, keyPair.Public, attributes, keyPair.Private); + + return new InfisicalCsrResult + { + CsrPem = WritePem(pkcs10), + PrivateKeyPem = WritePem(keyPair.Private) + }; + } + + private static AsymmetricCipherKeyPair GenerateKeyPair(InfisicalCsrOptions options, SecureRandom random) + { + switch (options.KeyAlgorithm) + { + case InfisicalKeyAlgorithm.Rsa: + { + int keySize = options.RsaKeySize; + if (keySize != 2048 && keySize != 3072 && keySize != 4096) + { + throw new ArgumentException("RsaKeySize must be 2048, 3072, or 4096.", nameof(options)); + } + + RsaKeyPairGenerator generator = new RsaKeyPairGenerator(); + generator.Init(new KeyGenerationParameters(random, keySize)); + return generator.GenerateKeyPair(); + } + case InfisicalKeyAlgorithm.Ecdsa: + { + DerObjectIdentifier curveOid = options.EcCurve == InfisicalEcCurve.P384 + ? SecObjectIdentifiers.SecP384r1 + : SecObjectIdentifiers.SecP256r1; + ECKeyPairGenerator generator = new ECKeyPairGenerator("ECDSA"); + generator.Init(new ECKeyGenerationParameters(curveOid, random)); + return generator.GenerateKeyPair(); + } + case InfisicalKeyAlgorithm.Ed25519: + { + Ed25519KeyPairGenerator generator = new Ed25519KeyPairGenerator(); + generator.Init(new Ed25519KeyGenerationParameters(random)); + return generator.GenerateKeyPair(); + } + default: + throw new ArgumentOutOfRangeException(nameof(options), options.KeyAlgorithm, "Unsupported KeyAlgorithm."); + } + } + + private static string ResolveSignatureAlgorithm(InfisicalCsrOptions options) + { + switch (options.KeyAlgorithm) + { + case InfisicalKeyAlgorithm.Rsa: + return "SHA256WITHRSA"; + case InfisicalKeyAlgorithm.Ecdsa: + return options.EcCurve == InfisicalEcCurve.P384 ? "SHA384WITHECDSA" : "SHA256WITHECDSA"; + case InfisicalKeyAlgorithm.Ed25519: + return "Ed25519"; + default: + throw new ArgumentOutOfRangeException(nameof(options), options.KeyAlgorithm, "Unsupported KeyAlgorithm."); + } + } + + private static X509Name BuildX509Name(InfisicalCsrSubject subject) + { + List order = new List(); + Dictionary values = new Dictionary(); + + AppendComponent(order, values, X509Name.C, subject.Country); + AppendComponent(order, values, X509Name.ST, subject.State); + AppendComponent(order, values, X509Name.L, subject.Locality); + AppendComponent(order, values, X509Name.O, subject.Organization); + AppendComponent(order, values, X509Name.OU, subject.OrganizationalUnit); + AppendComponent(order, values, X509Name.CN, subject.CommonName); + AppendComponent(order, values, X509Name.EmailAddress, subject.EmailAddress); + + return new X509Name(order, values); + } + + private static void AppendComponent(List order, Dictionary values, DerObjectIdentifier oid, string value) + { + if (string.IsNullOrEmpty(value)) { return; } + order.Add(oid); + values[oid] = value; + } + + private static Asn1Set BuildSanAttributes(IEnumerable dnsNames, IEnumerable ipAddresses) + { + List generalNames = new List(); + if (dnsNames != null) + { + foreach (string dns in dnsNames) + { + if (string.IsNullOrEmpty(dns)) { continue; } + generalNames.Add(new GeneralName(GeneralName.DnsName, dns)); + } + } + + if (ipAddresses != null) + { + foreach (string ip in ipAddresses) + { + if (string.IsNullOrEmpty(ip)) { continue; } + IPAddress parsed; + if (!IPAddress.TryParse(ip, out parsed)) { continue; } + generalNames.Add(new GeneralName(GeneralName.IPAddress, ip)); + } + } + + if (generalNames.Count == 0) { return null; } + + GeneralNames sanValue = new GeneralNames(generalNames.ToArray()); + X509Extensions extensions = new X509Extensions( + new Dictionary + { + { X509Extensions.SubjectAlternativeName, new X509Extension(false, new DerOctetString(sanValue)) } + }); + + BcAttribute extensionRequest = new BcAttribute(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest, new DerSet(extensions)); + return new DerSet(extensionRequest); + } + + private static string WritePem(object obj) + { + using (StringWriter sw = new StringWriter()) + { + PemWriter pemWriter = new PemWriter(sw); + pemWriter.WriteObject(obj); + pemWriter.Writer.Flush(); + return sw.ToString(); + } + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs b/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs new file mode 100644 index 0000000..366ed98 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalLocalCertificateLookup + { + public static X509Certificate2 FindMatch(StoreName storeName, StoreLocation storeLocation, string commonName, IEnumerable candidateSerialNumbers) + { + HashSet serialSet = NormalizeSerials(candidateSerialNumbers); + string subjectFilter = !string.IsNullOrEmpty(commonName) ? string.Concat("CN=", commonName) : null; + + X509Store store = new X509Store(storeName, storeLocation); + try + { + store.Open(OpenFlags.ReadOnly); + + X509Certificate2 bestMatch = null; + foreach (X509Certificate2 candidate in store.Certificates) + { + if (subjectFilter != null && candidate.Subject.IndexOf(subjectFilter, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + + if (serialSet.Count > 0) + { + string normalizedSerial = NormalizeSerial(candidate.SerialNumber); + if (!serialSet.Contains(normalizedSerial)) + { + continue; + } + } + + if (bestMatch == null || candidate.NotAfter > bestMatch.NotAfter) + { + bestMatch = candidate; + } + } + + return bestMatch; + } + finally + { + store.Close(); + } + } + + public static bool IsRenewable(X509Certificate2 cert, int renewalThresholdDays) + { + if (cert == null) { return true; } + DateTime threshold = DateTime.UtcNow.AddDays(renewalThresholdDays); + return cert.NotAfter.ToUniversalTime() <= threshold; + } + + private static HashSet NormalizeSerials(IEnumerable serials) + { + HashSet set = new HashSet(StringComparer.OrdinalIgnoreCase); + if (serials == null) { return set; } + foreach (string serial in serials) + { + string normalized = NormalizeSerial(serial); + if (!string.IsNullOrEmpty(normalized)) + { + set.Add(normalized); + } + } + + return set; + } + + private static string NormalizeSerial(string value) + { + if (string.IsNullOrEmpty(value)) { return null; } + string trimmed = value.Trim(); + if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + trimmed = trimmed.Substring(2); + } + + return trimmed.Replace(":", string.Empty).Replace(" ", string.Empty).TrimStart('0'); + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs index 06b38a3..ff8de17 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Newtonsoft.Json.Linq; using PSInfisicalAPI.Connections; using PSInfisicalAPI.Endpoints; using PSInfisicalAPI.Errors; @@ -8,6 +9,7 @@ using PSInfisicalAPI.Http; using PSInfisicalAPI.Logging; using PSInfisicalAPI.Models; using PSInfisicalAPI.Serialization; +using System.Linq; namespace PSInfisicalAPI.Pki { @@ -30,23 +32,23 @@ namespace PSInfisicalAPI.Pki public InfisicalCertificateAuthority[] ListInternalCertificateAuthorities(InfisicalConnection connection, string projectId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); List> query = null; - if (!string.IsNullOrEmpty(resolvedProjectId)) + if (!string.IsNullOrEmpty(projectId)) { - query = new List> { new KeyValuePair("projectId", resolvedProjectId) }; + query = new List> { new KeyValuePair("projectId", projectId) }; } 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(response.Body); + string body = response.Body; response.Clear(); - List source = dto != null ? (dto.CertificateAuthorities ?? dto.Cas) : null; - InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, resolvedProjectId); + List source = ParseCaListBody(body); + string fallbackProjectId = !string.IsNullOrEmpty(projectId) ? projectId : connection.ProjectId; + InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, fallbackProjectId); _logger.Information(Component, "Infisical internal certificate authority list retrieval was successful."); return mapped; } @@ -63,21 +65,22 @@ namespace PSInfisicalAPI.Pki if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); } Dictionary pathParameters = new Dictionary { { "caId", caId } }; + List> query = null; + if (!string.IsNullOrEmpty(projectId)) + { + query = new List> { new KeyValuePair("projectId", projectId) }; + } 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(response.Body); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveInternalCertificateAuthority, "RetrieveInternalCertificateAuthority", pathParameters, query, null); + string body = response.Body; response.Clear(); - InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null; - if (inner == null) - { - inner = _serializer.Deserialize(response.Body); - } - - InfisicalCertificateAuthority mapped = InfisicalCaMapper.Map(inner, FirstNonEmpty(projectId, connection.ProjectId)); + InfisicalInternalCaResponseDto inner = ParseCaSingleBody(body); + string fallbackProjectId = !string.IsNullOrEmpty(projectId) ? projectId : connection.ProjectId; + InfisicalCertificateAuthority mapped = InfisicalCaMapper.Map(inner, fallbackProjectId); _logger.Information(Component, "Infisical internal certificate authority retrieval was successful."); return mapped; } @@ -88,6 +91,68 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalCertificate RetrieveCertificate(InfisicalConnection connection, string identifier) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(identifier)) { throw new InfisicalConfigurationException("Identifier (serial number or id) is required."); } + + Dictionary pathParameters = new Dictionary { { "serialNumber", identifier } }; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate '", identifier, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveCertificate, "RetrieveCertificate", pathParameters, null, null); + string body = response.Body; + response.Clear(); + + InfisicalCertificateResponseDto inner = ParseCertificateSingleBody(body); + InfisicalCertificate mapped = InfisicalCertificateMapper.Map(inner, connection.ProjectId); + _logger.Information(Component, "Infisical certificate retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate retrieval failed."); + throw; + } + } + + private List ParseCaListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalInternalCaListResponseDto wrapper = token.ToObject(); + return wrapper != null ? (wrapper.CertificateAuthorities ?? wrapper.Cas) : null; + } + + private InfisicalInternalCaResponseDto ParseCaSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificateAuthority"] is JObject ca1) { return ca1.ToObject(); } + if (obj["ca"] is JObject ca2) { return ca2.ToObject(); } + return obj.ToObject(); + } + + private InfisicalCertificateResponseDto ParseCertificateSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificate"] is JObject cert) { return cert.ToObject(); } + return obj.ToObject(); + } + public InfisicalCertificateSearchResult SearchCertificates(InfisicalConnection connection, InfisicalCertificateSearchQuery query) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } @@ -118,6 +183,93 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalSignedCertificate SignCertificateBySubscriber(InfisicalConnection connection, string subscriberName, string projectId, string csrPem) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(subscriberName)) { throw new InfisicalConfigurationException("SubscriberName is required."); } + if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "subscriberName", subscriberName } }; + InfisicalSignCertificateBySubscriberRequestDto request = new InfisicalSignCertificateBySubscriberRequestDto + { + ProjectId = resolvedProjectId, + Csr = csrPem + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to sign certificate via subscriber '", subscriberName, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.SignCertificateBySubscriber, "SignCertificateBySubscriber", pathParameters, null, body); + InfisicalSignCertificateResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalSignedCertificate signed = MapSigned(dto); + _logger.Information(Component, "Infisical certificate signing (subscriber) was successful."); + return signed; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate signing (subscriber) failed."); + throw; + } + } + + public InfisicalSignedCertificate SignCertificateByCa(InfisicalConnection connection, string caId, string csrPem, string commonName, string altNames, string ttl, string notBefore, string notAfter, string friendlyName, string pkiCollectionId, IEnumerable keyUsages, IEnumerable extendedKeyUsages) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); } + if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); } + if (string.IsNullOrEmpty(ttl) && string.IsNullOrEmpty(notAfter)) { throw new InfisicalConfigurationException("Either Ttl or NotAfter must be provided."); } + + Dictionary pathParameters = new Dictionary { { "caId", caId } }; + InfisicalSignCertificateByCaRequestDto request = new InfisicalSignCertificateByCaRequestDto + { + Csr = csrPem, + CommonName = commonName, + AltNames = altNames, + Ttl = ttl, + NotBefore = notBefore, + NotAfter = notAfter, + FriendlyName = friendlyName, + PkiCollectionId = pkiCollectionId, + KeyUsages = keyUsages != null ? keyUsages.ToList() : null, + ExtendedKeyUsages = extendedKeyUsages != null ? extendedKeyUsages.ToList() : null + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to sign certificate via CA '", caId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.SignCertificateByCa, "SignCertificateByCa", pathParameters, null, body); + InfisicalSignCertificateResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalSignedCertificate signed = MapSigned(dto); + _logger.Information(Component, "Infisical certificate signing (CA) was successful."); + return signed; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate signing (CA) failed."); + throw; + } + } + + private static InfisicalSignedCertificate MapSigned(InfisicalSignCertificateResponseDto dto) + { + if (dto == null) { return null; } + return new InfisicalSignedCertificate + { + SerialNumber = dto.SerialNumber, + CertificatePem = dto.Certificate, + CertificateChainPem = dto.CertificateChain, + IssuingCaCertificatePem = dto.IssuingCaCertificate + }; + } + public InfisicalCertificateBundle GetCertificateBundle(InfisicalConnection connection, string serialNumber) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } diff --git a/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs b/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs new file mode 100644 index 0000000..0a793b9 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs @@ -0,0 +1,10 @@ +namespace PSInfisicalAPI.Pki +{ + public enum InfisicalPrivateKeyProtection + { + Exportable = 0, + LocalOnly = 1, + NonExportable = 2, + Ephemeral = 3 + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs new file mode 100644 index 0000000..e6a82eb --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalSignCertificateBySubscriberRequestDto + { + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("csr")] public string Csr { get; set; } + } + + internal sealed class InfisicalSignCertificateByCaRequestDto + { + [JsonProperty("csr")] public string Csr { get; set; } + [JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; } + [JsonProperty("altNames", NullValueHandling = NullValueHandling.Ignore)] public string AltNames { get; set; } + [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; } + [JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; } + [JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; } + [JsonProperty("friendlyName", NullValueHandling = NullValueHandling.Ignore)] public string FriendlyName { get; set; } + [JsonProperty("pkiCollectionId", NullValueHandling = NullValueHandling.Ignore)] public string PkiCollectionId { get; set; } + [JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List KeyUsages { get; set; } + [JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List ExtendedKeyUsages { get; set; } + } + + internal sealed class InfisicalSignCertificateResponseDto + { + [JsonProperty("certificate")] public string Certificate { get; set; } + [JsonProperty("certificateChain")] public string CertificateChain { get; set; } + [JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; } + [JsonProperty("serialNumber")] public string SerialNumber { get; set; } + } +}