diff --git a/CHANGELOG.md b/CHANGELOG.md index c5dda3a..2e1f651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.2.0 +* added parameter "PassphrasePath" to support custom passphrase path (no longer needs to be a secret named 'passphrase' on the same level) +* added support for optional parameter on store path and passphrase path to indicate the property containing the value (if JSON secret) +* the additional parameter and JSON property identifier apply to the following store types: HCVKVJKS, HCVKVP12, HCVKVPKS + ## 3.1.3 * documentation fix diff --git a/README.md b/README.md index dc4fb11..3fe4cb5 100644 --- a/README.md +++ b/README.md @@ -163,11 +163,49 @@ the Keyfactor Command Portal | ServerUsername | Server Username | The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 | Secret | | ✅ Checked | | ServerPassword | Server Password | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | Secret | | ✅ Checked | | MountPoint | Mount Point | This is the mount point of the instance of the PKI or Keyfactor secrets engine plugin. If using enterprise namespaces: / | String | | ✅ Checked | + | PassphrasePath | Passphrase Path | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | String | | 🔲 Unchecked | The Custom Fields tab should look like this: ![HCVPKI Custom Fields Tab](docsource/images/HCVPKI-custom-fields-store-type-dialog.png) + + ###### Server Username + The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Server Password + Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Mount Point + This is the mount point of the instance of the PKI or Keyfactor secrets engine plugin. If using enterprise namespaces: / + + ![HCVPKI Custom Field - MountPoint](docsource/images/HCVPKI-custom-field-MountPoint-dialog.png) + + + + ###### Passphrase Path + This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. + + ![HCVPKI Custom Field - PassphrasePath](docsource/images/HCVPKI-custom-field-PassphrasePath-dialog.png) + + + + + @@ -351,6 +389,50 @@ the Keyfactor Command Portal ![HCVKVPEM Custom Fields Tab](docsource/images/HCVKVPEM-custom-fields-store-type-dialog.png) + + ###### Server Username + The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Server Password + Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Subfolder Inventory + Should certificates found in sub-paths be included when performing an inventory? + + ![HCVKVPEM Custom Field - SubfolderInventory](docsource/images/HCVKVPEM-custom-field-SubfolderInventory-dialog.png) + + + + ###### Include Certificate Chain + Should the certificate chain be included when performing an enrollment? + + ![HCVKVPEM Custom Field - IncludeCertChain](docsource/images/HCVKVPEM-custom-field-IncludeCertChain-dialog.png) + + + + ###### Mount Point + The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / + + ![HCVKVPEM Custom Field - MountPoint](docsource/images/HCVKVPEM-custom-field-MountPoint-dialog.png) + + + + + @@ -370,10 +452,33 @@ The inventory job will catalog the certificates contained within the store. Add #### Secret naming -In ordered to be managed by this orchestrator extension, a certificate store is comprised of two secret entries: +In order to be managed by this orchestrator extension, a certificate store is comprised of two secret entries: - The certificate with the naming convention `_jks` - A secret containing the store passphrase located on the same level. This should be named `passphrase` +This is the convention followed by the certificate store if the full path to the secret is not provided, and no passphrase path is provided. + + +**As of version 3.2+ of this integration, any secret name can be used, and the passphrase path can be anywhere within an accessable area of the KeyValue secrets engine.** + +Additionally, we can read the certificate store and/or passphrase secret from a JSON secret that contains the value on a specific property. +The way to indicate the property name that should be used to retreive the value of the certificate store or passphrase, add a "?" at the end of the path, followed by the property name. + +**examples:** + +StorePath = `kv-v2/mycerts/myjkscertstore?certData` +> This path indicates that the secret containing the certificate store data is named "myjkscertstore" and is a JSON secret with the `certData` property containing the Base64 encoded certificate store. +> + +StorePath = `kv-v2/mycerts/myjkscertstore` +> This path indicates that the entire secret value is the base64 encoded certificate store + +> Generally, the paths to the certificate store data and passphrase should be in the following format +> `/?` + + +This convention applies to both the Store Path and Passphrase Path. + #### Base64 encoding Certificates should be stored in a base64 encoded format. @@ -467,11 +572,56 @@ the Keyfactor Command Portal | ServerPassword | Server Password | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | Secret | | ✅ Checked | | IncludeCertChain | Include Certificate Chain | Should the certificate chain be included when performing an enrollment? | Bool | false | 🔲 Unchecked | | MountPoint | Mount Point | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | String | | 🔲 Unchecked | + | PassphrasePath | Passphrase Path | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | String | | 🔲 Unchecked | The Custom Fields tab should look like this: ![HCVKVJKS Custom Fields Tab](docsource/images/HCVKVJKS-custom-fields-store-type-dialog.png) + + ###### Server Username + The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Server Password + Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Include Certificate Chain + Should the certificate chain be included when performing an enrollment? + + ![HCVKVJKS Custom Field - IncludeCertChain](docsource/images/HCVKVJKS-custom-field-IncludeCertChain-dialog.png) + + + + ###### Mount Point + The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / + + ![HCVKVJKS Custom Field - MountPoint](docsource/images/HCVKVJKS-custom-field-MountPoint-dialog.png) + + + + ###### Passphrase Path + This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. + + ![HCVKVJKS Custom Field - PassphrasePath](docsource/images/HCVKVJKS-custom-field-PassphrasePath-dialog.png) + + + + + @@ -495,6 +645,29 @@ In ordered to be managed by this orchestrator extension, a certificate store is - The certificate with the naming convention `_p12` - A secret containing the store passphrase located on the same level. This should be named `passphrase` +This is the convention followed by the certificate store if the full path to the secret is not provided, and no passphrase path is provided. + + +**As of version 3.2+ of this integration, any secret name can be used, and the passphrase path can be anywhere within an accessable area of the KeyValue secrets engine.** + +Additionally, we can read the certificate store and/or passphrase secret from a JSON secret that contains the value on a specific property. +The way to indicate the property name that should be used to retreive the value of the certificate store or passphrase, add a "?" at the end of the path, followed by the property name. + +**examples:** + +StorePath = `kv-v2/mycerts/myjkscertstore?certData` +> This path indicates that the secret containing the certificate store data is named "myjkscertstore" and is a JSON secret with the `certData` property containing the Base64 encoded certificate store. +> + +StorePath = `kv-v2/mycerts/myjkscertstore` +> This path indicates that the entire secret value is the base64 encoded certificate store + +> Generally, the paths to the certificate store data and passphrase should be in the following format +> `//?` +> if namespaces are not used, that section can be omitted. + +This convention applies to both the Store Path and Passphrase Path. + #### Base64 encoding Certificates should be stored in a base64 encoded format. @@ -588,11 +761,56 @@ the Keyfactor Command Portal | ServerPassword | Server Password | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | Secret | | ✅ Checked | | IncludeCertChain | Include Certificate Chain | Should the certificate chain be included when performing an enrollment? | Bool | false | 🔲 Unchecked | | MountPoint | Mount Point | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | String | | 🔲 Unchecked | + | PassphrasePath | Passphrase Path | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | String | | 🔲 Unchecked | The Custom Fields tab should look like this: ![HCVKVP12 Custom Fields Tab](docsource/images/HCVKVP12-custom-fields-store-type-dialog.png) + + ###### Server Username + The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Server Password + Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Include Certificate Chain + Should the certificate chain be included when performing an enrollment? + + ![HCVKVP12 Custom Field - IncludeCertChain](docsource/images/HCVKVP12-custom-field-IncludeCertChain-dialog.png) + + + + ###### Mount Point + The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / + + ![HCVKVP12 Custom Field - MountPoint](docsource/images/HCVKVP12-custom-field-MountPoint-dialog.png) + + + + ###### Passphrase Path + This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. + + ![HCVKVP12 Custom Field - PassphrasePath](docsource/images/HCVKVP12-custom-field-PassphrasePath-dialog.png) + + + + + @@ -616,6 +834,28 @@ In ordered to be managed by this orchestrator extension, a certificate store is - The certificate with the naming convention `_pfx` - A secret containing the store passphrase located on the same level. This should be named `passphrase` +This is the convention followed by the certificate store if the full path to the secret is not provided, and no passphrase path is provided. + +**As of version 3.2+ of this integration, any secret name can be used, and the passphrase path can be anywhere within an accessable area of the KeyValue secrets engine.** + +Additionally, we can read the certificate store and/or passphrase secret from a JSON secret that contains the value on a specific property. +The way to indicate the property name that should be used to retreive the value of the certificate store or passphrase, add a "?" at the end of the path, followed by the property name. + +**examples:** + +StorePath = `kv-v2/mycerts/myjkscertstore?certData` +> This path indicates that the secret containing the certificate store data is named "myjkscertstore" and is a JSON secret with the `certData` property containing the Base64 encoded certificate store. +> + +StorePath = `kv-v2/mycerts/myjkscertstore` +> This path indicates that the entire secret value is the base64 encoded certificate store + +> Generally, the paths to the certificate store data and passphrase should be in the following format +> `//?` +> if namespaces are not used, that section can be omitted. + +This convention applies to both the Store Path and Passphrase Path. + #### Base64 encoding Certificates should be stored in a base64 encoded format. @@ -709,11 +949,56 @@ the Keyfactor Command Portal | ServerPassword | Server Password | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | Secret | | ✅ Checked | | IncludeCertChain | Include Certificate Chain | Should the certificate chain be included when performing an enrollment? | Bool | false | 🔲 Unchecked | | MountPoint | Mount Point | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | String | | 🔲 Unchecked | + | PassphrasePath | Passphrase Path | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | String | | 🔲 Unchecked | The Custom Fields tab should look like this: ![HCVKVPFX Custom Fields Tab](docsource/images/HCVKVPFX-custom-fields-store-type-dialog.png) + + ###### Server Username + The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Server Password + Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance + + + > [!IMPORTANT] + > This field is created by the `Needs Server` on the Basic tab, do not create this field manually. + + + + + ###### Include Certificate Chain + Should the certificate chain be included when performing an enrollment? + + ![HCVKVPFX Custom Field - IncludeCertChain](docsource/images/HCVKVPFX-custom-field-IncludeCertChain-dialog.png) + + + + ###### Mount Point + The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / + + ![HCVKVPFX Custom Field - MountPoint](docsource/images/HCVKVPFX-custom-field-MountPoint-dialog.png) + + + + ###### Passphrase Path + This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. + + ![HCVKVPFX Custom Field - PassphrasePath](docsource/images/HCVKVPFX-custom-field-PassphrasePath-dialog.png) + + + + + @@ -722,15 +1007,14 @@ the Keyfactor Command Portal 1. **Download the latest Hashicorp Vault Universal Orchestrator extension from GitHub.** - Navigate to the [Hashicorp Vault Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/hashicorp-vault-orchestrator/releases/latest). Refer to the compatibility matrix below to determine whether the `net6.0` or `net8.0` asset should be downloaded. Then, click the corresponding asset to download the zip archive. + Navigate to the [Hashicorp Vault Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/hashicorp-vault-orchestrator/releases/latest). Refer to the compatibility matrix below to determine the asset should be downloaded. Then, click the corresponding asset to download the zip archive. | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `hashicorp-vault-orchestrator` .NET version to download | | --------- | ----------- | ----------- | ----------- | | Older than `11.0.0` | | | `net6.0` | | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` | - | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | - | `11.6` _and_ newer | `net8.0` | | `net8.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` || Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | + | `11.6` _and_ newer | `net8.0` | | `net8.0` | Unzip the archive containing extension assemblies to a known location. @@ -833,6 +1117,7 @@ The Hashicorp Vault Universal Orchestrator extension implements 5 Certificate St | ServerUsername | The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 | | ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | MountPoint | This is the mount point of the instance of the PKI or Keyfactor secrets engine plugin. If using enterprise namespaces: / | + | PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | @@ -861,6 +1146,7 @@ The Hashicorp Vault Universal Orchestrator extension implements 5 Certificate St | Properties.ServerUsername | The base URI (and port) to the instance of Hashicorp Vault ex: https://localhost:8200 | | Properties.ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | Properties.MountPoint | This is the mount point of the instance of the PKI or Keyfactor secrets engine plugin. If using enterprise namespaces: / | + | Properties.PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | 3. **Import the CSV file to create the certificate stores** @@ -1077,6 +1363,7 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov | ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | IncludeCertChain | Should the certificate chain be included when performing an enrollment? | | MountPoint | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | + | PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | @@ -1106,6 +1393,7 @@ Please refer to the **Universal Orchestrator (remote)** usage section ([PAM prov | Properties.ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | Properties.IncludeCertChain | Should the certificate chain be included when performing an enrollment? | | Properties.MountPoint | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | + | Properties.PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | 3. **Import the CSV file to create the certificate stores** @@ -1162,6 +1450,7 @@ Here are the steps for manually creating the store type in Keyfactor Command. - Click the "Custom Fields" tab to add the following custom fields: - **MountPoint** - Type: *string* - **IncludeCertChain** - Type: *bool* (If true, the available intermediate certificates will also be written to Vault during enrollment) + - **PassphrasePath** - Type: *string* (If the passphrase is in a location other than in a secret named 'passphrase' at the same level as the cert store, provide the path here) ![](images/cert-store-type-kv-notPEM-custom-tab.png) @@ -1181,10 +1470,11 @@ In Keyfactor Command create a new Certificate Store that resembles the one below - **Client Machine** - Enter an identifier for the client machine. This could be the Orchestrator host name, or anything else useful. This value is not used by the extension. - **Store Path** - This is the path after mount point where the certs will be stored. - - example: `kv-v2\kf-secrets\mystore_jks` would use the path "\kf-secrets" + - example: `kv-v2\kf-secrets\mystore_jks` - **Mount Point** - This is the mount point name for the instance of the Key Value secrets engine. - If left blank, will default to "kv-v2". - If your organization utilizes Vault enterprise namespaces, you should include the namespace here. +- **Passphrase Path** - The path to the secret (and optional JSON property) where the certificate store passphrase is located. ##### Set the server username and password @@ -1221,6 +1511,7 @@ In Keyfactor Command create a new Certificate Store that resembles the one below | ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | IncludeCertChain | Should the certificate chain be included when performing an enrollment? | | MountPoint | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | + | PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | @@ -1250,6 +1541,7 @@ In Keyfactor Command create a new Certificate Store that resembles the one below | Properties.ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | Properties.IncludeCertChain | Should the certificate chain be included when performing an enrollment? | | Properties.MountPoint | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | + | Properties.PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | 3. **Import the CSV file to create the certificate stores** @@ -1306,6 +1598,7 @@ Here are the steps for manually creating the store type in Keyfactor Command. - Click the "Custom Fields" tab to add the following custom fields: - **MountPoint** - Type: *string* - **IncludeCertChain** - Type: *bool* (If true, the available intermediate certificates will also be written to Vault during enrollment) + - **PassphrasePath** - Type: *string* (If the passphrase is in a location other than in a secret named 'passphrase' at the same level as the cert store, provide the path here) ![](images/cert-store-type-kv-notPEM-custom-tab.png) @@ -1325,10 +1618,11 @@ Create a new Certificate Store that resembles the one below: - **Client Machine** - Enter an identifier for the client machine. This could be the Orchestrator host name, or anything else useful. This value is not used by the extension. - **Store Path** - This is the path after mount point where the certs will be stored. - - example: `kv-v2\kf-secrets\mystore_p12` would use the path "\kf-secrets" + - example: `kv-v2\kf-secrets\mystore_p12` - **Mount Point** - This is the mount point name for the instance of the Key Value secrets engine. - If left blank, will default to "kv-v2". - If your organization utilizes Vault enterprise namespaces, you should include the namespace here. + - **Passphrase Path** - The path to the secret (and optional JSON property) where the certificate store passphrase is located. ##### Set the server username and password @@ -1367,6 +1661,7 @@ At this point, the certificate store should be created and ready to peform inven | ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | IncludeCertChain | Should the certificate chain be included when performing an enrollment? | | MountPoint | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | + | PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | @@ -1396,6 +1691,7 @@ At this point, the certificate store should be created and ready to peform inven | Properties.ServerPassword | Vault token that will be used by the Orchestrator integration for authenticating and performing operations in the Vault instance | | Properties.IncludeCertChain | Should the certificate chain be included when performing an enrollment? | | Properties.MountPoint | The base mount point of the secrets engine. If using Vault Namespaces, include the namespace; ie. / | + | Properties.PassphrasePath | This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret. | 3. **Import the CSV file to create the certificate stores** @@ -1452,6 +1748,7 @@ Here are the steps for manually creating the store type in Keyfactor Command. - Click the "Custom Fields" tab to add the following custom fields: - **MountPoint** - Type: *string* - **IncludeCertChain** - Type: *bool* (If true, the available intermediate certificates will also be written to Vault during enrollment) + - **PassphrasePath** - Type: *string* (If the passphrase is in a location other than in a secret named 'passphrase' at the same level as the cert store, provide the path here) ![](images/cert-store-type-kv-notPEM-custom-tab.png) @@ -1471,10 +1768,11 @@ Create a new Certificate Store that resembles the one below: - **Client Machine** - Enter an identifier for the client machine. This could be the Orchestrator host name, or anything else useful. This value is not used by the extension. - **Store Path** - This is the path to the secret containing the store. - - example: `kv-v2\kf-secrets\mystore_pfx` would use the path "\kf-secrets" + - example: `kv-v2\kf-secrets\mystore_pfx` - **Mount Point** - This is the mount point name for the instance of the Key Value secrets engine. - If left blank, will default to "kv-v2". - If your organization utilizes Vault enterprise namespaces, you should include the namespace here. +- **Passphrase Path** - The path to the secret (and optional JSON property) where the certificate store passphrase is located. ##### Set the server username and password @@ -1527,7 +1825,10 @@ The certificate store entry is returned from a discovery job when.. 1. There is an entry named `passphrase` that contains the password for the store on the same level. 1. The entry for the certificate contain the base64 encoded certificate file. -**Note**: Key/Value secrets that do not include the expected keys or names do not end with "_p12" will be ignored during inventory scans. +> :warning: +> While any secret and passphrase location can be used, the discovery job can only discover certificate stores that follow the default convention. +> If you store your certificate stores and passphrases with another convention, the discovery job will not work in that case. + Set the following fields to configure a discovery job for JKS Certificate Stores: - **Client Machine** - any string; it is unused by the Discovery job @@ -1556,7 +1857,10 @@ The certificate store entry is returned from a discovery job when.. 1. There is an entry named `passphrase` that contains the password for the store on the same level. 1. The entry for the certificate contain the base64 encoded certificate file. -**Note**: Key/Value secrets that do not include the expected keys or names do not end with "_p12" will be ignored during inventory scans. +> :warning: +> While any secret and passphrase location can be used, the discovery job can only discover certificate stores that follow the default convention. +> If you store your certificate stores and passphrases with another convention, the discovery job will not work in that case. + Set the following fields to configure a discovery job for PKCS12 Certificate Stores: - **Client Machine** - any string; it is unused by the Discovery job @@ -1585,7 +1889,9 @@ The certificate store entry is returned from a discovery job when.. 1. There is an entry named `passphrase` that contains the password for the store on the same level. 1. The entry for the certificate contain the base64 encoded certificate file. -**Note**: Key/Value secrets that do not include the expected keys or names do not end with "_pfx" will be ignored during inventory scans. +> :warning: +> While any secret and passphrase location can be used, the discovery job can only discover certificate stores that follow the default convention. +> If you store your certificate stores and passphrases with another convention, the discovery job will not work in that case. Set the following fields to configure a discovery job for PFX Certificate Stores: - **Client Machine** - any string; it is unused by the Discovery job diff --git a/docsource/hcvkvjks.md b/docsource/hcvkvjks.md index 7cb4bf2..3ede1b3 100644 --- a/docsource/hcvkvjks.md +++ b/docsource/hcvkvjks.md @@ -8,10 +8,34 @@ The inventory job will catalog the certificates contained within the store. Add ### Secret naming -In ordered to be managed by this orchestrator extension, a certificate store is comprised of two secret entries: +In order to be managed by this orchestrator extension, a certificate store is comprised of two secret entries: - The certificate with the naming convention `_jks` - A secret containing the store passphrase located on the same level. This should be named `passphrase` +This is the convention followed by the certificate store if the full path to the secret is not provided, and no passphrase path is provided. + + +**As of version 3.2+ of this integration, any secret name can be used, and the passphrase path can be anywhere within an accessable area of the KeyValue secrets engine.** + +Additionally, we can read the certificate store and/or passphrase secret from a JSON secret that contains the value on a specific property. +The way to indicate the property name that should be used to retreive the value of the certificate store or passphrase, add a "?" at the end of the path, followed by the property name. + +**examples:** + +StorePath = `kv-v2/mycerts/myjkscertstore?certData` +> This path indicates that the secret containing the certificate store data is named "myjkscertstore" and is a JSON secret with the `certData` property containing the Base64 encoded certificate store. +> + +StorePath = `kv-v2/mycerts/myjkscertstore` +> This path indicates that the entire secret value is the base64 encoded certificate store + +> Generally, the paths to the certificate store data and passphrase should be in the following format +> `/?` + + +This convention applies to both the Store Path and Passphrase Path. + + ### Base64 encoding Certificates should be stored in a base64 encoded format. @@ -46,6 +70,7 @@ Here are the steps for manually creating the store type in Keyfactor Command. - Click the "Custom Fields" tab to add the following custom fields: - **MountPoint** - Type: *string* - **IncludeCertChain** - Type: *bool* (If true, the available intermediate certificates will also be written to Vault during enrollment) + - **PassphrasePath** - Type: *string* (If the passphrase is in a location other than in a secret named 'passphrase' at the same level as the cert store, provide the path here) ![](images/cert-store-type-kv-notPEM-custom-tab.png) @@ -65,10 +90,11 @@ In Keyfactor Command create a new Certificate Store that resembles the one below - **Client Machine** - Enter an identifier for the client machine. This could be the Orchestrator host name, or anything else useful. This value is not used by the extension. - **Store Path** - This is the path after mount point where the certs will be stored. - - example: `kv-v2\kf-secrets\mystore_jks` would use the path "\kf-secrets" + - example: `kv-v2\kf-secrets\mystore_jks` - **Mount Point** - This is the mount point name for the instance of the Key Value secrets engine. - If left blank, will default to "kv-v2". - If your organization utilizes Vault enterprise namespaces, you should include the namespace here. +- **Passphrase Path** - The path to the secret (and optional JSON property) where the certificate store passphrase is located. #### Set the server username and password @@ -84,7 +110,10 @@ The certificate store entry is returned from a discovery job when.. 1. There is an entry named `passphrase` that contains the password for the store on the same level. 1. The entry for the certificate contain the base64 encoded certificate file. -**Note**: Key/Value secrets that do not include the expected keys or names do not end with "_p12" will be ignored during inventory scans. +> :warning: +> While any secret and passphrase location can be used, the discovery job can only discover certificate stores that follow the default convention. +> If you store your certificate stores and passphrases with another convention, the discovery job will not work in that case. + Set the following fields to configure a discovery job for JKS Certificate Stores: - **Client Machine** - any string; it is unused by the Discovery job diff --git a/docsource/hcvkvp12.md b/docsource/hcvkvp12.md index 960d8a3..ccbe06e 100644 --- a/docsource/hcvkvp12.md +++ b/docsource/hcvkvp12.md @@ -12,6 +12,29 @@ In ordered to be managed by this orchestrator extension, a certificate store is - The certificate with the naming convention `_p12` - A secret containing the store passphrase located on the same level. This should be named `passphrase` +This is the convention followed by the certificate store if the full path to the secret is not provided, and no passphrase path is provided. + + +**As of version 3.2+ of this integration, any secret name can be used, and the passphrase path can be anywhere within an accessable area of the KeyValue secrets engine.** + +Additionally, we can read the certificate store and/or passphrase secret from a JSON secret that contains the value on a specific property. +The way to indicate the property name that should be used to retreive the value of the certificate store or passphrase, add a "?" at the end of the path, followed by the property name. + +**examples:** + +StorePath = `kv-v2/mycerts/myjkscertstore?certData` +> This path indicates that the secret containing the certificate store data is named "myjkscertstore" and is a JSON secret with the `certData` property containing the Base64 encoded certificate store. +> + +StorePath = `kv-v2/mycerts/myjkscertstore` +> This path indicates that the entire secret value is the base64 encoded certificate store + +> Generally, the paths to the certificate store data and passphrase should be in the following format +> `//?` +> if namespaces are not used, that section can be omitted. + +This convention applies to both the Store Path and Passphrase Path. + ### Base64 encoding Certificates should be stored in a base64 encoded format. @@ -46,6 +69,7 @@ Here are the steps for manually creating the store type in Keyfactor Command. - Click the "Custom Fields" tab to add the following custom fields: - **MountPoint** - Type: *string* - **IncludeCertChain** - Type: *bool* (If true, the available intermediate certificates will also be written to Vault during enrollment) + - **PassphrasePath** - Type: *string* (If the passphrase is in a location other than in a secret named 'passphrase' at the same level as the cert store, provide the path here) ![](images/cert-store-type-kv-notPEM-custom-tab.png) @@ -65,11 +89,11 @@ Create a new Certificate Store that resembles the one below: - **Client Machine** - Enter an identifier for the client machine. This could be the Orchestrator host name, or anything else useful. This value is not used by the extension. - **Store Path** - This is the path after mount point where the certs will be stored. - - example: `kv-v2\kf-secrets\mystore_p12` would use the path "\kf-secrets" + - example: `kv-v2\kf-secrets\mystore_p12` - **Mount Point** - This is the mount point name for the instance of the Key Value secrets engine. - If left blank, will default to "kv-v2". - If your organization utilizes Vault enterprise namespaces, you should include the namespace here. - + - **Passphrase Path** - The path to the secret (and optional JSON property) where the certificate store passphrase is located. #### Set the server username and password - **SERVER USERNAME** should be the full URL to the instance of Vault that will be accessible by the orchestrator. (example: `http://127.0.0.1:8200`) @@ -86,7 +110,10 @@ The certificate store entry is returned from a discovery job when.. 1. There is an entry named `passphrase` that contains the password for the store on the same level. 1. The entry for the certificate contain the base64 encoded certificate file. -**Note**: Key/Value secrets that do not include the expected keys or names do not end with "_p12" will be ignored during inventory scans. +> :warning: +> While any secret and passphrase location can be used, the discovery job can only discover certificate stores that follow the default convention. +> If you store your certificate stores and passphrases with another convention, the discovery job will not work in that case. + Set the following fields to configure a discovery job for PKCS12 Certificate Stores: - **Client Machine** - any string; it is unused by the Discovery job diff --git a/docsource/hcvkvpfx.md b/docsource/hcvkvpfx.md index 9d4dc8f..a1a1f68 100644 --- a/docsource/hcvkvpfx.md +++ b/docsource/hcvkvpfx.md @@ -12,6 +12,28 @@ In ordered to be managed by this orchestrator extension, a certificate store is - The certificate with the naming convention `_pfx` - A secret containing the store passphrase located on the same level. This should be named `passphrase` +This is the convention followed by the certificate store if the full path to the secret is not provided, and no passphrase path is provided. + +**As of version 3.2+ of this integration, any secret name can be used, and the passphrase path can be anywhere within an accessable area of the KeyValue secrets engine.** + +Additionally, we can read the certificate store and/or passphrase secret from a JSON secret that contains the value on a specific property. +The way to indicate the property name that should be used to retreive the value of the certificate store or passphrase, add a "?" at the end of the path, followed by the property name. + +**examples:** + +StorePath = `kv-v2/mycerts/myjkscertstore?certData` +> This path indicates that the secret containing the certificate store data is named "myjkscertstore" and is a JSON secret with the `certData` property containing the Base64 encoded certificate store. +> + +StorePath = `kv-v2/mycerts/myjkscertstore` +> This path indicates that the entire secret value is the base64 encoded certificate store + +> Generally, the paths to the certificate store data and passphrase should be in the following format +> `//?` +> if namespaces are not used, that section can be omitted. + +This convention applies to both the Store Path and Passphrase Path. + ### Base64 encoding Certificates should be stored in a base64 encoded format. @@ -46,6 +68,7 @@ Here are the steps for manually creating the store type in Keyfactor Command. - Click the "Custom Fields" tab to add the following custom fields: - **MountPoint** - Type: *string* - **IncludeCertChain** - Type: *bool* (If true, the available intermediate certificates will also be written to Vault during enrollment) + - **PassphrasePath** - Type: *string* (If the passphrase is in a location other than in a secret named 'passphrase' at the same level as the cert store, provide the path here) ![](images/cert-store-type-kv-notPEM-custom-tab.png) @@ -65,10 +88,11 @@ Create a new Certificate Store that resembles the one below: - **Client Machine** - Enter an identifier for the client machine. This could be the Orchestrator host name, or anything else useful. This value is not used by the extension. - **Store Path** - This is the path to the secret containing the store. - - example: `kv-v2\kf-secrets\mystore_pfx` would use the path "\kf-secrets" + - example: `kv-v2\kf-secrets\mystore_pfx` - **Mount Point** - This is the mount point name for the instance of the Key Value secrets engine. - If left blank, will default to "kv-v2". - If your organization utilizes Vault enterprise namespaces, you should include the namespace here. +- **Passphrase Path** - The path to the secret (and optional JSON property) where the certificate store passphrase is located. #### Set the server username and password @@ -86,7 +110,9 @@ The certificate store entry is returned from a discovery job when.. 1. There is an entry named `passphrase` that contains the password for the store on the same level. 1. The entry for the certificate contain the base64 encoded certificate file. -**Note**: Key/Value secrets that do not include the expected keys or names do not end with "_pfx" will be ignored during inventory scans. +> :warning: +> While any secret and passphrase location can be used, the discovery job can only discover certificate stores that follow the default convention. +> If you store your certificate stores and passphrases with another convention, the discovery job will not work in that case. Set the following fields to configure a discovery job for PFX Certificate Stores: - **Client Machine** - any string; it is unused by the Discovery job diff --git a/docsource/images/HCVKVJKS-advanced-store-type-dialog.png b/docsource/images/HCVKVJKS-advanced-store-type-dialog.png new file mode 100644 index 0000000..4827627 Binary files /dev/null and b/docsource/images/HCVKVJKS-advanced-store-type-dialog.png differ diff --git a/docsource/images/HCVKVJKS-basic-store-type-dialog.png b/docsource/images/HCVKVJKS-basic-store-type-dialog.png new file mode 100644 index 0000000..4f49223 Binary files /dev/null and b/docsource/images/HCVKVJKS-basic-store-type-dialog.png differ diff --git a/docsource/images/HCVKVJKS-custom-field-IncludeCertChain-dialog.png b/docsource/images/HCVKVJKS-custom-field-IncludeCertChain-dialog.png new file mode 100644 index 0000000..77ecd66 Binary files /dev/null and b/docsource/images/HCVKVJKS-custom-field-IncludeCertChain-dialog.png differ diff --git a/docsource/images/HCVKVJKS-custom-field-MountPoint-dialog.png b/docsource/images/HCVKVJKS-custom-field-MountPoint-dialog.png new file mode 100644 index 0000000..ebffcc6 Binary files /dev/null and b/docsource/images/HCVKVJKS-custom-field-MountPoint-dialog.png differ diff --git a/docsource/images/HCVKVJKS-custom-field-PassphrasePath-dialog.png b/docsource/images/HCVKVJKS-custom-field-PassphrasePath-dialog.png new file mode 100644 index 0000000..bd98256 Binary files /dev/null and b/docsource/images/HCVKVJKS-custom-field-PassphrasePath-dialog.png differ diff --git a/docsource/images/HCVKVJKS-custom-fields-store-type-dialog.png b/docsource/images/HCVKVJKS-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..fc6d7ca Binary files /dev/null and b/docsource/images/HCVKVJKS-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/HCVKVP12-advanced-store-type-dialog.png b/docsource/images/HCVKVP12-advanced-store-type-dialog.png new file mode 100644 index 0000000..4827627 Binary files /dev/null and b/docsource/images/HCVKVP12-advanced-store-type-dialog.png differ diff --git a/docsource/images/HCVKVP12-basic-store-type-dialog.png b/docsource/images/HCVKVP12-basic-store-type-dialog.png new file mode 100644 index 0000000..83c762c Binary files /dev/null and b/docsource/images/HCVKVP12-basic-store-type-dialog.png differ diff --git a/docsource/images/HCVKVP12-custom-field-IncludeCertChain-dialog.png b/docsource/images/HCVKVP12-custom-field-IncludeCertChain-dialog.png new file mode 100644 index 0000000..77ecd66 Binary files /dev/null and b/docsource/images/HCVKVP12-custom-field-IncludeCertChain-dialog.png differ diff --git a/docsource/images/HCVKVP12-custom-field-MountPoint-dialog.png b/docsource/images/HCVKVP12-custom-field-MountPoint-dialog.png new file mode 100644 index 0000000..ebffcc6 Binary files /dev/null and b/docsource/images/HCVKVP12-custom-field-MountPoint-dialog.png differ diff --git a/docsource/images/HCVKVP12-custom-field-PassphrasePath-dialog.png b/docsource/images/HCVKVP12-custom-field-PassphrasePath-dialog.png new file mode 100644 index 0000000..bd98256 Binary files /dev/null and b/docsource/images/HCVKVP12-custom-field-PassphrasePath-dialog.png differ diff --git a/docsource/images/HCVKVP12-custom-fields-store-type-dialog.png b/docsource/images/HCVKVP12-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..fc6d7ca Binary files /dev/null and b/docsource/images/HCVKVP12-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/HCVKVPEM-advanced-store-type-dialog.png b/docsource/images/HCVKVPEM-advanced-store-type-dialog.png new file mode 100644 index 0000000..4827627 Binary files /dev/null and b/docsource/images/HCVKVPEM-advanced-store-type-dialog.png differ diff --git a/docsource/images/HCVKVPEM-basic-store-type-dialog.png b/docsource/images/HCVKVPEM-basic-store-type-dialog.png new file mode 100644 index 0000000..ffbb724 Binary files /dev/null and b/docsource/images/HCVKVPEM-basic-store-type-dialog.png differ diff --git a/docsource/images/HCVKVPEM-custom-field-IncludeCertChain-dialog.png b/docsource/images/HCVKVPEM-custom-field-IncludeCertChain-dialog.png new file mode 100644 index 0000000..8757103 Binary files /dev/null and b/docsource/images/HCVKVPEM-custom-field-IncludeCertChain-dialog.png differ diff --git a/docsource/images/HCVKVPEM-custom-field-MountPoint-dialog.png b/docsource/images/HCVKVPEM-custom-field-MountPoint-dialog.png new file mode 100644 index 0000000..ce2b319 Binary files /dev/null and b/docsource/images/HCVKVPEM-custom-field-MountPoint-dialog.png differ diff --git a/docsource/images/HCVKVPEM-custom-field-SubfolderInventory-dialog.png b/docsource/images/HCVKVPEM-custom-field-SubfolderInventory-dialog.png new file mode 100644 index 0000000..99f7040 Binary files /dev/null and b/docsource/images/HCVKVPEM-custom-field-SubfolderInventory-dialog.png differ diff --git a/docsource/images/HCVKVPEM-custom-fields-store-type-dialog.png b/docsource/images/HCVKVPEM-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..252c98e Binary files /dev/null and b/docsource/images/HCVKVPEM-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/HCVKVPFX-advanced-store-type-dialog.png b/docsource/images/HCVKVPFX-advanced-store-type-dialog.png new file mode 100644 index 0000000..4827627 Binary files /dev/null and b/docsource/images/HCVKVPFX-advanced-store-type-dialog.png differ diff --git a/docsource/images/HCVKVPFX-basic-store-type-dialog.png b/docsource/images/HCVKVPFX-basic-store-type-dialog.png new file mode 100644 index 0000000..c9fc789 Binary files /dev/null and b/docsource/images/HCVKVPFX-basic-store-type-dialog.png differ diff --git a/docsource/images/HCVKVPFX-custom-field-IncludeCertChain-dialog.png b/docsource/images/HCVKVPFX-custom-field-IncludeCertChain-dialog.png new file mode 100644 index 0000000..77ecd66 Binary files /dev/null and b/docsource/images/HCVKVPFX-custom-field-IncludeCertChain-dialog.png differ diff --git a/docsource/images/HCVKVPFX-custom-field-MountPoint-dialog.png b/docsource/images/HCVKVPFX-custom-field-MountPoint-dialog.png new file mode 100644 index 0000000..ebffcc6 Binary files /dev/null and b/docsource/images/HCVKVPFX-custom-field-MountPoint-dialog.png differ diff --git a/docsource/images/HCVKVPFX-custom-field-PassphrasePath-dialog.png b/docsource/images/HCVKVPFX-custom-field-PassphrasePath-dialog.png new file mode 100644 index 0000000..bd98256 Binary files /dev/null and b/docsource/images/HCVKVPFX-custom-field-PassphrasePath-dialog.png differ diff --git a/docsource/images/HCVKVPFX-custom-fields-store-type-dialog.png b/docsource/images/HCVKVPFX-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..fc6d7ca Binary files /dev/null and b/docsource/images/HCVKVPFX-custom-fields-store-type-dialog.png differ diff --git a/docsource/images/HCVPKI-advanced-store-type-dialog.png b/docsource/images/HCVPKI-advanced-store-type-dialog.png index 9fc6c81..ce89bb7 100644 Binary files a/docsource/images/HCVPKI-advanced-store-type-dialog.png and b/docsource/images/HCVPKI-advanced-store-type-dialog.png differ diff --git a/docsource/images/HCVPKI-custom-field-MountPoint-dialog.png b/docsource/images/HCVPKI-custom-field-MountPoint-dialog.png new file mode 100644 index 0000000..3b592de Binary files /dev/null and b/docsource/images/HCVPKI-custom-field-MountPoint-dialog.png differ diff --git a/docsource/images/HCVPKI-custom-field-PassphrasePath-dialog.png b/docsource/images/HCVPKI-custom-field-PassphrasePath-dialog.png new file mode 100644 index 0000000..455978e Binary files /dev/null and b/docsource/images/HCVPKI-custom-field-PassphrasePath-dialog.png differ diff --git a/docsource/images/HCVPKI-custom-fields-store-type-dialog.png b/docsource/images/HCVPKI-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..f9c6c12 Binary files /dev/null and b/docsource/images/HCVPKI-custom-fields-store-type-dialog.png differ diff --git a/hashicorp-vault-orchestrator/CertUtility.cs b/hashicorp-vault-orchestrator/CertUtility.cs index 7d0056a..a6cba28 100644 --- a/hashicorp-vault-orchestrator/CertUtility.cs +++ b/hashicorp-vault-orchestrator/CertUtility.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; diff --git a/hashicorp-vault-orchestrator/Constants.cs b/hashicorp-vault-orchestrator/Constants.cs index 0ede848..02890c1 100644 --- a/hashicorp-vault-orchestrator/Constants.cs +++ b/hashicorp-vault-orchestrator/Constants.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. namespace Keyfactor.Extensions.Orchestrator.HashicorpVault { @@ -37,6 +38,8 @@ static class StoreFileExtensions public const string HCVKVPKCS12 = "_p12"; public const string HCVKVPFX = "_pfx"; public const string HCVKVPEM = "certificate"; + public const string PASSPHRASE = "passphrase"; + public static string ForStoreType(string type) { switch (type) diff --git a/hashicorp-vault-orchestrator/FileStores/FileStoreBase.cs b/hashicorp-vault-orchestrator/FileStores/FileStoreBase.cs new file mode 100644 index 0000000..e75d019 --- /dev/null +++ b/hashicorp-vault-orchestrator/FileStores/FileStoreBase.cs @@ -0,0 +1,17 @@ + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using Microsoft.Extensions.Logging; + +namespace Keyfactor.Extensions.Orchestrator.HashicorpVault.FileStores +{ + public class FileStoreBase + { + internal protected ILogger logger { get; set; } + } +} diff --git a/hashicorp-vault-orchestrator/FileStores/IFileStore.cs b/hashicorp-vault-orchestrator/FileStores/IFileStore.cs index 135d6b3..aa5e0a1 100644 --- a/hashicorp-vault-orchestrator/FileStores/IFileStore.cs +++ b/hashicorp-vault-orchestrator/FileStores/IFileStore.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System.Collections.Generic; using Keyfactor.Orchestrators.Extensions; @@ -15,6 +16,6 @@ public interface IFileStore string AddCertificate(string alias, string pfxPassword, string entryContents, bool includeChain, string certContent, string passphrase); string RemoveCertificate(string alias, string passphrase, string storeFileContent); byte[] CreateFileStore(string passphrase); - IEnumerable GetInventory(Dictionary certFields); + IEnumerable GetInventory(string cert, string passphrase); } } diff --git a/hashicorp-vault-orchestrator/FileStores/JksFileStore.cs b/hashicorp-vault-orchestrator/FileStores/JksFileStore.cs index 97b57d8..1baff89 100644 --- a/hashicorp-vault-orchestrator/FileStores/JksFileStore.cs +++ b/hashicorp-vault-orchestrator/FileStores/JksFileStore.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; @@ -12,7 +13,6 @@ using Keyfactor.Logging; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; -using NLog.Config; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.Security; @@ -35,12 +35,10 @@ public byte[] CreateFileStore(string password) { var newStore = new JksStore(); - using (var outstream = new MemoryStream()) - { - logger.LogDebug("Created new JKS store, saving it to outStream"); - newStore.Save(outstream, password.ToCharArray()); - return outstream.ToArray(); - } + using var outstream = new MemoryStream(); + logger.LogDebug("Created new JKS store, saving it to outStream"); + newStore.Save(outstream, password.ToCharArray()); + return outstream.ToArray(); } public string AddCertificate(string alias, string pfxPassword, string entryContents, bool includeChain, string storeFileContent, string passphrase) @@ -70,46 +68,18 @@ public string RemoveCertificate(string alias, string passphrase, string storeFil return Convert.ToBase64String(newJksBytes); } - public IEnumerable GetInventory(Dictionary certFields) + public IEnumerable GetInventory(string base64encodedCertStore, string passphrase) { logger.MethodEntry(); - // certFields should contain two entries. The certificate with the "_jks" suffix, and "passphrase" - string password; - string base64EncodedJksStore; var certs = new List(); string certKey = null; try { - logger.LogTrace($"checking these keys for one that ends in {StoreFileExtensions.HCVKVJKS}"); - certFields.Keys.ToList().ForEach(key => logger.LogTrace(key)); - - certKey = certFields.Keys.First(f => f.EndsWith(StoreFileExtensions.HCVKVJKS)); - - if (certKey == null) - { - throw new Exception($"No entry with extension '{StoreFileExtensions.HCVKVJKS}' found"); - } - else - { - base64EncodedJksStore = certFields[certKey].ToString(); - logger.LogTrace($"reading the base64 encoded file store. It is {base64EncodedJksStore.Length} characters in size."); - } - - if (certFields.TryGetValue("passphrase", out object filePasswordObj)) - { - password = filePasswordObj.ToString(); - logger.LogTrace($"retreived the store passphrase. it is {password.Length} characters."); - } - else - { - throw new Exception($"No passphrase entry found for JKS store '{certKey}'."); - } - logger.LogTrace("converting base64 encoded cert to binary."); - var jksBytes = Convert.FromBase64String(base64EncodedJksStore); - var pkcs12Store = JksToPkcs12Store(jksBytes, password); + var jksBytes = Convert.FromBase64String(base64encodedCertStore); + var pkcs12Store = JksToPkcs12Store(jksBytes, passphrase); certs = CertUtility.CurrentInventoryFromPkcs12(pkcs12Store); logger.MethodExit(); return certs; @@ -117,8 +87,8 @@ public IEnumerable GetInventory(Dictionary } catch (Exception ex) { - logger.LogError(ex, $"Error reading entry for {certKey} in vault. {ex.Message}"); - + logger.LogError($"error reading entry for {certKey} in vault. {ex.Message}"); + logger.LogError($"{LogHandler.FlattenException(ex)}"); throw; } } @@ -137,7 +107,7 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC // If existingStore is not null, load it into jksStore if (existingStore != null) { - logger.LogDebug("Loading existing JKS store"); + logger.LogDebug("loading existing JKS store"); try { using (var ms = new MemoryStream(existingStore)) @@ -150,18 +120,16 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC logger.LogDebug(ex, "error loading store as JKS, attempting to load as PKCS12"); try { - using (var ms = new MemoryStream(existingStore)) - { - logger.LogTrace("creating pkcs12 store for working with the certificate."); - Pkcs12StoreBuilder pkcs12storeBuilder = new Pkcs12StoreBuilder(); - existingPKCS12Store = pkcs12storeBuilder.Build(); - existingPKCS12Store.Load(ms, existingStorePassword.ToCharArray()); - isPKCS12Format = true; - } + using var ms = new MemoryStream(existingStore); + logger.LogTrace("creating pkcs12 store for working with the certificate"); + Pkcs12StoreBuilder pkcs12storeBuilder = new Pkcs12StoreBuilder(); + existingPKCS12Store = pkcs12storeBuilder.Build(); + existingPKCS12Store.Load(ms, existingStorePassword.ToCharArray()); + isPKCS12Format = true; } catch (Exception innerEx) { - logger.LogError(innerEx, $"Unable to load store as JKS or PKCS12: {innerEx.Message}"); + logger.LogError(innerEx, $"unable to load store as JKS or PKCS12: {innerEx.Message}"); isPKCS12Format = false; throw; } @@ -170,14 +138,14 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC if ((!isPKCS12Format && existingJksStore.ContainsAlias(alias)) || (isPKCS12Format && existingPKCS12Store.ContainsAlias(alias))) { // If alias exists, delete it from existingJksStore - logger.LogDebug("Alias '{Alias}' exists in existing JKS store, deleting it", alias); + logger.LogDebug($"alias '{alias}' exists in existing JKS store, deleting it", alias); if (isPKCS12Format) existingPKCS12Store.DeleteEntry(alias); else existingJksStore.DeleteEntry(alias); if (remove) { // If remove is true, save existingJksStore and return - logger.LogDebug("This is a removal operation, saving existing JKS store"); + logger.LogDebug("this is a removal operation, saving existing JKS store"); using (var mms = new MemoryStream()) { if (isPKCS12Format) @@ -189,7 +157,7 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC existingJksStore.Save(mms, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray()); } - logger.LogDebug("Returning existing JKS store"); + logger.LogDebug("returning existing JKS store"); return mms.ToArray(); } } @@ -197,7 +165,7 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC else if (remove) { // If alias does not exist and remove is true, return existingStore - logger.LogDebug("Alias '{Alias}' does not exist in existing JKS store and this is a removal operation, returning existing JKS store as-is", alias); + logger.LogDebug($"alias '{alias}' does not exist in existing JKS store and this is a removal operation, returning existing JKS store as-is", alias); using (var mms = new MemoryStream()) { if (isPKCS12Format) @@ -226,10 +194,8 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC try { logger.LogDebug("Loading new Pkcs12Store from newPkcs12Bytes"); - using (var pkcs12Ms = new MemoryStream(newCertBytes)) - { - newCert.Load(pkcs12Ms, string.IsNullOrEmpty(newCertPassword) ? Array.Empty() : newCertPassword.ToCharArray()); - } + using var pkcs12Ms = new MemoryStream(newCertBytes); + newCert.Load(pkcs12Ms, string.IsNullOrEmpty(newCertPassword) ? Array.Empty() : newCertPassword.ToCharArray()); } catch (Exception) { @@ -241,7 +207,7 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC // create new Pkcs12Store from certificate storeBuilder = new Pkcs12StoreBuilder(); newCert = storeBuilder.Build(); - logger.LogDebug("Setting certificate entry in new Pkcs12Store as alias '{Alias}'", alias); + logger.LogDebug($"Setting certificate entry in new Pkcs12Store as alias '{alias}'", alias); newCert.SetCertificateEntry(alias, new X509CertificateEntry(certificate)); } @@ -250,21 +216,21 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC logger.LogDebug("Iterating through new Pkcs12Store aliases"); foreach (var al in newCert.Aliases) { - logger.LogTrace("Alias: {Alias}", al); + logger.LogTrace($"alias: {al}", al); if (newCert.IsKeyEntry(al)) { - logger.LogDebug("Alias '{Alias}' is a key entry, getting key entry and certificate chain", al); + logger.LogDebug($"alias '{al}' is a key entry, getting key entry and certificate chain", al); var keyEntry = newCert.GetKey(al); - logger.LogDebug("Getting certificate chain for alias '{Alias}'", al); + logger.LogDebug($"getting certificate chain for alias '{al}'", al); var certificateChain = newCert.GetCertificateChain(al); - logger.LogDebug("Creating certificate list from certificate chain"); + logger.LogDebug("creating certificate list from certificate chain.."); var certificates = certificateChain.Select(certificateEntry => certificateEntry.Certificate).ToList(); if (createdNewStore) { // If createdNewStore is true, create a new store - logger.LogDebug("Created new JKS store, setting key entry for alias '{Alias}'", al); + logger.LogDebug($"created new JKS store, setting key entry for alias '{al}'", al); newJksStore.SetKeyEntry(alias, keyEntry.Key, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), @@ -274,14 +240,14 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC { // If createdNewStore is false, add to existingJksStore // check if alias exists in existingJksStore - if ((isPKCS12Format && existingPKCS12Store.ContainsAlias(alias)) || (!isPKCS12Format && existingJksStore.ContainsAlias(alias))) + if ((isPKCS12Format && existingPKCS12Store.ContainsAlias(al)) || (!isPKCS12Format && existingJksStore.ContainsAlias(alias))) { // If alias exists, delete it from existingJksStore - logger.LogDebug("Alias '{Alias}' exists in existing JKS store, deleting it", alias); - if (isPKCS12Format) existingPKCS12Store.DeleteEntry(alias); else existingJksStore.DeleteEntry(alias); + logger.LogDebug($"alias '{al}' exists in existing JKS store, deleting it", alias); + if (isPKCS12Format) existingPKCS12Store.DeleteEntry(al); else existingJksStore.DeleteEntry(alias); } - logger.LogDebug("Setting key entry for alias '{Alias}'", alias); + logger.LogDebug($"setting key entry for alias '{alias}'", alias); if (!isPKCS12Format) { @@ -308,15 +274,13 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC } } - using (var outStream = new MemoryStream()) - { - logger.LogDebug("Saving existing JKS store to outStream"); - if (isPKCS12Format) existingPKCS12Store.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); - else existingJksStore.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray()); + using var outStream = new MemoryStream(); + logger.LogDebug("Saving existing JKS store to outStream"); + if (isPKCS12Format) existingPKCS12Store.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); + else existingJksStore.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray()); - logger.LogDebug("Returning updated JKS store as byte[]"); - return outStream.ToArray(); - } + logger.LogDebug("Returning updated JKS store as byte[]"); + return outStream.ToArray(); } private Pkcs12Store JksToPkcs12Store(byte[] storeContents, string storePassword) @@ -332,14 +296,12 @@ private Pkcs12Store JksToPkcs12Store(byte[] storeContents, string storePassword) // first, see if it is already in the pkcs12 format try { - using (var ms = new MemoryStream(storeContents)) - { - logger.LogTrace("creating pkcs12 store for working with the certificate."); - Pkcs12StoreBuilder pkcs12storeBuilder = new Pkcs12StoreBuilder(); - existingPKCS12Store = pkcs12storeBuilder.Build(); - existingPKCS12Store.Load(ms, storePassword.ToCharArray()); - isPKCS12Format = true; - } + using var ms = new MemoryStream(storeContents); + logger.LogTrace("creating pkcs12 store for working with the certificate."); + Pkcs12StoreBuilder pkcs12storeBuilder = new Pkcs12StoreBuilder(); + existingPKCS12Store = pkcs12storeBuilder.Build(); + existingPKCS12Store.Load(ms, storePassword.ToCharArray()); + isPKCS12Format = true; } catch (Exception) { @@ -348,10 +310,8 @@ private Pkcs12Store JksToPkcs12Store(byte[] storeContents, string storePassword) try { - using (var ms = new MemoryStream(storeContents)) - { - jksStore.Load(ms, string.IsNullOrEmpty(storePassword) ? new char[0] : storePassword.ToCharArray()); - } + using var ms = new MemoryStream(storeContents); + jksStore.Load(ms, string.IsNullOrEmpty(storePassword) ? new char[0] : storePassword.ToCharArray()); } catch (Exception innerEx) { diff --git a/hashicorp-vault-orchestrator/FileStores/PfxFileStore.cs b/hashicorp-vault-orchestrator/FileStores/PfxFileStore.cs index ed60036..1def118 100644 --- a/hashicorp-vault-orchestrator/FileStores/PfxFileStore.cs +++ b/hashicorp-vault-orchestrator/FileStores/PfxFileStore.cs @@ -1,15 +1,15 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Security.Cryptography.X509Certificates; using Keyfactor.Logging; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; @@ -19,10 +19,8 @@ namespace Keyfactor.Extensions.Orchestrator.HashicorpVault.FileStores { - public class PfxFileStore : IFileStore + public class PfxFileStore : FileStoreBase, IFileStore { - internal protected ILogger logger { get; set; } - public PfxFileStore() { logger = LogHandler.GetClassLogger(); @@ -30,13 +28,12 @@ public PfxFileStore() public byte[] CreateFileStore(string password) { - Pkcs12Store newStore = null; - using (var outstream = new MemoryStream()) - { - logger.LogDebug("Created new PFX store, saving it to outStream"); - newStore.Save(outstream, password.ToCharArray(), new SecureRandom()); - return outstream.ToArray(); - } + Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder(); + Pkcs12Store newStore = storeBuilder.Build(); + using var outstream = new MemoryStream(); + logger.LogDebug("Created new PFX store, saving it to outStream"); + newStore.Save(outstream, password.ToCharArray(), new SecureRandom()); + return outstream.ToArray(); } public string AddCertificate(string alias, string pfxPassword, string entryContents, bool includeChain, string storeFileContent, string passphrase) @@ -45,8 +42,6 @@ public string AddCertificate(string alias, string pfxPassword, string entryConte logger.LogTrace("converting base64 encoded PFX store to binary."); var pfxBytes = Convert.FromBase64String(storeFileContent); - - var newCertBytes = Convert.FromBase64String(entryContents); logger.LogTrace("adding the new certificate, and getting the new PFX store bytes."); @@ -66,44 +61,22 @@ public string RemoveCertificate(string alias, string passphrase, string storeFil return Convert.ToBase64String(newPfxStoreBytes); } - public IEnumerable GetInventory(Dictionary certFields) - { + public IEnumerable GetInventory(string base64encodedCert, string passphrase) + { logger.MethodEntry(); - // certFields should contain two entries. The certificate with the "_pfx" suffix, and "passphrase" - string password; - string base64encodedCert; - var certs = new List(); - - - var certKey = certFields.Keys.First(f => f.Contains(StoreFileExtensions.HCVKVPFX)); - if (certKey == null) - { - throw new Exception($"No entry with extension '{StoreFileExtensions.HCVKVPFX}' found"); - } - else - { - base64encodedCert = certFields[certKey].ToString(); - } - - if (certFields.TryGetValue("passphrase", out object filePasswordObj)) - { - password = filePasswordObj.ToString(); - } - else - { - throw new Exception($"No password entry found for PFX store '{certKey}'."); - } - logger.LogTrace("converting base64 encoded cert to binary format."); + var certs = new List(); var pfxBytes = Convert.FromBase64String(base64encodedCert); + Pkcs12Store p; + using (var pfxBytesMemoryStream = new MemoryStream(pfxBytes)) { logger.LogTrace("creating pkcs12 store for working with the certificate."); Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder(); p = storeBuilder.Build(); - p.Load(pfxBytesMemoryStream, password.ToCharArray()); + p.Load(pfxBytesMemoryStream, passphrase.ToCharArray()); } certs = CertUtility.CurrentInventoryFromPkcs12(p); @@ -126,46 +99,40 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC try { - using (var pfxBytesMemoryStream = new MemoryStream(existingStore)) - { - logger.LogTrace("creating pkcs12 store for working with the certificate."); - Pkcs12StoreBuilder sb = new Pkcs12StoreBuilder(); - existingPfxStore = sb.Build(); - existingPfxStore.Load(pfxBytesMemoryStream, existingStorePassword.ToCharArray()); - } + using var pfxBytesMemoryStream = new MemoryStream(existingStore); + logger.LogTrace("creating pkcs12 store for working with the certificate."); + var sb = new Pkcs12StoreBuilder(); + existingPfxStore = sb.Build(); + existingPfxStore.Load(pfxBytesMemoryStream, existingStorePassword.ToCharArray()); } catch (Exception ex) { - logger.LogError(ex, $"Error loading existing PFX store: {ex.Message}"); + logger.LogError($"error loading existing PFX store: {ex.Message}"); } if (existingPfxStore.ContainsAlias(alias)) { // If alias exists, delete it from existingJksStore - logger.LogDebug($"Alias '{alias}' exists in existing PFX store, deleting it"); + logger.LogDebug($"alias '{alias}' exists in existing PFX store, deleting it"); existingPfxStore.DeleteEntry(alias); if (remove) { // If remove is true, save existingJksStore and return - logger.LogDebug("This is a removal operation, saving existing PFX store"); - using (var mms = new MemoryStream()) - { - existingPfxStore.Save(mms, - string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); - logger.LogDebug("Returning existing PFX store"); - return mms.ToArray(); - } + logger.LogDebug("this is a removal operation, saving existing PFX store"); + using var mms = new MemoryStream(); + existingPfxStore.Save(mms, + string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); + logger.LogDebug("returning existing PFX store"); + return mms.ToArray(); } } else if (remove) { // If alias does not exist and remove is true, return existingStore - logger.LogDebug($"Alias '{alias}' does not exist in existing PFX store and this is a removal operation, returning existing PFX store as-is"); - using (var mms = new MemoryStream()) - { - existingPfxStore.Save(mms, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); - return mms.ToArray(); - } + logger.LogDebug($"alias '{alias}' does not exist in existing PFX store and this is a removal operation, returning existing PFX store as-is"); + using var mms = new MemoryStream(); + existingPfxStore.Save(mms, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); + return mms.ToArray(); } // adding the new certificate @@ -177,10 +144,8 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC try { logger.LogDebug("Loading new certificate as pfx/pkcs12 from newPkcs12Bytes"); - using (var pkcs12Ms = new MemoryStream(newCertBytes)) - { - newCert.Load(pkcs12Ms, string.IsNullOrEmpty(newCertPassword) ? Array.Empty() : newCertPassword.ToCharArray()); - } + using var pkcs12Ms = new MemoryStream(newCertBytes); + newCert.Load(pkcs12Ms, string.IsNullOrEmpty(newCertPassword) ? Array.Empty() : newCertPassword.ToCharArray()); } catch (Exception) { @@ -196,7 +161,6 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC newCert.SetCertificateEntry(alias, new X509CertificateEntry(certificate)); } - // Iterate through newCert aliases. logger.LogDebug("Iterating through new Pkcs12Store aliases"); foreach (var al in newCert.Aliases) @@ -204,12 +168,12 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC logger.LogTrace($"Alias: {al}"); if (newCert.IsKeyEntry(al)) { - logger.LogDebug($"Alias '{al}' is a key entry, getting key entry and certificate chain"); + logger.LogDebug($"alias '{al}' is a key entry, getting key entry and certificate chain"); var keyEntry = newCert.GetKey(al); - logger.LogDebug($"Getting certificate chain for alias '{al}'"); + logger.LogDebug($"getting certificate chain for alias '{al}'"); var certificateChain = newCert.GetCertificateChain(al); - logger.LogDebug("Creating certificate list from certificate chain"); + logger.LogDebug("creating certificate list from certificate chain"); var certificates = certificateChain.ToList(); // If createdNewStore is false, add to existingJksStore @@ -217,30 +181,28 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC if (existingPfxStore.ContainsAlias(alias)) { // If alias exists, delete it from existingJksStore - logger.LogDebug($"Alias '{alias}' exists in existing PFX store, deleting it"); - existingPfxStore.DeleteEntry(alias); + logger.LogDebug($"alias '{al}' exists in existing PFX store, deleting it"); + existingPfxStore.DeleteEntry(al); } - logger.LogDebug($"Setting key entry for alias '{alias}'"); + logger.LogDebug($"setting key entry for alias '{alias}'"); existingPfxStore.SetKeyEntry(alias, keyEntry, certificates.ToArray()); } else { - logger.LogDebug($"Setting certificate with alias '{alias}' for existing PFX store"); - existingPfxStore.SetCertificateEntry(alias, newCert.GetCertificate(alias)); + logger.LogDebug($"setting certificate with alias '{al}' for existing PFX store"); + existingPfxStore.SetCertificateEntry(al, newCert.GetCertificate(al)); } } - using (var outStream = new MemoryStream()) - { - logger.LogDebug("Saving existing PFX store to outStream"); - existingPfxStore.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); + using var outStream = new MemoryStream(); + logger.LogDebug("Saving existing PFX store to outStream"); + existingPfxStore.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); - logger.LogDebug("Returning updated PFX store as byte[]"); - return outStream.ToArray(); - } + logger.LogDebug("Returning updated PFX store as byte[]"); + return outStream.ToArray(); } } } diff --git a/hashicorp-vault-orchestrator/FileStores/Pkcs12FileStore.cs b/hashicorp-vault-orchestrator/FileStores/Pkcs12FileStore.cs index 5c90705..acb1383 100644 --- a/hashicorp-vault-orchestrator/FileStores/Pkcs12FileStore.cs +++ b/hashicorp-vault-orchestrator/FileStores/Pkcs12FileStore.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; @@ -29,7 +30,8 @@ public Pkcs12FileStore() public byte[] CreateFileStore(string password) { - Pkcs12Store newStore = null; + Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder(); + Pkcs12Store newStore = storeBuilder.Build(); using (var outstream = new MemoryStream()) { logger.LogDebug("Created new PKCS12 store, saving it to outStream"); @@ -66,37 +68,15 @@ public string RemoveCertificate(string alias, string passphrase, string storeFil return Convert.ToBase64String(newPkcs12StoreBytes); } - public IEnumerable GetInventory(Dictionary certFields) + public IEnumerable GetInventory(string base64encodedCert, string passphrase) { logger.MethodEntry(); // certFields should contain two entries. The certificate with the "_pfx" suffix, and "passphrase" - string password; - string base64encodedCert; var certs = new List(); try { - var certKey = certFields.Keys.First(f => f.Contains(StoreFileExtensions.HCVKVPKCS12)); - - if (certKey == null) - { - throw new Exception($"No entry with extension '{StoreFileExtensions.HCVKVPKCS12}' found"); - } - else - { - base64encodedCert = certFields[certKey].ToString(); - } - - if (certFields.TryGetValue("passphrase", out object filePasswordObj)) - { - password = filePasswordObj.ToString(); - } - else - { - throw new Exception($"No password entry found for PKCS12 store '{certKey}'."); - } - // certFields should contain two entries. The certificate with the "p12-contents" suffix, and "password" logger.LogTrace("converting base64 encoded cert to binary."); var bytes = Convert.FromBase64String(base64encodedCert); @@ -108,7 +88,7 @@ public IEnumerable GetInventory(Dictionary logger.LogTrace("creating pkcs12 store for working with the certificate."); Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder(); pkcs12Store = storeBuilder.Build(); - pkcs12Store.Load(stream, password.ToCharArray()); + pkcs12Store.Load(stream, passphrase.ToCharArray()); } certs = CertUtility.CurrentInventoryFromPkcs12(pkcs12Store); logger.MethodExit(); @@ -116,7 +96,7 @@ public IEnumerable GetInventory(Dictionary } catch (Exception ex) { - logger.LogError("Unable to read PKCS12 file.", ex); + logger.LogError(ex, "Unable to read PKCS12 file."); throw; } } @@ -153,12 +133,12 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC if (existingPkcs12Store.ContainsAlias(alias)) { - // If alias exists, delete it from existingJksStore + // If alias exists, delete it from existing PKCS12 store logger.LogDebug($"Alias '{alias}' exists in existing PKCS12 store, deleting it"); existingPkcs12Store.DeleteEntry(alias); if (remove) { - // If remove is true, save existingJksStore and return + // If remove is true, save existing P12 store and return logger.LogDebug("This is a removal operation, saving existing PKCS12 store"); using (var mms = new MemoryStream()) { @@ -173,25 +153,23 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC { // If alias does not exist and remove is true, return existingStore logger.LogDebug($"Alias '{alias}' does not exist in existing PKCS12 store and this is a removal operation, returning existing PKCS12 store as-is"); - using (var mms = new MemoryStream()) - { - existingPkcs12Store.Save(mms, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); - return mms.ToArray(); - } + using var mms = new MemoryStream(); + existingPkcs12Store.Save(mms, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); + return mms.ToArray(); } // adding the new certificate // Create new Pkcs12Store from newPkcs12Bytes var storeBuilder = new Pkcs12StoreBuilder(); - var newCert = storeBuilder.Build(); + var newCertStore = storeBuilder.Build(); try { logger.LogDebug("Loading new certificate as pfx/pkcs12 from newPkcs12Bytes"); using (var pkcs12Ms = new MemoryStream(newCertBytes)) { - newCert.Load(pkcs12Ms, string.IsNullOrEmpty(newCertPassword) ? Array.Empty() : newCertPassword.ToCharArray()); + newCertStore.Load(pkcs12Ms, string.IsNullOrEmpty(newCertPassword) ? Array.Empty() : newCertPassword.ToCharArray()); } } catch (Exception) @@ -203,56 +181,66 @@ private byte[] AddOrRemoveCert(string alias, string newCertPassword, byte[] newC logger.LogDebug("Creating new Pkcs12Store from certificate"); // create new Pkcs12Store from certificate storeBuilder = new Pkcs12StoreBuilder(); - newCert = storeBuilder.Build(); + newCertStore = storeBuilder.Build(); logger.LogDebug($"Setting certificate entry in new Pkcs12Store as alias '{alias}'"); - newCert.SetCertificateEntry(alias, new X509CertificateEntry(certificate)); + newCertStore.SetCertificateEntry(alias, new X509CertificateEntry(certificate)); } - // Iterate through newCert aliases. + // Iterate through newCertStore aliases. logger.LogDebug("Iterating through new Pkcs12Store aliases"); - foreach (var al in newCert.Aliases) + foreach (var al in newCertStore.Aliases) { - logger.LogTrace($"Alias: {al}"); - if (newCert.IsKeyEntry(al)) + logger.LogTrace($"alias: {al}"); + + if (newCertStore.IsCertificateEntry(al)) { + logger.LogTrace("is a certificate entry.."); + + if (existingPkcs12Store.ContainsAlias(al)) + { + // it does.. so we remove it + logger.LogTrace("that already exists in the cert store, removing.."); + + existingPkcs12Store.DeleteEntry(al); + + logger.LogTrace("and now replacing it with the new one.."); + existingPkcs12Store.SetCertificateEntry(al, newCertStore.GetCertificate(al)); + } + } + else if (newCertStore.IsKeyEntry(al)) + { + // it's the private key; get the chain and set it as the key entry for the new cert logger.LogDebug($"Alias '{al}' is a key entry, getting key entry and certificate chain"); - var keyEntry = newCert.GetKey(al); + var keyEntry = newCertStore.GetKey(al); logger.LogDebug($"Getting certificate chain for alias '{al}'"); - var certificateChain = newCert.GetCertificateChain(al); + var certificateChain = newCertStore.GetCertificateChain(al); logger.LogDebug("Creating certificate list from certificate chain"); var certificates = certificateChain.ToList(); - // If createdNewStore is false, add to existingJksStore // check if alias exists in existingJksStore - if (existingPkcs12Store.ContainsAlias(alias)) + if (existingPkcs12Store.ContainsAlias(al)) { - // If alias exists, delete it from existingJksStore - logger.LogDebug($"Alias '{alias}' exists in existing PKCS12 store, deleting it"); - existingPkcs12Store.DeleteEntry(alias); + // If alias al exists, delete it from existingJksStore + logger.LogDebug($"Alias '{al}' exists in existing PKCS12 store, deleting it"); + existingPkcs12Store.DeleteEntry(al); } - logger.LogDebug($"Setting key entry for alias '{alias}'"); + // we found the key, setting the key for the new cert alias in the existing cert store + logger.LogDebug($"Setting key entry with alias '{al}' for cert with alias '{alias}' in the existing store.."); existingPkcs12Store.SetKeyEntry(alias, keyEntry, certificates.ToArray()); } - else - { - logger.LogDebug($"Setting certificate with alias '{alias}' for existing PKCS12 store"); - existingPkcs12Store.SetCertificateEntry(alias, newCert.GetCertificate(alias)); - } } - using (var outStream = new MemoryStream()) - { - logger.LogDebug("Saving existing PKCS12 store to outStream"); - existingPkcs12Store.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); + using var outStream = new MemoryStream(); + logger.LogDebug("Saving existing PKCS12 store to outStream"); + existingPkcs12Store.Save(outStream, string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(), new SecureRandom()); - logger.LogDebug("Returning updated PKCS12 store as byte[]"); - return outStream.ToArray(); - } + logger.LogDebug("Returning updated PKCS12 store as byte[]"); + return outStream.ToArray(); } } } diff --git a/hashicorp-vault-orchestrator/HcvKeyValueClient.cs b/hashicorp-vault-orchestrator/HcvKeyValueClient.cs index 931e8c5..ced1817 100644 --- a/hashicorp-vault-orchestrator/HcvKeyValueClient.cs +++ b/hashicorp-vault-orchestrator/HcvKeyValueClient.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; @@ -16,6 +17,7 @@ using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Pkcs; @@ -36,13 +38,16 @@ public class HcvKeyValueClient : IHashiClient private ILogger logger = LogHandler.GetClassLogger(); - private string _storePath { get; set; } + private string _certPath { get; set; } + private string _passphrasePath { get; set; } + private string _certPropName { get; set; } + private string _passphrasePropName { get; set; } private string _mountPoint { get; set; } private bool _subfolderInventory { get; set; } private string _storeType { get; set; } private string _namespace { get; set; } - public HcvKeyValueClient(string vaultToken, string serverUrl, string mountPoint, string ns, string storePath, string storeType, bool SubfolderInventory = false) + public HcvKeyValueClient(string vaultToken, string serverUrl, string mountPoint, string ns, string storeType, string certPath, string certPropName, string passphrasePath, string passphrasePropName, bool SubfolderInventory = false) { // Initialize one of the several auth methods. IAuthMethodInfo authMethod = new TokenAuthMethodInfo(vaultToken); @@ -50,15 +55,13 @@ public HcvKeyValueClient(string vaultToken, string serverUrl, string mountPoint, // Initialize settings. You can also set proxies, custom delegates etc. here. var clientSettings = new VaultClientSettings(serverUrl, authMethod) { Namespace = _namespace, UseVaultTokenHeaderInsteadOfAuthorizationHeader = true }; - _vaultClient = new VaultClient(clientSettings); - - //logger.LogTrace("----- vault client has been initialized with these settings ------ "); - //logger.LogTrace($"url with port: {_vaultClient.Settings.VaultServerUriWithPort}"); - //logger.LogTrace($"namespace: {_vaultClient.Settings.Namespace}"); - //logger.LogTrace($"use token header?: {_vaultClient.Settings.UseVaultTokenHeaderInsteadOfAuthorizationHeader}"); + _vaultClient = new VaultClient(clientSettings); _mountPoint = mountPoint; - _storePath = (!string.IsNullOrEmpty(storePath) && !storePath.StartsWith("/")) ? "/" + storePath.Trim() : storePath?.Trim(); + _certPath = (!string.IsNullOrEmpty(certPath) && !certPath.StartsWith("/")) ? "/" + certPath.Trim() : certPath?.Trim(); + _passphrasePath = (!string.IsNullOrEmpty(passphrasePath) && !passphrasePath.StartsWith("/")) ? "/" + passphrasePath.Trim() : passphrasePath?.Trim(); + _certPropName = certPropName; + _passphrasePropName = passphrasePropName; _subfolderInventory = SubfolderInventory; _storeType = storeType?.Split('.')[1]; } @@ -79,7 +82,7 @@ public async Task CreateCertStore() } catch (Exception ex) { - logger.LogError(ex, "Error when adding the new certificate."); + logger.LogError($"Error when adding the new certificate: {LogHandler.FlattenException(ex)}"); throw; } logger.MethodExit(); @@ -87,11 +90,17 @@ public async Task CreateCertStore() private async Task CreateFileStore() { + logger.MethodEntry(); + IFileStore fileStore; - var parentPath = _storePath.Substring(0, _storePath.LastIndexOf("/")); - logger.LogTrace($"parent path = {parentPath}"); - var entryName = _storePath.Substring(_storePath.LastIndexOf("/")); - entryName = entryName.TrimStart('/'); + + (var certParentPath, var certSecretName, var passphraseParentPath, var passphraseSecretName) = GetSecretPaths(); + + var certSecretIsJSON = !string.IsNullOrEmpty(_certPropName); + if (certSecretIsJSON) logger.LogTrace($"the certificate data will be stored as a JSON object with the base64 encoded cert stored in the property '{_certPropName}'"); + + var passphraseSecretIsJSON = !string.IsNullOrEmpty(_passphrasePropName); + if (passphraseSecretIsJSON) logger.LogTrace($"the passphrase secret will be stored as a JSON object with the passphrase in the property '{_passphrasePropName}'"); switch (_storeType) { @@ -123,19 +132,69 @@ private async Task CreateFileStore() { VaultClient.V1.Auth.ResetVaultToken(); - var newData = new Dictionary { { entryName, Convert.ToBase64String(newStoreBytes) }, { "passphrase", passphrase } }; + // create the cert secret + Dictionary certSecretContent; + var pathToWriteCert = string.Empty; + + + // the content will be either the base64 encoded cert, or a json object with a property containing the base64encoded cert + if (certSecretIsJSON) + { + // this means the cert should be stored as a JSON object with property _certPropName, as opposed to a raw base64 string. + certSecretContent = new Dictionary { { _certPropName, Convert.ToBase64String(newStoreBytes) } }; // the content includes the property name + pathToWriteCert = certParentPath + certSecretName; // we write to the secret + } + else + { + certSecretContent = new Dictionary { { certSecretName, Convert.ToBase64String(newStoreBytes) } }; // the content includes the secret name.. + pathToWriteCert = certParentPath; // we write to the parent path + } + + logger.LogTrace($"we will send the request to write the cert secret at the path {pathToWriteCert}, keyed by the secret or property name: '{certSecretContent.Keys.First()}'"); + + // write the certificate secret + + logger.LogTrace($"sending request to write new cert store secret"); + var res = await VaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(pathToWriteCert, certSecretContent, null, _mountPoint); + logger.LogTrace($"request to write certificate secret was successful. secret created time: {res.Data?.CreatedTime}"); + + // create the passphrase secret + + Dictionary passphraseSecretContent; + var pathToWritePassphrase = string.Empty; + + if (passphraseSecretIsJSON) + { + passphraseSecretContent = new Dictionary { { _passphrasePropName, passphrase } }; + pathToWritePassphrase = passphraseParentPath + passphraseSecretName; + } + else + { + passphraseSecretContent = new Dictionary { { passphraseSecretName, passphrase } }; + pathToWritePassphrase = passphraseParentPath; + } + + logger.LogTrace($"we will send the request to write the passphrase secret at the path {pathToWritePassphrase}, keyed by the secret or property name: '{passphraseSecretContent.Keys.First()}'"); + + // write the passphrase secret + var req = new PatchSecretDataRequest(); + req.Data = passphraseSecretContent; + + logger.LogTrace($"sending request to write new cert store passphrase"); + res = await VaultClient.V1.Secrets.KeyValue.V2.PatchSecretAsync(pathToWritePassphrase, req, _mountPoint); + logger.LogTrace($"request to write passphrase secret was successful. secret created time: {res.Data?.CreatedTime}"); - await VaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(parentPath, newData, null, _mountPoint); } catch (Exception ex) { - logger.LogError(ex, $"Error writing cert to Vault: {ex.Message}"); + logger.LogError($"Error writing cert to Vault: {ex.Message}"); throw; } - } private async Task CreatePemStore() { + logger.MethodEntry(); + //without a certificate, the only thing to do is create the secret path in Vault with empty values var newData = new Dictionary { { "certificate", string.Empty }, { "private_key", string.Empty } }; @@ -143,16 +202,16 @@ private async Task CreatePemStore() { if (_mountPoint == null) { - await VaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(_storePath, newData); + await VaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(_certPath, newData); } else { - await VaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(_storePath, newData, mountPoint: _mountPoint); + await VaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(_certPath, newData, mountPoint: _mountPoint); } } catch (Exception ex) { - logger.LogError(ex, $"Error creating the PEM certificate store at path {_storePath}"); + logger.LogError(ex, $"Error creating the PEM certificate store at path {_certPath}"); throw; } } @@ -164,7 +223,7 @@ public async Task GetCertificateFromPemStore(string key) Dictionary certData = new Dictionary(); Secret res; - var fullPath = _storePath + key; + var fullPath = _certPath + key; try @@ -191,7 +250,7 @@ public async Task GetCertificateFromPemStore(string key) string certificate = null; string privateKey = null; - //Validates if the "certificate" and "private_key" keys exist in certData + //Validates if the "certificate" and "private_key" keys exist in certFileObj if (certData.TryGetValue(StoreFileExtensions.HCVKVPEM, out object publicKeyObj)) { certificate = publicKeyObj.ToString(); @@ -216,6 +275,7 @@ public async Task GetCertificateFromPemStore(string key) logger.LogWarning($"The secret entry located at `{fullPath}` is missing `{missing}` but has `{exists}`. Inventory will continue."); throw new PemException($"The secret entry located at `{fullPath}` is missing `{missing}` but has `{exists}`"); } + return null; } @@ -261,7 +321,7 @@ public async Task GetCertificateFromPemStore(string key) // there are 4 store types that use the KV secrets engine. HCVKVPEM uses the folder as the store path. The others (KCVKVJKS,HCVKVPKCS12,HCVKVPFX) use the full file path. - storePath = storePath ?? _storePath; + storePath = storePath ?? _certPath; if (!storePath.StartsWith("/")) storePath = "/" + storePath; if (!storePath.EndsWith("/")) storePath = storePath + "/"; @@ -341,7 +401,7 @@ public async Task GetCertificateFromPemStore(string key) } - public async Task PutCertificate(string certName, string contents, string pfxPassword, bool includeChain) + public async Task PutCertificate(string certName, string contents, string pfxPassword, string certPath, string certPropName, string keyPath, string keyPropName, bool includeChain) { logger.MethodEntry(); try @@ -375,7 +435,7 @@ private async Task PutCertificateIntoPemStore(string certName, string contents, p.Load(pfxBytesMemoryStream, pfxPassword.ToCharArray()); } - // Extract private key + // Extract private secretName string alias; string privateKeyString; using (var memoryStream = new MemoryStream()) @@ -391,7 +451,7 @@ private async Task PutCertificateIntoPemStore(string certName, string contents, logger.LogTrace($"publicKey = {publicKey}"); var KeyEntry = p.GetKey(alias); - if (KeyEntry == null) throw new Exception("Unable to retrieve private key"); + if (KeyEntry == null) throw new Exception("Unable to retrieve private secretName"); var privateKey = KeyEntry.Key; var keyPair = new AsymmetricCipherKeyPair(publicKey, privateKey); @@ -452,7 +512,7 @@ private async Task PutCertificateIntoPemStore(string certName, string contents, logger.LogTrace("writing secret to vault."); VaultClient.V1.Auth.ResetVaultToken(); - var fullPath = _storePath + certName; + var fullPath = _certPath + certName; await VaultClient.V1.Secrets.KeyValue.V2.WriteSecretAsync(fullPath, certDict, mountPoint: _mountPoint); } @@ -464,15 +524,25 @@ private async Task PutCertificateIntoPemStore(string certName, string contents, logger.MethodExit(); } - private async Task PutCertificateIntoFileStore(string certName, string contents, string pfxPassword, bool includeChain) + private async Task PutCertificateIntoFileStore(string newCertName, string contents, string pfxPassword, bool includeChain) { logger.MethodEntry(); IFileStore fileStore; - var parentPath = _storePath.Substring(0, _storePath.LastIndexOf("/")); - logger.LogTrace($"parent path = {parentPath}"); - Secret res; - Dictionary certData; + + (var certParentPath, var certSecretName, var passphraseParentPath, var passphraseSecretName) = GetSecretPaths(); + + (var certificate, var passphrase) = await GetCertificateAndPassphrase(); + + var certSecretIsJSON = !string.IsNullOrEmpty(_certPropName); + + if (certSecretIsJSON) logger.LogTrace($"the certificate data will be stored at '{_certPath}' as a JSON object with the base64 encoded cert stored in the property '{_certPropName}'"); + else logger.LogTrace($"the certificate secret will be stored at '{_certPath}' with the contents being the base64 encoded certificate."); + + var passphraseSecretIsJSON = !string.IsNullOrEmpty(_passphrasePropName); + + if (passphraseSecretIsJSON) logger.LogTrace($"the passphrase secret will be stored at '{passphraseParentPath}/{passphraseSecretName}' as a JSON object with the passphrase in the property '{_passphrasePropName}'"); + else logger.LogTrace($"the passphrase secret will be stored at '{passphraseParentPath}/{passphraseSecretName}' as a string containing the passphrase for the certificate store"); switch (_storeType) { @@ -494,63 +564,72 @@ private async Task PutCertificateIntoFileStore(string certName, string contents, try { - // first get entry contents and passphrase - logger.LogTrace("getting all secrets in the parent container for the store."); + logger.LogTrace("got passphrase and certificate store secrets from vault."); + logger.LogTrace("calling method to add certificate to store file."); - res = await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(parentPath, mountPoint: _mountPoint); + // get new store entry + var newCertFileStore = fileStore.AddCertificate(newCertName, pfxPassword, contents, includeChain, certificate, passphrase); - certData = (Dictionary)res.Data.Data; - logger.LogTrace($"got secret data.."); + logger.LogTrace("got new store file."); - string certificate = null; - string passphrase = null; + // write new store entry + try + { + logger.LogTrace("writing file store with new certificate to vault."); + VaultClient.V1.Auth.ResetVaultToken(); - //Validates if the "certificate" and "private_key" keys exist in certData + // if the certificate and/or passphrase is stored as a property in a JSON secret.. + // then we need to write the full path to the secret, and pass a dictionary of the object for the PATCH operation - var key = _storePath.Substring(_storePath.LastIndexOf("/")); - key = key.TrimStart('/'); + // if the cert or passphrase is the full contents of the secret.. + // then we need to write to the _parent_ path, a dictionary with a key of the secret name and value of the contents - logger.LogTrace($"getting the contents of {key}"); + // first write the certificate + var newCertSecretData = new Dictionary(); + var newPassphraseSecretData = new Dictionary(); + var certPathToWrite = string.Empty; + var passphrasePathToWrite = string.Empty; - if (!certData.TryGetValue(key, out object certFileObj)) - { - throw new DirectoryNotFoundException($"entry named {key} not found at {parentPath}"); - } - certificate = certFileObj.ToString(); + logger.LogTrace($"creating the patch request for the certificate secret..."); + if (certSecretIsJSON) + { + // we will create a dictionary to represent the secret itself.. + newCertSecretData = new Dictionary { { _certPropName, newCertFileStore } }; - if (!certData.TryGetValue("passphrase", out object passphraseObj)) - { - throw new DirectoryNotFoundException($"no passphrase entry found at {parentPath}"); - } - passphrase = passphraseObj.ToString(); + // and write it to the full path of the secret + certPathToWrite = certParentPath + "/" + certSecretName; + } + else + { + // we will create a dictionary to represent the contents of the parent path + newCertSecretData = new Dictionary { { certSecretName, newCertFileStore } }; - logger.LogTrace("got passphrase and certificate store secrets from vault."); + // and write it to the parent path of the secret + certPathToWrite = certParentPath; + } - logger.LogTrace("calling method to add certificate to store file."); - // get new store entry - var newEntry = fileStore.AddCertificate(certName, pfxPassword, contents, includeChain, certificate, passphrase); - logger.LogTrace("got new store file."); - // write new store entry - try - { - logger.LogTrace("writing file store with new certificate to vault."); - VaultClient.V1.Auth.ResetVaultToken(); + var patchCertReq = new PatchSecretDataRequest() { Data = newCertSecretData }; - var newData = new Dictionary { { key, newEntry } }; - var patchReq = new PatchSecretDataRequest() { Data = newData }; - logger.LogTrace($"patching {key} to path {parentPath} at mount point {_mountPoint}"); - await VaultClient.V1.Secrets.KeyValue.V2.PatchSecretAsync(parentPath, patchReq, _mountPoint); + // submit the patch request + logger.LogTrace($"patching {newCertSecretData.Keys.First()} to path {certPathToWrite} at mount point {_mountPoint}"); + await VaultClient.V1.Secrets.KeyValue.V2.PatchSecretAsync(certPathToWrite, patchCertReq, _mountPoint); + + logger.LogTrace("The certificate and passphrase have been successfully written to Vault."); + + // since this is an existing store, no update needs to be made to the passphrase } catch (Exception ex) { - logger.LogError(ex, $"Error writing cert to Vault: {ex.Message}"); + logger.LogError($"Error writing cert to Vault: {ex.Message}"); + logger.LogError($"{LogHandler.FlattenException(ex)}"); + throw; } } catch (Exception ex) { - logger.LogError(ex, $"Error adding certificate to {_storeType}: {ex.Message}"); + logger.LogError(ex, $"An error occurred when trying to update the secret for {_storeType}: {ex.Message}"); throw; } } @@ -583,7 +662,7 @@ public async Task RemoveCertificateFromFileStore(string certName) logger.MethodEntry(); IFileStore fileStore; - var parentPath = _storePath.Substring(0, _storePath.LastIndexOf("/")); + var parentPath = _certPath.Substring(0, _certPath.LastIndexOf("/")); logger.LogTrace($"parent path = {parentPath}"); Secret res; Dictionary certData; @@ -619,16 +698,16 @@ public async Task RemoveCertificateFromFileStore(string certName) string certStoreContents = null; string passphrase = null; - //Validates if the "certificate" and "private_key" keys exist in certData + //Validates if the "certificate" and "private_key" keys exist in certFileObj - var key = _storePath.Substring(_storePath.LastIndexOf("/")); - key = key.TrimStart('/'); + var secretName = _certPath.Substring(_certPath.LastIndexOf("/")); + secretName = secretName.TrimStart('/'); - logger.LogTrace($"getting the contents of {key}"); + logger.LogTrace($"getting the contents of {secretName}"); - if (!certData.TryGetValue(key, out object certFileObj)) + if (!certData.TryGetValue(secretName, out object certFileObj)) { - throw new DirectoryNotFoundException($"entry named {key} not found at {parentPath}"); + throw new DirectoryNotFoundException($"entry named {secretName} not found at {parentPath}"); } certStoreContents = certFileObj.ToString(); @@ -650,7 +729,7 @@ public async Task RemoveCertificateFromFileStore(string certName) logger.LogTrace("writing file store sans certificate to vault."); VaultClient.V1.Auth.ResetVaultToken(); - var newData = new Dictionary { { key, newEntry } }; + var newData = new Dictionary { { secretName, newEntry } }; var patchReq = new PatchSecretDataRequest() { Data = newData }; await VaultClient.V1.Secrets.KeyValue.V2.PatchSecretAsync(parentPath, patchReq, _mountPoint); } @@ -674,7 +753,7 @@ public async Task RemoveCertificateFromPemStore(string certName) try { - var fullPath = _storePath + certName; + var fullPath = _certPath + certName; await VaultClient.V1.Secrets.KeyValue.V2.DeleteSecretAsync(fullPath, _mountPoint); } catch (Exception ex) @@ -706,28 +785,28 @@ public async Task RemoveCertificateFromPemStore(string certName) List inventoryExceptions = new List(); //Grabs the list of subpaths to get certificates from, if SubFolder Inventory is turned on. - //Otherwise just define the single path _storePath + //Otherwise just define the single path _certPath logger.LogDebug($"SubInventoryEnabled: {_subfolderInventory}"); if (_subfolderInventory == true) { logger.LogTrace("getting all sub-paths for container"); - subPaths = await GetSubPaths(_storePath); - subPaths.Add(_storePath); + subPaths = await GetSubPaths(_certPath); + subPaths.Add(_certPath); } else { - subPaths.Add(_storePath); + subPaths.Add(_certPath); } - logger.LogTrace($"got all subpaths for container {_storePath}"); + logger.LogTrace($"got all subpaths for container {_certPath}"); logger.LogTrace($"subPaths = {string.Join(", ", subPaths)}"); foreach (var path in subPaths) { logger.LogTrace($"checking for entries at {path}"); - var relative_path = path.Substring(_storePath.Length); + var relative_path = path.Substring(_certPath.Length); try { @@ -768,32 +847,23 @@ public async Task RemoveCertificateFromPemStore(string certName) public async Task<(List, List)> GetCertificatesFromFileStore() { - Secret res; - - //file stores for JKS, PKCS12 and PFX will have a "passphrase" entry on the same level by convention. We'll need this in order to extract the certificates for inventory. - var pos = _storePath.LastIndexOf("/"); - var parentPath = _storePath.Substring(0, pos); - logger.LogTrace($"reading secrets at path {parentPath}, which should include the key and certificate for {_storePath}"); + logger.MethodEntry(); + Secret res = null; + string certStore = string.Empty; + string passphrase = string.Empty; try { - res = (await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(parentPath, mountPoint: _mountPoint)); + (certStore, passphrase) = await GetCertificateAndPassphrase(); } catch (Exception ex) { - var warning = $"Error getting {_storeType} certificate data from {parentPath}. Exception message: {ex.Message}"; - logger.LogError(ex, warning); + var warning = $"Vault returned an error when attempting to read the secret from {_certPath}. Exception message: {ex.Message}"; + logger.LogError(LogHandler.FlattenException(ex)); + res?.Warnings?.ForEach(w => logger.LogTrace(w)); return (null, new List { warning }); } - var certFields = (Dictionary)res.Data.Data; - - logger.LogTrace("retrieved the following entries:"); - certFields.Keys?.ToList()?.ForEach(key => - { - logger.LogTrace($"key: `{key}`, value: {certFields[key].ToString().Length} character long string (value hidden)."); - }); - IFileStore fileStore; switch (_storeType) { @@ -815,11 +885,11 @@ public async Task RemoveCertificateFromPemStore(string certName) try { - return (fileStore.GetInventory(certFields).ToList(), null); + return (fileStore.GetInventory(certStore, passphrase).ToList(), null); } catch (Exception ex) { - logger.LogError(ex, $"Error performing inventory on {_storePath}: {ex.Message}"); + logger.LogError(ex, $"Error performing inventory on {_certPath}: {ex.Message}"); throw; } } @@ -855,5 +925,124 @@ private async Task> GetSubPaths(string storagePath) logger.MethodExit(); return componentPaths; } + + private (string, string, string, string) GetSecretPaths() + { + var certParentPath = _certPath.Substring(0, _certPath.LastIndexOf("/")); + + // if a seperate passphrase path is not provided, we use the same parent path as the certificate to store the passphrase. + var passphraseParentPath = string.IsNullOrEmpty(_passphrasePath) ? certParentPath : _passphrasePath?[.._passphrasePath.LastIndexOf('/')]; + + logger.LogTrace($"cert parent path = {certParentPath}"); + logger.LogTrace($"passphrase parent path = {passphraseParentPath}"); + + var certSecretName = _certPath.Substring(_certPath.LastIndexOf('/')).TrimStart('/'); + certSecretName = certSecretName.Split('?')[0]; // we want the name of the secret without the optional property name parameter + var passphraseSecretName = string.IsNullOrEmpty(_passphrasePath) ? StoreFileExtensions.PASSPHRASE : _passphrasePath[_passphrasePath.LastIndexOf('/')..]; + passphraseSecretName = passphraseSecretName.Split('?')[0].TrimStart('/'); // we want the name of the secret without the optional property name parameter + logger.LogTrace($"cert secret name = {certSecretName}"); + logger.LogTrace($"passphrase secret name = {passphraseSecretName}"); + + return (certParentPath, certSecretName, passphraseParentPath, passphraseSecretName); + } + + private async Task<(string, string)> GetCertificateAndPassphrase() + { + (var certParentPath, var certSecretName, var passphraseParentPath, var passphraseSecretName) = GetSecretPaths(); + var certSecretIsJSON = !string.IsNullOrEmpty(_certPropName); + var passphraseSecretIsJSON = !string.IsNullOrEmpty(_passphrasePropName); + + string certContent = string.Empty; + string passphrase = string.Empty; + Secret res = null; + Dictionary certFileObj = null; + + // first get cert contents + try + { + logger.LogTrace($"retreiving the certificate store secret at {_certPath} from the Key-Value secrets engine mounted at {_mountPoint}.."); + logger.LogTrace($"the cert is {(certSecretIsJSON ? "" : "not")} a JSON property."); + if (certSecretIsJSON) logger.LogTrace($"the cert is stored in the property named {_certPropName}"); + + res = await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(_certPath, mountPoint: _mountPoint); + + logger.LogTrace($"received a response: {JsonConvert.SerializeObject(res)}"); + + if (res.Warnings.Any()) + { + logger.LogTrace($"response warnings: {res.Warnings}"); + } + + certFileObj = (Dictionary)res?.Data?.Data; + + logger.LogTrace($"got cert secret data.. contents: "); + + if (certFileObj == null || certFileObj?.Keys?.Count == 0) + { + logger.LogError($"no secret content was found at path {_certPath}"); + throw new DirectoryNotFoundException($"entry named {certSecretName} not found at {certParentPath} or is empty."); + } + + foreach (var key in certFileObj.Keys) + { + logger.LogTrace($"key = {key}, value = {certFileObj[key]}"); + } + + logger.LogTrace($"getting the contents of {certSecretName}"); + + + if (certSecretIsJSON) + { + // if the cert data is stored as a property in a JSON secret object, we get the value from the property + certContent = certFileObj[_certPropName].ToString(); + } + else + { + // otherwise, the entire secret content is the base64 encoded cert + certContent = certFileObj.First().Value.ToString(); + } + + logger.LogTrace($"base64 encoded cert: {certContent}"); + + logger.LogTrace($"now we retrieve the passphrase from {passphraseParentPath + passphraseSecretName}"); + res = await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(_passphrasePath, mountPoint: _mountPoint); + var passphraseObj = (Dictionary)res?.Data?.Data; + + foreach (var key in passphraseObj.Keys) + { + logger.LogTrace($"key = {key}, value = {passphraseObj[key]}"); + } + + if (passphraseSecretIsJSON) + { + // the secret is a json object with one of the fields containing the passphrase + passphrase = passphraseObj[_passphrasePropName].ToString(); + } + else + { + // the entire contents of the secret is the passphrase + passphrase = passphraseObj.First().Value.ToString(); + } + + if (string.IsNullOrEmpty(passphrase)) + { + throw new DirectoryNotFoundException($"no passphrase found at {_passphrasePath}"); + } + else { logger.LogTrace($"retrieved passphrase of length {passphrase.Length}"); } + } + catch (Exception ex) + { + var warning = $"Vault returned an error when attempting to read the certificate from {_certPath} or the passphrase from {_passphrasePath}. Exception message: {ex.Message}"; + logger.LogError($"there was an error when attempting to retrieve the cert and passphrase: {LogHandler.FlattenException(ex)}"); + if (res != null && res.Warnings.Any()) res.Warnings.ForEach(w => logger.LogTrace(w)); + throw; + } + + logger.LogTrace("successfully retreived the secrets.. "); + logger.LogTrace($"cert file contents: {certContent}"); + logger.LogTrace($"passphrase length: {passphrase.Length}"); + + return (certContent, passphrase); + } } } \ No newline at end of file diff --git a/hashicorp-vault-orchestrator/HcvKeyfactorClient.cs b/hashicorp-vault-orchestrator/HcvKeyfactorClient.cs index a9aee7b..ae59981 100644 --- a/hashicorp-vault-orchestrator/HcvKeyfactorClient.cs +++ b/hashicorp-vault-orchestrator/HcvKeyfactorClient.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; @@ -164,7 +165,7 @@ public class ListResponse : HashiResponse throw new NotSupportedException(); } - public Task PutCertificate(string certName, string contents, string pfxPassword, bool includeChain) + public Task PutCertificate(string alias, string contents, string pfxPassword, string certSecretPath, string certSecretPropName, string passphrasePath, string passphrasePropName, bool includeChain) { throw new NotSupportedException(); } diff --git a/hashicorp-vault-orchestrator/IHashiClient.cs b/hashicorp-vault-orchestrator/IHashiClient.cs index 19ad3ea..e17a74d 100644 --- a/hashicorp-vault-orchestrator/IHashiClient.cs +++ b/hashicorp-vault-orchestrator/IHashiClient.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System.Collections.Generic; using System.Threading.Tasks; @@ -16,7 +17,7 @@ public interface IHashiClient Task<(List, List)> GetCertificates(); Task GetCertificateFromPemStore(string key); Task<(List, List)> GetVaults(string storePath); - Task PutCertificate(string certName, string contents, string pfxPassword, bool includeChain); + Task PutCertificate(string certName, string contents, string pfxPassword, string certPath, string certPropName, string keyPath, string keyPropName, bool includeChain); Task RemoveCertificate(string certName); Task CreateCertStore(); } diff --git a/hashicorp-vault-orchestrator/JobProperties.cs b/hashicorp-vault-orchestrator/JobProperties.cs new file mode 100644 index 0000000..f192428 --- /dev/null +++ b/hashicorp-vault-orchestrator/JobProperties.cs @@ -0,0 +1,19 @@ +namespace Keyfactor.Extensions.Orchestrator.HashicorpVault +{ + public class JobProperties + { + public string StorePath { get; set; } + public string CertSecretPath => StorePath.Split('?')[0]; // everything before the optional ? is the path to the cert secret + public string CertSecretPropName => StorePath.Split('?').Length > 1 ? StorePath.Split('?')[1] : string.Empty; // anything after the ? is the optional property name within the secret for the certificate + public string VaultToken { get; set; } + public string ClientMachine { get; set; } + public string VaultServerUrl { get; set; } + public string PassphrasePath { get; set; } + public string PassphraseSecretPath => PassphrasePath.Split('?')[0] ?? string.Empty; // everything before the optional ? is the path to the cert store password secret + public string PassphraseSecretPropName => PassphrasePath.Split('?').Length > 1 ? PassphrasePath.Split('?')[1] : string.Empty; // anything after the ? is the optional property name within the secret for the password + public bool SubfolderInventory { get; set; } + public bool IncludeCertChain { get; set; } + public string MountPoint { get; set; } // the mount point of the KV secrets engine. defaults to kv-v2 if not provided. + public string Namespace { get; set; } // for enterprise editions of vault that utilize namespaces; split from the passed in mount point if needed. + } +} diff --git a/hashicorp-vault-orchestrator/Jobs/Discovery.cs b/hashicorp-vault-orchestrator/Jobs/Discovery.cs index 1a14943..a5a4a70 100644 --- a/hashicorp-vault-orchestrator/Jobs/Discovery.cs +++ b/hashicorp-vault-orchestrator/Jobs/Discovery.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; @@ -32,7 +33,7 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd try { - (vaults, warnings) = VaultClient.GetVaults(StorePath).Result; + (vaults, warnings) = VaultClient.GetVaults(JobParameters.StorePath).Result; if (vaults?.Count() > 0) jobStatus = OrchestratorJobStatusJobResult.Success; diff --git a/hashicorp-vault-orchestrator/Jobs/Inventory.cs b/hashicorp-vault-orchestrator/Jobs/Inventory.cs index 3ddb9fc..d939165 100644 --- a/hashicorp-vault-orchestrator/Jobs/Inventory.cs +++ b/hashicorp-vault-orchestrator/Jobs/Inventory.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; @@ -13,7 +14,6 @@ using Keyfactor.Orchestrators.Extensions; using Keyfactor.Orchestrators.Extensions.Interfaces; using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Serialization; namespace Keyfactor.Extensions.Orchestrator.HashicorpVault.Jobs { diff --git a/hashicorp-vault-orchestrator/Jobs/JobBase.cs b/hashicorp-vault-orchestrator/Jobs/JobBase.cs index 21235df..6c2f817 100644 --- a/hashicorp-vault-orchestrator/Jobs/JobBase.cs +++ b/hashicorp-vault-orchestrator/Jobs/JobBase.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; using System.Collections.Generic; @@ -18,51 +19,71 @@ namespace Keyfactor.Extensions.Orchestrator.HashicorpVault.Jobs public abstract class JobBase { public string ExtensionName => "HCV"; - - public string StorePath { get; set; } - public string VaultToken { get; set; } - public string ClientMachine { get; set; } - public string VaultServerUrl { get; set; } - public bool SubfolderInventory { get; set; } - public bool IncludeCertChain { get; set; } - public string MountPoint { get; set; } // the mount point of the KV secrets engine. defaults to kv-v2 if not provided. - public string Namespace { get; set; } // for enterprise editions of vault that utilize namespaces; split from the passed in mount point if needed. + public JobProperties JobParameters { get; set; } internal protected IHashiClient VaultClient { get; set; } internal protected string _storeType { get; set; } internal protected ILogger logger { get; set; } internal protected IPAMSecretResolver PamSecretResolver { get; set; } - public JobBase(IPAMSecretResolver resolver) { + public JobBase(IPAMSecretResolver resolver) + { PamSecretResolver = resolver; } public void Initialize(InventoryJobConfiguration config) { logger = LogHandler.GetClassLogger(GetType()); + JobParameters = new JobProperties(); + + JobParameters.ClientMachine = config.CertificateStoreDetails.ClientMachine; + JobParameters.MountPoint = "kv-v2"; // default - ClientMachine = config.CertificateStoreDetails.ClientMachine; - MountPoint = "kv-v2"; // default - - VaultServerUrl = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server UserName", config.ServerUsername); + JobParameters.VaultServerUrl = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server UserName", config.ServerUsername); - VaultToken = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server Password", config.ServerPassword); + JobParameters.VaultToken = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server Password", config.ServerPassword); - StorePath = config.CertificateStoreDetails.StorePath; - ClientMachine = config.CertificateStoreDetails.ClientMachine; + JobParameters.StorePath = config.CertificateStoreDetails.StorePath; + JobParameters.ClientMachine = config.CertificateStoreDetails.ClientMachine; var props = JsonConvert.DeserializeObject>(config.CertificateStoreDetails.Properties); InitProps(props, config.Capability); + + LogInitValues(); + } + + private void LogInitValues() + { + logger.LogTrace("- - - job initialization complete. resolved values: - - -"); + logger.LogTrace($"ClientMachine:\t{JobParameters.ClientMachine}"); + logger.LogTrace($"VaultServerUrl:\t{JobParameters.VaultServerUrl}"); + logger.LogTrace($"Namespace:\t{JobParameters.Namespace}"); + logger.LogTrace($"MountPoint:\t{JobParameters.MountPoint}"); + logger.LogTrace($"VaultToken:\t{JobParameters.VaultToken.Length} characters (value hidden)"); + logger.LogTrace($"StorePath:\t{JobParameters.StorePath}"); + logger.LogTrace($"IncludeCertChain:\t{JobParameters.IncludeCertChain}"); + + if (!_storeType.Contains(StoreType.HCVKVPEM) && !_storeType.Contains(StoreType.HCVPKI)) + { + logger.LogTrace($"CertSecretPath:\t{JobParameters.CertSecretPath}"); + logger.LogTrace($"CertSecretPropName:\t{(String.IsNullOrEmpty(JobParameters.CertSecretPropName) ? "-not set- (entire secret content should be base64 encoded cert)" : JobParameters.CertSecretPropName)}"); + logger.LogTrace($"PassphraseSecretPath:\t{JobParameters.PassphraseSecretPath}"); + logger.LogTrace($"PassphraseSecretPropName:\t{(String.IsNullOrEmpty(JobParameters.PassphraseSecretPropName) ? "-not set- (entire secret content should be the passphrase)" : JobParameters.PassphraseSecretPropName)}"); + } + if (_storeType.Contains(StoreType.HCVKVPEM)) logger.LogTrace($"SubfolderInventory:\t{JobParameters.SubfolderInventory}"); + logger.LogTrace("- - - - - - - - -"); } public void Initialize(DiscoveryJobConfiguration config) { logger = LogHandler.GetClassLogger(GetType()); - ClientMachine = config.ClientMachine; + JobParameters = new JobProperties(); + + JobParameters.ClientMachine = config.ClientMachine; - VaultServerUrl = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server UserName", config.ServerUsername); + JobParameters.VaultServerUrl = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server UserName", config.ServerUsername); - VaultToken = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server Password", config.ServerPassword); + JobParameters.VaultToken = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server Password", config.ServerPassword); var subPath = config.JobProperties?["dirs"] as string; var mp = config.JobProperties?["extensions"] as string; @@ -71,7 +92,7 @@ public void Initialize(DiscoveryJobConfiguration config) // The mount point and namespace should be passed in the "Extensions" field. // if nothing is provided, we default to mount point: "kv-v2" and no namespace. - StorePath = "/"; + JobParameters.StorePath = "/"; logger.LogTrace($"parsing the passed in mountpoint value: {mp}"); if (!string.IsNullOrEmpty(mp) && mp.Trim() != "/" && mp.Trim() != "\\") @@ -80,32 +101,33 @@ public void Initialize(DiscoveryJobConfiguration config) if (splitmp.Length > 1) { logger.LogTrace($"detected an included namespace {splitmp[0]}, storing for authentication."); - Namespace = splitmp[0].Trim(); - MountPoint = splitmp[1].Trim(); + JobParameters.Namespace = splitmp[0].Trim(); + JobParameters.MountPoint = splitmp[1].Trim(); } else { - MountPoint = mp.TrimStart(new[] { '/' }); + JobParameters.MountPoint = mp.TrimStart(new[] { '/' }); } } if (!string.IsNullOrEmpty(subPath)) { - StorePath = subPath.Trim(); + JobParameters.StorePath = subPath.Trim(); } - logger.LogTrace($"Directories to search (mount point): {MountPoint}"); - logger.LogTrace($"Enterprise Namespace: {Namespace}"); + logger.LogTrace($"Directories to search (mount point): {JobParameters.MountPoint}"); + logger.LogTrace($"Enterprise Namespace: {JobParameters.Namespace}"); logger.LogTrace($"Directories to ignore (subpath to search): {subPath}"); InitProps(config.JobProperties, config.Capability); } public void Initialize(ManagementJobConfiguration config) { logger = LogHandler.GetClassLogger(GetType()); + JobParameters = new JobProperties(); - ClientMachine = config.CertificateStoreDetails.ClientMachine; - VaultServerUrl = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server UserName", config.ServerUsername); - VaultToken = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server Password", config.ServerPassword); - StorePath = config.CertificateStoreDetails.StorePath; + JobParameters.ClientMachine = config.CertificateStoreDetails.ClientMachine; + JobParameters.VaultServerUrl = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server UserName", config.ServerUsername); + JobParameters.VaultToken = PAMUtilities.ResolvePAMField(PamSecretResolver, logger, "Server Password", config.ServerPassword); + JobParameters.StorePath = config.CertificateStoreDetails.StorePath; dynamic props = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties.ToString()); InitProps(props, config.Capability); } @@ -118,30 +140,38 @@ private void InitProps(dynamic props, string capability) if (props.ContainsKey("StorePath")) { - StorePath = props["StorePath"].ToString(); - StorePath = StorePath.TrimStart('/'); - StorePath = StorePath.TrimEnd('/'); + JobParameters.StorePath = props["StorePath"].ToString(); + JobParameters.StorePath = JobParameters.StorePath.TrimStart('/'); + JobParameters.StorePath = JobParameters.StorePath.TrimEnd('/'); if (_storeType.Contains(StoreType.HCVKVPEM) || _storeType.Contains(StoreType.HCVPKI)) { - StorePath += "/"; //ensure single trailing slash for path for PKI or PEM stores. Others use the entry value instead of the container. + JobParameters.StorePath += "/"; //ensure single trailing slash for path for PKI or PEM stores. Others use the entry value instead of the container. } } var mp = props.ContainsKey("MountPoint") ? props["MountPoint"].ToString() : null; - MountPoint = !string.IsNullOrEmpty(mp) ? mp : MountPoint; + JobParameters.MountPoint = !string.IsNullOrEmpty(mp) ? mp : JobParameters.MountPoint; - SubfolderInventory = props.ContainsKey("SubfolderInventory") ? bool.Parse(props["SubfolderInventory"].ToString()) : false; - IncludeCertChain = props.ContainsKey("IncludeCertChain") ? bool.Parse(props["IncludeCertChain"].ToString()) : false; + JobParameters.SubfolderInventory = props.ContainsKey("SubfolderInventory") ? bool.Parse(props["SubfolderInventory"].ToString()) : false; + JobParameters.IncludeCertChain = props.ContainsKey("IncludeCertChain") ? bool.Parse(props["IncludeCertChain"].ToString()) : false; - var isPki = _storeType.Contains("HCVPKI"); + JobParameters.PassphrasePath = props.ContainsKey("PassphrasePath") ? props["PassphrasePath"].ToString() : null; + + if (JobParameters.PassphrasePath == null && _storeType != StoreType.HCVKVPEM && _storeType != StoreType.HCVPKI) + { + // the passphrase path was not provided for HCVKVPFX, HCVKVJKS, or HCVKVP12. + // we assume the convention of a secret named "passphrase" at the same level as the cert secret. + // we assume the contents are a single string containing the passphrase + JobParameters.PassphrasePath = $"{JobParameters.CertSecretPath}/{StoreFileExtensions.PASSPHRASE}"; + } - if (!isPki) + if (!_storeType.Contains("HCVPKI")) { - VaultClient = new HcvKeyValueClient(VaultToken, VaultServerUrl, MountPoint, Namespace, StorePath, _storeType, SubfolderInventory); + VaultClient = new HcvKeyValueClient(JobParameters.VaultToken, JobParameters.VaultServerUrl, JobParameters.MountPoint, JobParameters.Namespace, _storeType, JobParameters.StorePath, JobParameters.CertSecretPropName, JobParameters.PassphrasePath, JobParameters.PassphraseSecretPropName, JobParameters.SubfolderInventory); } else { - VaultClient = new HcvKeyfactorClient(VaultToken, VaultServerUrl, MountPoint, StorePath); + VaultClient = new HcvKeyfactorClient(JobParameters.VaultToken, JobParameters.VaultServerUrl, JobParameters.MountPoint, JobParameters.StorePath); } } } diff --git a/hashicorp-vault-orchestrator/Jobs/Management.cs b/hashicorp-vault-orchestrator/Jobs/Management.cs index 928e706..6aef348 100644 --- a/hashicorp-vault-orchestrator/Jobs/Management.cs +++ b/hashicorp-vault-orchestrator/Jobs/Management.cs @@ -1,11 +1,13 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using System; +using System.Threading.Tasks; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; @@ -55,13 +57,14 @@ protected virtual JobResult PerformCreateCertStore(ManagementJobConfiguration co try { - VaultClient.CreateCertStore(); + Task.Run(VaultClient.CreateCertStore).Wait(); complete.Result = OrchestratorJobStatusJobResult.Success; } catch (Exception ex) { - logger.LogError(ex, "Error when trying to create the new certificate store."); - complete.FailureMessage = $"Error when trying to create the new certificate store. {ex.Message}"; + logger.LogError("Error when trying to create the new certificate store. Returning Job Failed response"); + complete.Result = OrchestratorJobStatusJobResult.Failure; + complete.FailureMessage = $"An error occurred when trying to create the new certificate store: {ex.Message}"; } return complete; } @@ -78,11 +81,10 @@ protected virtual JobResult PerformAddition(string alias, string pfxPassword, st { complete.FailureMessage = "You must supply an alias for the certificate."; return complete; - } - + } try { - var cert = VaultClient.PutCertificate(alias, entryContents, pfxPassword, IncludeCertChain); + var cert = VaultClient.PutCertificate(alias, entryContents, pfxPassword, JobParameters.CertSecretPath, JobParameters.CertSecretPropName, JobParameters.PassphraseSecretPath, JobParameters.PassphraseSecretPropName, JobParameters.IncludeCertChain); cert.Wait(); complete.Result = OrchestratorJobStatusJobResult.Success; } @@ -95,7 +97,7 @@ protected virtual JobResult PerformAddition(string alias, string pfxPassword, st } else { - complete.FailureMessage = $"An error occured while adding {alias} to {StorePath}: " + ex.Message; + complete.FailureMessage = $"An error occured while adding {alias} to {JobParameters.StorePath}: " + ex.Message; if (ex.InnerException != null) complete.FailureMessage += " - " + ex.InnerException.Message; @@ -144,8 +146,8 @@ protected virtual JobResult PerformRemoval(string alias, long jobHistoryId) } else { - logger.LogError("Error deleting cert from Vault", ex); - complete.FailureMessage = $"An error occured while removing {alias} from {StorePath}: " + ex.Message; + logger.LogError($"Error deleting cert from Vault: {ex.Message}"); + complete.FailureMessage = $"An error occured while removing {alias} from {JobParameters.StorePath}: " + ex.Message; } } return complete; diff --git a/hashicorp-vault-orchestrator/PamUtilities.cs b/hashicorp-vault-orchestrator/PamUtilities.cs index d6d291c..4424ed0 100644 --- a/hashicorp-vault-orchestrator/PamUtilities.cs +++ b/hashicorp-vault-orchestrator/PamUtilities.cs @@ -1,9 +1,10 @@ -// Copyright 2023 Keyfactor -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. -// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions -// and limitations under the License. + +// Copyright 2025 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. using Keyfactor.Orchestrators.Extensions.Interfaces; using Microsoft.Extensions.Logging; diff --git a/hashicorp-vault-orchestrator/hashicorp-vault-orchestrator.csproj b/hashicorp-vault-orchestrator/hashicorp-vault-orchestrator.csproj index 00f7d7b..d8fe3b2 100644 --- a/hashicorp-vault-orchestrator/hashicorp-vault-orchestrator.csproj +++ b/hashicorp-vault-orchestrator/hashicorp-vault-orchestrator.csproj @@ -5,6 +5,7 @@ disable true true + Keyfactor.Extensions.Orchestrator.HashicorpVault @@ -19,6 +20,7 @@ all + diff --git a/integration-manifest.json b/integration-manifest.json index ae8935a..61a1432 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -66,6 +66,15 @@ "DependsOn": "", "DefaultValue": "", "Required": true + }, + { + "Name": "PassphrasePath", + "DisplayName": "Passphrase Path", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret." } ], "EntryParameters": [], @@ -167,7 +176,7 @@ "StorePathDescription": "This is the path to the secret containing the store.", "LocalStore": false, "StorePathType": "", - "StorePathValue": "", + "StorePathValue": "example: '/mycerts/certstore.jks?b64cert'", "PrivateKeyAllowed": "Optional", "JobProperties": [], "ServerRequired": true, @@ -219,6 +228,15 @@ "DependsOn": "", "DefaultValue": "", "Required": false + }, + { + "Name": "PassphrasePath", + "DisplayName": "Passphrase Path", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret." } ], "EntryParameters": [], @@ -240,7 +258,7 @@ "StorePathDescription": "This is the path to the secret containing the store.", "LocalStore": false, "StorePathType": "", - "StorePathValue": "", + "StorePathValue": "example: '/mycerts/certstore.p12?b64cert'", "PrivateKeyAllowed": "Optional", "JobProperties": [], "ServerRequired": true, @@ -292,6 +310,15 @@ "DependsOn": "", "DefaultValue": "", "Required": false + }, + { + "Name": "PassphrasePath", + "DisplayName": "Passphrase Path", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret." } ], "EntryParameters": [], @@ -313,7 +340,7 @@ "StorePathDescription": "This is the path to the secret containing the store.", "LocalStore": false, "StorePathType": "", - "StorePathValue": "", + "StorePathValue": "example: '/mycerts/certstore.pfx?b64cert'", "PrivateKeyAllowed": "Optional", "JobProperties": [], "ServerRequired": true, @@ -365,6 +392,15 @@ "DependsOn": "", "DefaultValue": "", "Required": false + }, + { + "Name": "PassphrasePath", + "DisplayName": "Passphrase Path", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "Description": "This is the path to the secret that contains the passphrase to the cert store file. If empty or omitted, assume the secret is named 'passphrase' on the same level as the certificate store secret." } ], "EntryParameters": [],