diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..d846769
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,235 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = true
+dotnet_style_allow_statement_immediately_after_block_experimental = true
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = false
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true
+csharp_style_expression_bodied_constructors = false
+csharp_style_expression_bodied_indexers = true
+csharp_style_expression_bodied_lambdas = true
+csharp_style_expression_bodied_local_functions = false
+csharp_style_expression_bodied_methods = false
+csharp_style_expression_bodied_operators = false
+csharp_style_expression_bodied_properties = true
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true
+csharp_style_pattern_matching_over_is_with_cast_check = true
+csharp_style_prefer_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true
+csharp_style_prefer_switch_expression = true
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_style_prefer_readonly_struct = true
+csharp_style_prefer_readonly_struct_member = true
+
+# Code-block preferences
+csharp_prefer_braces = true
+csharp_prefer_simple_using_statement = true
+csharp_style_namespace_declarations = block_scoped
+csharp_style_prefer_method_group_conversion = true
+csharp_style_prefer_primary_constructors = true
+csharp_style_prefer_top_level_statements = true
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true
+csharp_style_deconstructed_variable_declaration = true
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+csharp_style_inlined_variable_declaration = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = true
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
+csharp_style_allow_embedded_statements_on_same_line_experimental = true
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+# File headers
+file_header_template = Copyright {year} Keyfactor \n Licensed under the Apache License, Version 2.0 (the "License")\; you may not use this file except in compliance with the License. \n You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 \n Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, \n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions \n and limitations under the License.
diff --git a/NuGet.config b/NuGet.config
new file mode 100644
index 0000000..de0bbec
--- /dev/null
+++ b/NuGet.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 480ce41..16c26ae 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Hashicorp Vault AnyCA REST Gateway Plugin
-Hashicorp Vault PKI Secrets Engine integration using AnyCA REST Gateway framework
+Hashicorp Vault plugin for the AnyCA REST Gateway Framework
#### Integration status: Prototype - Demonstration quality. Not for use in customer environments.
@@ -22,152 +22,133 @@ Hashicorp Vault AnyCA REST Gateway Plugin is open source and community supported
-# Introduction
+# Introduction
This AnyGateway plug-in enables issuance, revocation, and synchronization of certificates from the Hashicorp Vault PKI Secrets Engine.
# Hashicorp Vault Authentication
-This plug-in supports two types of authentication into Hashicorp Vault.
-1. Token
-1. Certificate
-
-When filling in the configuration values, if a value for "AuthToken" is present, it will be used. If not, then the values for certificate location should be populated for Authentication via certificate.
+Currently this plug-in only supports authentication into Vault via Token.
# Prerequisites
+1. An instance of Hashicorp Vault v10.5+ that is accessible from the CA Gateway host
+1. An instance of the CA Gateway Framework (REST version)
## Certificate Chain
In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you create your Root and/or Subordinate CA, make sure to import the certificate chain into the AnyGateway and Command Server certificate store
-# Install
-* Download latest successful build from [GitHub Releases](../../releases/latest)
+# Installation
-* Copy .dll to the Program Files\Keyfactor\Keyfactor AnyGateway directory
+## Requirements
+Make sure the following information is available, as it will be needed to complete the installation.
-* Update the CAProxyServer.config file
- * Update the CAConnection section to point at the DigiCertCAProxy class
- ```xml
-
- ```
+- The fully qualified URI of the instance of Hashicorp Vault
+- The namespace and mountpoint of the instance of the PKI secrets engine running in Vault
+- An authentication token that has sufficient authority to perform operations on the PKI Secrets engine
+- PKI Secrets Engine Roles defined that will correspond to certificate templates to be used when signing certificates with the CA.
-# Configuration
-The following sections will breakdown the required configurations for the AnyGatewayConfig.json file that will be imported to configure the AnyGateway.
-
-## Templates
-The Template section will map the CA's products to an AD template.
-* ```ProductID```
-This is the ID of the product to map to the specified template.
-
- ```json
- "Templates": {
- "WebServer": {
- "ProductID": "",
- "Parameters": {
- }
- }
-}
- ```
-
-## Security
-The security section does not change specifically for the Hashicorp Vault PKI CA Gateway. Refer to the AnyGateway Documentation for more detail.
-```json
- /*Grant permissions on the CA to users or groups in the local domain.
- READ: Enumerate and read contents of certificates.
- ENROLL: Request certificates from the CA.
- OFFICER: Perform certificate functions such as issuance and revocation. This is equivalent to "Issue and Manage" permission on the Microsoft CA.
- ADMINISTRATOR: Configure/reconfigure the gateway.
- Valid permission settings are "Allow", "None", and "Deny".*/
- "Security": {
- "Keyfactor\\Administrator": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- },
- "Keyfactor\\gateway_test": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- },
- "Keyfactor\\SVC_TimerService": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "None"
- },
- "Keyfactor\\SVC_AppPool": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- }
- }
-```
-## CerificateManagers
-The Certificate Managers section is optional.
- If configured, all users or groups granted OFFICER permissions under the Security section
- must be configured for at least one Template and one Requester.
- Uses "" to specify all templates. Uses "Everyone" to specify all requesters.
- Valid permission values are "Allow" and "Deny".
-```json
- "CertificateManagers":{
- "DOMAIN\\Username":{
- "Templates":{
- "MyTemplateShortName":{
- "Requesters":{
- "Everyone":"Allow",
- "DOMAIN\\Groupname":"Deny"
- }
- },
- "":{
- "Requesters":{
- "Everyone":"Allow"
- }
- }
- }
- }
- }
-```
-## CAConnection
-The CA Connection section will determine the API endpoint and configuration data used to connect to the API.
+### Steps
+1. Install the AnyCA Gateway Rest per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm).
+1. Download latest successful build from [GitHub Releases](https://github.com/Keyfactor/hashicorp-vault-caplugin/releases/latest)
+1. Copy the contents of the release zip file into the (AnyGatewayRest Installation Folder)\AnyGatewayREST\net6.0\Extensions AnyGateway directory.
+1. The _manifest.json_ tells the Gateway how to locate our plugin. It should be copied to the *Connectors* sub-folder in the AnyCA Gateway Rest installation path.
+1. Restart the gateway service.
+1. Navigate to the AnyCA Gateway Rest portal and verify that the Gateway recognizes the Hashicorp Vault CA plugin by hovering over the ⓘ symbol to the right of the Gateway name.
+#### _manifest.json_
```json
- "CAConnection": {
- "AuthToken":"",
- "ClientCertificate": {
- "StoreName": "My",
- "StoreLocation": "LocalMachine",
- "Thumbprint": "0123456789abcdef"
- },
- "Name": "TestUser",
- "Email": "email@email.invalid",
- "PhoneNumber": "0000000000",
- "IgnoreExpired": "false"
- },
-```
-## GatewayRegistration
-There are no specific Changes for the GatewayRegistration section. Refer to the AnyGateway Documentation for more detail.
-```json
- "GatewayRegistration": {
- "LogicalName": "CASandbox",
- "GatewayCertificate": {
- "StoreName": "CA",
- "StoreLocation": "LocalMachine",
- "Thumbprint": "0123456789abcdef"
+{
+ "extensions": {
+ "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
+ "HashicorpVaultCAPlugin": {
+ "assemblypath": "../HashicorpVaultCAPlugin.dll",
+ "TypeFullName": "Keyfactor.Extensions.CAPlugin.HashicorpVault.HashicorpVaultCAConnector"
+ }
}
}
+}
```
-## ServiceSettings
-There are no specific Changes for the ServiceSettings section. Refer to the AnyGateway Documentation for more detail.
-```json
- "ServiceSettings": {
- "ViewIdleMinutes": 8,
- "FullScanPeriodHours": 24,
- "PartialScanPeriodMinutes": 240
- }
-```
+# Configuration
+
+1. Follow the [official AnyCA Gateway REST documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm) to define a new Certificate Authority, and use the notes below to configure the **Gateway Registration** and **CA Connection** tabs:
+
+### Configure the CA in the AnyCA Gateway Rest Portal
+
+
+* **Gateway Registration**
+
+ In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you know your Root and/or Subordinate CA in your Hashicorp Vault instance, make sure to download and import the certificate chain into the Command Server certificate store
+
+ Once the necessary files are copied to the appropriate locations and the AnyCA Gateway Rest is up and running, navigate to the AnyCA Gateway Rest portal and configure the CA.
+
+* **CA Connection**
+
+ Populate using the configuration fields collected in the [requirements](##requirements) section.
+
+ * **Host** - The fully qualified URI including port for the instance of vault. ex: https://127.0.0.1:8001
+ * **Namespace** - If you are utilizing Vault Namespaces (Enterprise feature); the namespace containing the PKI secrets engine containing your CA.
+ * **MountPoint** - The mount point of the PKI secrets engine.
+ * **Token** - The token that will be used by the gateway for authentication. It should have policies defined that ensure it can perform operations on the path defined by `//`
+ * **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.
+
+
+* **Template mapping**
+
+ The product ID's correspond to the role names in the Hashicorp Vault PKI Secrets engine. After the certificate profile is associated with the product ID and imported as a certificate template into Command, requests for certificates will pass the associated role name as part of the request and the issuance policies defined in Vault for that role will be applied as if you were issuing the certificates directly from Vault.
+
+ In order to create create the certificate templates associated with the role names once the CA has been defined in the gateway portal, follow these steps:
+
+ 1. navigate to the "Certificate Profiles" tab
+ 1. Create an entry for each of the PKI secrets engine roles you would like to use for issuing certificates from the Hashicorp Vault CA.
+ 1. Navigate to the "Certificate Authorities" tab and click "Edit"
+ 1. In the "Edit CA" window, navigate to the "Templates" tab.
+ 1. Create an association between each of the certificate profiles we just created with the PKI secrets engine roles retreived from Vault.
+
+### Configure the CA in Keyfactor Command
+
+Now that the AnyCA Gateway Rest is configured with the details of our Hashicorp Vault hosted CA, we will need to define the CA in Keyfactor Command
+
+* **Certificate Authorities**
+ 1. Log into Keyfactor Command with an account that has sufficient permissions to define a new Certificate Authorities.
+ 1. Navigate to "Locations > Certificate Authorities"
+ 1. If the AnyCA Gateway Rest host is Active Directory joined with Command
+ 1. click "Import" to automatically load the details from the Gateway
+ 1. If not Active Directory domain joined, click "Add" in order to manually fill in the details
+ * **Basic**
+ 1. **Logical Name**: The logical name of the CA, as defined in the Gateway Portal.
+ 1. **Host URL**: The host url of the instance of the AnyCA Gateway Rest. This will be the same URL you use to navigate to the Gateway Portal
+ 1. **Configuration Tenant**: this can be any name. It is used by Command to create an Active Directory tenant for the CA.
+ 1. Fill in the rest of the details according to your requirements
+
+ * **Authorization Methods**
+ You will need to have the PFX certificate, including private key, for Keyfactor Command to use when authenticating into the Gateway. This should be the certificate associated with the identifier (thumbprint or serial number) that was provided when the Gateway was installed.
+ 1. Click the "Select authentication certificate" button and choose this PFX file, enter the password if prompted.
+ 1. Click "Save and Test" in order to save the configuration and see the result of Command attempting to authenticate.
+
+
+
+### Troubleshooting
+
+When troubleshooting the Gateway configuration, the log files can be very useful. They are located in the "logs" sub-folder in the gateway installation path.
+
+1. Authentication into the Gateway Portal fails
+
+ - Make sure that the authentication certificate with private key is installed into the "Current User > Personal" certificate store
+ - If you are seeing an error that indicates the gateway is unable to check the CRL for the certificate..
+ - make sure the CRL endpoint is defined on the CA in Vault
+ - If no CRL is available, you can turn off the CRL check on the authentication certificate by the Gateway thusly:
+ - stop the Gateway service on the host
+ - edit the "appsettings.json" file in the Gateway installation directory
+ - Change the value of "CheckClientCRL" to "False"
+ - Restart the gateway
+ - re-attempt login
+
+1. If an error response is returned when attempting to sign or issue certificates via the CA in Command
+ - Check the CA_Gateway_Log.txt file in the "logs" subfolder of the Gateway installation path
+ - Make sure that the Vault PKI Role policies allow issuing certificates with the defined values
+
+
diff --git a/hashicorp-vault-cagateway/APIProxy/CertResponse.cs b/hashicorp-vault-cagateway/APIProxy/CertResponse.cs
new file mode 100644
index 0000000..305ec3f
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/CertResponse.cs
@@ -0,0 +1,24 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class CertResponse
+ {
+ [JsonPropertyName("certificate")]
+ public string Certificate { get; set; }
+
+ [JsonPropertyName("revocation_time_rfc3339")]
+ public DateTime? RevocationTime { get; set; }
+
+ [JsonPropertyName("issuer_id")]
+ public string IssuerId { get; set; }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/ErrorResponse.cs b/hashicorp-vault-cagateway/APIProxy/ErrorResponse.cs
new file mode 100644
index 0000000..8da94ad
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/ErrorResponse.cs
@@ -0,0 +1,18 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class ErrorResponse
+ {
+ [JsonPropertyName("errors")]
+ public List Errors { get; set; }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/HashicorpVaultBaseCall.cs b/hashicorp-vault-cagateway/APIProxy/HashicorpVaultBaseCall.cs
deleted file mode 100644
index c9383c7..0000000
--- a/hashicorp-vault-cagateway/APIProxy/HashicorpVaultBaseCall.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Newtonsoft.Json;
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Keyfactor.Extensions.AnyGateway.HashicorpVault.APIProxy
-{
- public abstract class ProductNameBaseRequest
- {
- [JsonIgnore]
- public string Resource { get; internal set; }
-
- [JsonIgnore]
- public string Method { get; internal set; }
-
- [JsonIgnore]
- public string targetURI { get; set; }
-
- public string BuildParameters()
- {
- return "";
- }
- }
-}
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/APIProxy/KeyedList.cs b/hashicorp-vault-cagateway/APIProxy/KeyedList.cs
new file mode 100644
index 0000000..9fe9ecc
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/KeyedList.cs
@@ -0,0 +1,18 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class KeyedList
+ {
+ [JsonPropertyName("keys")]
+ public List Entries { get; set; }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/RevokeRequest.cs b/hashicorp-vault-cagateway/APIProxy/RevokeRequest.cs
new file mode 100644
index 0000000..7a4421a
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/RevokeRequest.cs
@@ -0,0 +1,22 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class RevokeRequest
+ {
+ [JsonPropertyName("serial_number")]
+ public string SerialNumber { get; set; }
+
+ public RevokeRequest(string serialNumber)
+ {
+ SerialNumber = serialNumber;
+ }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/RevokeResponse.cs b/hashicorp-vault-cagateway/APIProxy/RevokeResponse.cs
new file mode 100644
index 0000000..6a1e144
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/RevokeResponse.cs
@@ -0,0 +1,22 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class RevokeResponse
+ {
+ [JsonPropertyName("revocation_time_rfc3339")]
+ public DateTime RevocationTime { get; set; }
+
+ [JsonPropertyName("state")]
+ public string State { get; set; }
+
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/SealStatusResponse.cs b/hashicorp-vault-cagateway/APIProxy/SealStatusResponse.cs
new file mode 100644
index 0000000..e6cb1c3
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/SealStatusResponse.cs
@@ -0,0 +1,23 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class SealStatusResponse
+ {
+ [JsonPropertyName("sealed")]
+ public bool Sealed { get; set; }
+
+ [JsonPropertyName("initialized")]
+ public bool Initialized { get; set; }
+
+ [JsonPropertyName("version")]
+ public string VaultVersion { get; set; }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/SignRequest.cs b/hashicorp-vault-cagateway/APIProxy/SignRequest.cs
new file mode 100644
index 0000000..f0360f2
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/SignRequest.cs
@@ -0,0 +1,111 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class SignRequest
+ {
+ ///
+ /// The PEM encoded CSR
+ ///
+ [JsonPropertyName("csr")]
+ public string CSR { get; set; }
+
+ ///
+ /// Specifies the requested CN for the certificate. If the CN is allowed by role policy, it will be issued.
+ /// If more than one common_name is desired, specify the alternative names in the alt_names list.
+ ///
+ [JsonPropertyName("common_name"), ]
+ public string CommonName { get; set; }
+
+ ///
+ /// Specifies the requested Subject Alternative Names, in a comma-delimited list.
+ /// These can be host names or email addresses; they will be parsed into their respective fields.
+ /// If any requested names do not match role policy, the entire request will be denied.
+ ///
+ [JsonPropertyName("alt_names")]
+ public string AltNames { get; set; }
+
+ ///
+ /// Specifies custom OID/UTF8-string SANs. These must match values specified on the role in allowed_other_sans
+ /// (see role creation for allowed_other_sans globbing rules).
+ /// The format is the same as OpenSSL: ;: where the only current valid type is UTF8.
+ /// This can be a comma-delimited list or a JSON string slice.
+ ///
+ [JsonPropertyName("other_sans")]
+ public string OtherSans { get; set; }
+
+ ///
+ /// Specifies the requested IP Subject Alternative Names, in a comma-delimited list.
+ /// Only valid if the role allows IP SANs (which is the default).
+ ///
+ [JsonPropertyName("ip_sans")]
+ public string IpSans { get; set; }
+
+ ///
+ /// Specifies the requested URI Subject Alternative Names, in a comma-delimited list.
+ /// If any requested URIs do not match role policy, the entire request will be denied.
+ ///
+ [JsonPropertyName("uri_sans")]
+ public string UriSans { get; set; }
+
+ ///
+ /// Specifies the requested Time To Live. Cannot be greater than the role's max_ttl value.
+ /// If not provided, the role's ttl value will be used. Note that the role values default to system values if not explicitly set.
+ /// See not_after as an alternative for setting an absolute end date (rather than a relative one).
+ ///
+ [JsonPropertyName("ttl")]
+ public string TTL { get; set; }
+
+ ///
+ /// Specifies the format for returned data. Can be pem, der, or pem_bundle. If der, the output is base64 encoded.
+ /// If pem_bundle, the certificate field will contain the certificate and, if the issuing CA is not a Vault-derived self-signed root,
+ /// it will be concatenated with the certificate.
+ ///
+ [JsonPropertyName("format")]
+ public string Format { get; set; }
+
+ ///
+ /// If true, the given common_name will not be included in DNS or Email Subject Alternate Names (as appropriate).
+ /// Useful if the CN is not a hostname or email address, but is instead some human-readable identifier.
+ ///
+ [JsonPropertyName("exclude_cn_from_sans")]
+ public bool ExcludeCnFromSans { get; set; }
+
+ ///
+ /// Set the Not After field of the certificate with specified date value.
+ /// The value format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ.
+ /// Supports the Y10K end date for IEEE 802.1AR-2018 standard devices, 9999-12-31T23:59:59Z.
+ ///
+ [JsonPropertyName("not_after")]
+ public string NotAfter { get; set; }
+
+ ///
+ /// If true, the returned ca_chain field will not include any self-signed CA certificates.
+ /// Useful if end-users already have the root CA in their trust store.
+ ///
+ [JsonPropertyName("remove_roots_from_chain")]
+ public bool RemoveRootsFromChain { get; set; }
+
+ ///
+ /// Specifies the comma-separated list of requested User ID (OID 0.9.2342.19200300.100.1.1)
+ /// Subject values to be placed on the signed certificate. This field is validated against allowed_user_ids on the role.
+ ///
+ [JsonPropertyName("user_ids")]
+ public string UserIds { get; set; }
+
+ ///
+ /// **Vault Enterprise edition only**
+ /// A base 64 encoded value or an empty string to associate with the certificate's serial number.
+ /// The role's no_store_metadata must be set to false, otherwise an error is returned when specified
+ ///
+ [JsonPropertyName("cert_metadata")]
+ public string CertMetadata { get; set; }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/SignResponse.cs b/hashicorp-vault-cagateway/APIProxy/SignResponse.cs
new file mode 100644
index 0000000..2aa29d7
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/SignResponse.cs
@@ -0,0 +1,35 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class SignResponse
+ {
+ [JsonPropertyName("expiration")]
+ public int Expiration { get; set; }
+
+ [JsonPropertyName("certificate")]
+ public string Certificate { get; set; }
+
+ [JsonPropertyName("issuing_ca")]
+ public string IssuingCA { get; set; }
+
+ [JsonPropertyName("ca_chain")]
+ public List CAChain { get; set; }
+
+ private string _vaultSerial { get; set; }
+ [JsonPropertyName("serial_number")]
+ public string SerialNumber // replacing ":" with "-" since they work interchangeably in Vault, and we cannot use ":" in our tracking ids.
+ {
+ get { return _vaultSerial.Replace(":", "-"); }
+ set { _vaultSerial = value; }
+ }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/TokenLookupResponse.cs b/hashicorp-vault-cagateway/APIProxy/TokenLookupResponse.cs
new file mode 100644
index 0000000..0b7d141
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/TokenLookupResponse.cs
@@ -0,0 +1,22 @@
+// Copyright 2024 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.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class TokenLookupResponse
+ {
+ public List IdentityPolicies { get; set; }
+ public List Policies { get; set; }
+ public string DisplayName { get; set; }
+ }
+}
diff --git a/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs b/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs
new file mode 100644
index 0000000..7725dd9
--- /dev/null
+++ b/hashicorp-vault-cagateway/APIProxy/WrappedResponse.cs
@@ -0,0 +1,40 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy
+{
+ public class WrappedResponse
+ {
+ [JsonPropertyName("lease_id")]
+ public string LeaseId { get; set; }
+
+ [JsonPropertyName("renewable")]
+ public bool? Renewable { get; set; }
+
+ [JsonPropertyName("lease_duration")]
+ public int? LeaseDuration { get; set; }
+
+ [JsonPropertyName("auth")]
+ public string Auth { get; set; }
+
+ [JsonPropertyName("warnings")]
+ public List Warnings { get; set; }
+
+ [JsonPropertyName("mount_point")]
+ public string MountPoint { get; set; }
+
+ [JsonPropertyName("mount_running_plugin_version")]
+ public string PluginVersion { get; set; }
+
+ [JsonPropertyName("data")]
+ public T Data { get; set; }
+
+ }
+}
diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs
index 5f629be..3ab133d 100644
--- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs
+++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs
@@ -1,71 +1,286 @@
-using Org.BouncyCastle.Asn1.X509;
+// Copyright 2024 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.Extensions.CAPlugin.HashicorpVault.APIProxy;
+using Keyfactor.Extensions.CAPlugin.HashicorpVault.Client;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+using Org.BouncyCastle.Asn1.X509;
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
+using System.Text.Json;
using System.Threading.Tasks;
-using VaultSharp;
-using VaultSharp.V1.Commons;
-using VaultSharp.V1.SecretsEngines.PKI;
-namespace Keyfactor.Extensions.AnyGateway.HashicorpVault.Client
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault
{
+ ///
+ /// This is our client for interacting with the Hashicorp Vault API.
+ /// It wraps our vault http client with the ability to translate objects
+ /// to and from what is needed for Vault API requests.
+ ///
public class HashicorpVaultClient
{
- private VaultClient _vaultClient { get; set; }
+ private VaultHttp _vaultHttp { get; set; }
+ private static readonly ILogger logger = LogHandler.GetClassLogger();
+
+ public HashicorpVaultClient(HashicorpVaultCAConfig caConfig, HashicorpVaultCATemplateConfig templateConfig = null)
+ {
+ logger.MethodEntry();
- public HashicorpVaultClient(string vaultServerUri) {
- _vaultClient = new VaultClient(new VaultClientSettings(vaultServerUri, n) {
- })
-
+ SetClientValuesFromConfigs(caConfig, templateConfig);
+
+ logger.MethodExit();
}
- public Secret SignCSR(string csr, string subject, Dictionary san, string roleName)
+
+ public async Task SignCSR(string csr, string subject, Dictionary san, string roleName)
{
+ logger.MethodEntry();
- var reqOptions = new SignCertificatesRequestOptions();
+ var dnsNames = new List();
+ SignRequest request = null;
+ WrappedResponse response = null;
+ X509Name subjectParsed = null;
+ string commonName = null, organization = null, orgUnit = null;
+
+ logger.LogTrace($"SAN values: ");
+ foreach (var key in san.Keys) {
+ logger.LogTrace($"{key}: {string.Join(",", san[key])}");
+ }
- List dnsNames = new List();
- if (san.ContainsKey("Dns"))
+ if (san.ContainsKey("dnsname"))
+ {
+ dnsNames = new List(san["dnsname"]);
+ logger.LogTrace($"the SAN contains DNS name{(dnsNames.Count > 1 ? 's' : string.Empty)}: {string.Join(",", dnsNames)}");
+ }
+ else
{
- dnsNames = new List(san["Dns"]);
+ logger.LogTrace("the provided SANs contain no DNS names");
}
- // Parse subject
- X509Name subjectParsed = null;
- string commonName = null, organization = null, orgUnit = null;
try
{
+ logger.LogTrace($"parsing the subject: {subject}");
subjectParsed = new X509Name(subject);
commonName = subjectParsed.GetValueList(X509Name.CN).Cast().LastOrDefault();
+ logger.LogTrace($"CN: {commonName}");
organization = subjectParsed.GetValueList(X509Name.O).Cast().LastOrDefault();
+ logger.LogTrace($"Org: {organization}");
orgUnit = subjectParsed.GetValueList(X509Name.OU).Cast().LastOrDefault();
+ logger.LogTrace($"OU: {orgUnit}");
+ }
+ catch (Exception ex)
+ {
+ logger.LogTrace("couldn't parse all values from subject; it's ok.. they may not be present.");
+ logger.LogWarning(LogHandler.FlattenException(ex));
}
- catch (Exception) { }
- if (commonName == null)
+ try
{
- if (dnsNames.Count > 0)
+ if (commonName == null)
{
- commonName = dnsNames[0];
+ logger.LogTrace("no CN present; will use first DNS name (if present)");
+ if (dnsNames.Count > 0)
+ {
+ commonName = dnsNames[0];
+ }
+ else
+ {
+ throw new Exception("No Common Name or DNS SAN provided, unable to enroll");
+ }
}
- else
+
+ request = new SignRequest()
{
- throw new Exception("No Common Name or DNS SAN provided, unable to enroll");
- }
+ CommonName = commonName,
+ AltNames = dnsNames.Count > 0 ? string.Join(",", dnsNames) : null,
+ Format = "pem_bundle",
+ CSR = csr
+ };
+
+ logger.LogTrace($"sending request to vault..");
+ logger.LogTrace($"serialized request: {JsonSerializer.Serialize(request)}");
+ response = await _vaultHttp.PostAsync>($"sign/{roleName}", request);
+ logger.LogTrace($"got a response from vault..");
+
+ if (response.Warnings?.Count > 0) { logger.LogTrace($"the response contained warnings: {string.Join(", ", response.Warnings)}"); }
+
+ logger.LogTrace($"serialized SignResponse: {JsonSerializer.Serialize(response.Data)}");
+
+ return response.Data;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"There was an error when submitting the request to Vault: {LogHandler.FlattenException(ex)}");
+ throw;
}
+ finally
+ {
+ logger.MethodExit();
+ }
+ }
- reqOptions.CommonName = commonName;
- reqOptions.SubjectAlternativeNames = string.Join(",", dnsNames);
- //reqOptions.IPSubjectAlternativeNames
- reqOptions.TimeToLive =
+ public async Task GetCertificate(string certSerial)
+ {
+ logger.MethodEntry();
+ logger.LogTrace($"requesting the certificate with serial number: {certSerial}");
+
+ try
+ {
+ var response = await _vaultHttp.GetAsync($"cert/{certSerial}");
+ logger.LogTrace($"successfully received a response for certificate with serial number: {certSerial}");
+ return response;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"an error occurred attempting to retrieve certificate: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ finally
+ {
+ logger.MethodExit();
+ }
}
-
+ public async Task RevokeCertificate(string serial)
+ {
+ logger.MethodEntry();
+ logger.LogTrace($"making request to revoke cert with serial: {serial}");
+ try
+ {
+ var response = await _vaultHttp.PostAsync("revoke", new RevokeRequest(serial));
+ logger.LogTrace($"successfully revoked cert with serial {serial}, revocation time: {response.RevocationTime}");
+ return response;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"an error occurred when attempting to revoke the certificate: {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
+ }
+ public async Task PingServer()
+ {
+ logger.MethodEntry();
+ logger.LogTrace($"performing a system health check request to Vault");
+ try
+ {
+ var res = await _vaultHttp.HealthCheckAsync();
+ logger.LogTrace($"-- Vault health check response --");
+ logger.LogTrace($"Vault version : {res.VaultVersion}");
+ logger.LogTrace($"sealed? : {res.Sealed}");
+ logger.LogTrace($"initialized? : {res.Initialized}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Vault healthcheck failed with error: {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ logger.MethodExit();
+ }
+ }
+
+ ///
+ /// Retreives all serial numbers for issued certificates
+ ///
+ /// a list of the certificate serial number strings
+ public async Task> GetAllCertSerialNumbers()
+ {
+ logger.MethodEntry();
+ var keys = new List();
+ try
+ {
+ var res = await _vaultHttp.GetAsync>("certs/?list=true");
+ return res.Data.Entries;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"there was an error retreiving the certificate keys: {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
+ }
+
+ private async Task> GetRevokedSerialNumbers()
+ {
+ logger.MethodEntry();
+ var keys = new List();
+ try
+ {
+ var res = await _vaultHttp.GetAsync("certs/revoked");
+ keys = res.Entries;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"there was an error retreiving the revoked certificate keys: {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
+ return keys;
+ }
+
+ public async Task> GetRoleNamesAsync()
+ {
+ logger.MethodEntry();
+ var roleNames = new List();
+ try
+ {
+ logger.LogTrace("getting the role names as a wrapped keyed-list response..");
+ var response = await _vaultHttp.GetAsync>("roles/?list=true");
+ logger.LogTrace($"received {response.Data?.Entries?.Count} role names (or product IDs)");
+ return response.Data?.Entries;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"There was a problem retreiving the PKI role names: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
+ }
+
+ private void SetClientValuesFromConfigs(HashicorpVaultCAConfig caConfig, HashicorpVaultCATemplateConfig templateConfig)
+ {
+ logger.MethodEntry();
- // example using vaultsharp:
- // var signCertificateRequestOptions = new SignCertificateRequestOptions { // initialize };
- // Secret certSecret = await vaultClient.V1.Secrets.PKI.SignCertificateAsync(pkiRoleName, signCertificateRequestOptions);
- // string certificateContent = certSecret.Data.CertificateContent;
+ var hostUrl = caConfig.Host; // host url and authentication details come from the CA config
+ var token = caConfig.Token;
+ var nameSpace = string.IsNullOrEmpty(templateConfig?.Namespace) ? caConfig.Namespace : templateConfig.Namespace; // Namespace comes from templateconfig if available, otherwise defaults to caConfig; can be null
+ var mountPoint = string.IsNullOrEmpty(templateConfig?.MountPoint) ? caConfig.MountPoint : templateConfig.MountPoint; // Mountpoint comes from templateconfig if available, otherwise defaults to caConfig; if null, uses "pki" (Vault Default)
+ mountPoint = mountPoint ?? "pki"; // using the vault default PKI secrets engine mount point if not present in config
+
+ logger.LogTrace($"set value for Host url: {hostUrl}");
+ logger.LogTrace($"set value for authentication token: {token ?? "(not defined)"}");
+ logger.LogTrace($"set value for Namespace: {nameSpace ?? "(not defined)"}");
+ logger.LogTrace($"set value for Mountpoint: {mountPoint}");
+
+ // _certAuthInfo = caConfig?.ClientCertificate;
+ // logger.LogTrace($"set value for Certificate authentication; thumbprint: {_certAuthInfo?.Thumbprint ?? "(missing) - using token authentication"}");
+
+ //if (_token == null && _certAuthInfo == null)
+ //{
+ // throw new MissingFieldException("Either an authentication token or certificate to use for authentication into Vault must be provided.");
+ //}
+
+ _vaultHttp = new VaultHttp(hostUrl, mountPoint, token, nameSpace);
+
+ logger.MethodExit();
+ }
+
+ private static string ConvertSerialToTrackingId(string serialNumber)
+ {
+ // vault returns certificate serial formatted thusly: 17:67:16:b0:b9:45:58:c0:3a:29:e3:cb:d6:98:33:7a:a6:3b:66:c1
+ // we cannot use the ':' character as part of our internal tracking id, but Vault requests can work with either ':' or '-'
+ // so we convert from colon-separated pairs to hyphen separated pairs.
+
+ return serialNumber.Replace(":", "-");
+ }
}
}
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/Client/VaultHttp.cs b/hashicorp-vault-cagateway/Client/VaultHttp.cs
new file mode 100644
index 0000000..c1c5f99
--- /dev/null
+++ b/hashicorp-vault-cagateway/Client/VaultHttp.cs
@@ -0,0 +1,189 @@
+// Copyright 2024 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.Extensions.CAPlugin.HashicorpVault.APIProxy;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+using RestSharp;
+using RestSharp.Serializers.Json;
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.Client
+{
+ ///
+ /// This class encapsulates a restsharp client configured for communication to the Vault API
+ ///
+ public class VaultHttp
+ {
+ private string _mountPoint { get; set; } // not all requests are the the secrets engine, so can't append this permanently to the base path
+ private RestClient _restClient { get; set; }
+ private JsonSerializerOptions _serializerOptions { get; set; }
+
+ private static readonly ILogger logger = LogHandler.GetClassLogger();
+
+ public VaultHttp(string host, string mountPoint, string authToken, string nameSpace = null)
+ {
+ logger.MethodEntry();
+
+ _serializerOptions = new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
+ RespectNullableAnnotations = true,
+ PropertyNameCaseInsensitive = true,
+ PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace,
+ };
+
+ var restClientOptions = new RestClientOptions($"{host.TrimEnd('/')}/v1") { ThrowOnAnyError = true };
+ _restClient = new RestClient(restClientOptions, configureSerialization: s => s.UseSystemTextJson(_serializerOptions));
+
+ _mountPoint = mountPoint.TrimStart('/').TrimEnd('/'); // remove leading and trailing slashes
+ _restClient.AddDefaultHeader("X-Vault-Request", "true");
+ _restClient.AddDefaultHeader("X-Vault-Token", authToken);
+
+ if (nameSpace != null) _restClient.AddDefaultHeader("X-Vault-Namespace", nameSpace);
+
+ logger.LogTrace($"configured an instance of our restsharp client with the provided values:");
+ logger.LogTrace($"host url: {host}");
+ logger.LogTrace($"mount point: {_mountPoint}");
+ logger.LogTrace($"namespace: {nameSpace}");
+
+ logger.MethodExit();
+ }
+
+ ///
+ /// Makes a request to the configured endpoint and provided path using the GET HTTP verb.
+ ///
+ /// The path to the resource where we will send the GET request.
+ /// A dictionary of values to be passed along with the request as query parameters.
+ ///
+ ///
+
+ public async Task GetAsync(string path, Dictionary parameters = null)
+ {
+ logger.MethodEntry();
+ logger.LogTrace($"preparing to send GET request to {path} with parameters {JsonSerializer.Serialize(parameters)}");
+ logger.LogTrace($"will attempt to deserialize the response into a {typeof(T)}");
+ try
+ {
+ var request = new RestRequest($"{_mountPoint}/{path}", Method.Get);
+ if (parameters != null) { request.AddJsonBody(parameters); }
+
+ var response = await _restClient.ExecuteGetAsync(request);
+
+ response.ThrowIfError();
+
+ return response.Data;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"there was an error making the request: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ finally
+ {
+ logger.MethodExit();
+ }
+ }
+
+ public async Task PostAsync(string path, dynamic parameters = default)
+ {
+ logger.MethodEntry();
+
+ var resourcePath = $"{_mountPoint}/{path}";
+
+ logger.LogTrace($"preparing to send POST request to {_restClient.Options.BaseUrl}{resourcePath}");
+ logger.LogTrace($"will attempt to deserialize the response into a {typeof(T)}");
+
+ try
+ {
+ var request = new RestRequest(resourcePath, Method.Post);
+ if (parameters != null)
+ {
+ string serializedParams = JsonSerializer.Serialize(parameters, _serializerOptions);
+ logger.LogTrace($"serialized parameters (from {parameters.GetType()?.Name}): {serializedParams}");
+ request.AddJsonBody(serializedParams);
+ }
+
+ logger.LogTrace($"full url for the request: {_restClient.Options.BaseUrl}/{request.Resource}");
+
+ var response = await _restClient.ExecutePostAsync(request);
+
+ logger.LogTrace($"request completed. response returned:");
+ logger.LogTrace($"response.StatusCode: {response!.StatusCode}");
+ logger.LogTrace($"response.contentType: {response!.ContentType}");
+
+ if (response.ErrorMessage != null) logger.LogTrace($"response.ErrorMessage: {response!.ErrorMessage}");
+
+ ErrorResponse errorResponse = null;
+
+ if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
+ {
+ errorResponse = JsonSerializer.Deserialize(response.Content!);
+ string allErrors = "(Bad Request)";
+ if (errorResponse?.Errors.Count > 0)
+ {
+ allErrors = string.Join(",", errorResponse.Errors);
+ }
+ logger.LogTrace($"errors: {allErrors}");
+ throw new Exception(allErrors);
+ }
+ return response.Data;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"there was an error making the request: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ finally
+ {
+ logger.MethodExit();
+ }
+ }
+
+ public async Task HealthCheckAsync()
+ {
+ logger.MethodEntry();
+
+ try
+ {
+ return await _restClient.GetAsync("sys/seal-status");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"there was an error making the request: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
+ }
+
+ ///
+ /// gets the capabilities for the current token in the given namespace.
+ /// using this method to verify connectivity
+ ///
+ ///
+ public async Task> GetCapabilitiesForThisTokenAndNamespace()
+ {
+ logger.MethodEntry();
+ try
+ {
+ var response = await _restClient.GetAsync("sys/capabilities/self");
+ response!.ThrowIfError();
+ return response.Content?.data?.capabilities as List;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"request to get capabilities for token failed: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
+ }
+ }
+}
diff --git a/hashicorp-vault-cagateway/Connectors/manifest.json b/hashicorp-vault-cagateway/Connectors/manifest.json
new file mode 100644
index 0000000..66ca7df
--- /dev/null
+++ b/hashicorp-vault-cagateway/Connectors/manifest.json
@@ -0,0 +1,10 @@
+{
+ "extensions": {
+ "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
+ "HashicorpVaultCAPlugin": {
+ "assemblypath": "../HashicorpVaultCAPlugin.dll",
+ "TypeFullName": "Keyfactor.Extensions.CAPlugin.HashicorpVault.HashicorpVaultCAConnector"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/Constants.cs b/hashicorp-vault-cagateway/Constants.cs
index 2eebf69..d82b13b 100644
--- a/hashicorp-vault-cagateway/Constants.cs
+++ b/hashicorp-vault-cagateway/Constants.cs
@@ -1,13 +1,31 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+// Copyright 2024 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.AnyGateway.HashicorpVault
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault
{
- public class Constants
- {
- //Define any constants needed here (mostly field names for config parameters)
- }
+ public static class Constants
+ {
+ //Define any constants needed here (mostly field names for config parameters)
+ public static class CAConfig
+ {
+ public const string HOST = "Host";
+ public const string MOUNTPOINT = "MountPoint";
+ public const string TOKEN = "Token";
+ public const string CLIENTCERT = "ClientCertificate";
+ public const string NAMESPACE = "Namespace";
+ public const string ENABLED = "Enabled";
+ }
+
+ public static class TemplateConfig
+ {
+ public const string ROLENAME = "RoleName";
+ public const string NAMESPACE = "Namespace";
+ public const string MOUNTPOINT = "MountPoint";
+ public const string TOKEN = "Token";
+ }
+ }
}
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs
index 2e4d89a..0413b0c 100644
--- a/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs
+++ b/hashicorp-vault-cagateway/HashicorpVaultCAConfig.cs
@@ -1,26 +1,41 @@
-// Copyright 2022 Keyfactor
+// Copyright 2024 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 Newtonsoft.Json;
+using System.Text.Json.Serialization;
-namespace Keyfactor.Extensions.AnyGateway.HashicorpVault
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault
{
- public class HashicorpVaultCAConfig
- {
- [JsonProperty("Host")]
- public string Host { get; set; }
+ public class HashicorpVaultCAConfig
+ {
+ [JsonPropertyName(Constants.CAConfig.HOST)]
+ public string Host { get; set; }
- [JsonProperty("EnginePath")]
- public string EnginePath { get; set; }
+ [JsonPropertyName(Constants.CAConfig.MOUNTPOINT)]
+ public string MountPoint { get; set; }
- [JsonProperty("Role")]
- public string Role { get; set; }
+ [JsonPropertyName(Constants.CAConfig.TOKEN)]
+ public string Token { get; set; }
- [JsonProperty("Token")]
- public string Token { get; set; }
- }
+ [JsonPropertyName(Constants.CAConfig.NAMESPACE)]
+ public string Namespace { get; set; }
+
+ [JsonPropertyName(Constants.CAConfig.CLIENTCERT)]
+ public AuthCert ClientCertificate { get; set; }
+
+ [JsonPropertyName(Constants.CAConfig.ENABLED)]
+ public bool Enabled { get; set; }
+ }
+
+ public class AuthCert
+ {
+ public string StoreName { get; set; }
+ public string StoreLocation { get; set; }
+ public string Thumbprint { get; set; }
+ public string CertificatePath { get; set; }
+ public string CertificatePassword { get; set; }
+ }
}
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs
index 40ea92b..e020e9e 100644
--- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs
+++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs
@@ -1,58 +1,59 @@
-using CAProxy.AnyGateway;
-using CAProxy.AnyGateway.Configuration;
-using CAProxy.AnyGateway.Interfaces;
-using CAProxy.AnyGateway.Models;
-using CAProxy.AnyGateway.Models.Configuration;
-using CAProxy.Common;
-using CSS.Common.Logging;
-using CSS.PKI;
-using Keyfactor.Extensions.AnyGateway.HashicorpVault.Client;
+// Copyright 2024 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.AnyGateway.Extensions;
+using Keyfactor.Extensions.CAPlugin.HashicorpVault.APIProxy;
using Keyfactor.Logging;
+using Keyfactor.PKI.Enums.EJBCA;
using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
-using System.Net;
-using System.Runtime.ConstrainedExecution;
-using System.Text;
+using System.Text.Json.Serialization;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using static CAProxy.AnyGateway.Constants;
-namespace Keyfactor.Extensions.AnyGateway.HashicorpVault
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault
{
- public class HashicorpVaultCAConnector : BaseCAConnector, ICAConnectorConfigInfoProvider
+ public class HashicorpVaultCAConnector : IAnyCAPlugin
{
- #region Fields and Constructors
-
- private static readonly ILogger logger = Logging.LogHandler.GetClassLogger();
-
- ///
- /// Provides configuration information for the
- ///
- private HashicorpVaultCAConfig Config { get; set; }
+ private readonly ILogger logger;
+ private HashicorpVaultCAConfig _caConfig { get; set; }
private HashicorpVaultClient _client { get; set; }
- //Define any additional private fields here
+ private ICertificateDataReader _certificateDataReader;
+ private JsonSerializerOptions _serializerOptions;
- #endregion Fields and Constructors
+ public HashicorpVaultCAConnector()
+ {
+ logger = LogHandler.GetClassLogger();
- #region ICAConnector Methods
+ _serializerOptions = new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
+ RespectNullableAnnotations = true,
+ PropertyNameCaseInsensitive = true,
+ PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace,
+ };
+ }
///
/// Initialize the
///
/// The config provider contains information required to connect to the CA.
- public override void Initialize(ICAConnectorConfigProvider configProvider)
+ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader)
{
logger.MethodEntry(LogLevel.Trace);
- string rawConfig = JsonConvert.SerializeObject(configProvider.CAConnectionData);
+ string rawConfig = JsonSerializer.Serialize(configProvider.CAConnectionData);
logger.LogTrace($"serialized config: {rawConfig}");
- Config = JsonConvert.DeserializeObject(rawConfig);
- _client = new HashicorpVaultClient();
- GatewayCertificate
+ _caConfig = JsonSerializer.Deserialize(rawConfig);
logger.MethodExit(LogLevel.Trace);
+ _client = new HashicorpVaultClient(_caConfig);
}
///
@@ -66,36 +67,71 @@ public override void Initialize(ICAConnectorConfigProvider configProvider)
/// The format of the request.
/// The type of the enrollment, i.e. new, renew, or reissue.
///
- public override EnrollmentResult Enroll(ICertificateDataReader certificateDataReader, string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
+ public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType)
{
logger.MethodEntry(LogLevel.Trace);
- Logger.Info($"Begin {enrollmentType} enrollment for {subject}");
+ logger.LogInformation($"Begin {enrollmentType} enrollment for {subject}");
+ string statusMessage;
+ SignResponse signResponse;
+
try
{
- Logger.Debug("Parse subject for Common Name");
+ logger.LogTrace("getting product info");
+ var serializedProductInfo = JsonSerializer.Serialize(productInfo.ProductParameters);
+ logger.LogTrace($"got product info: {serializedProductInfo}");
+ var templateConfig = JsonSerializer.Deserialize(serializedProductInfo);
+ templateConfig!.RoleName = productInfo.ProductID; // product ID corresponds to RoleName
+
+ // create the client
+ logger.LogTrace("instantiating the client..");
+ _client = new HashicorpVaultClient(_caConfig, templateConfig);
+
+ logger.LogDebug("Parse subject for Common Name");
string commonName = ParseSubject(subject, "CN=");
- Logger.Trace($"Common Name: {commonName}");
- var vaultHost = Config.Host;
- var vaultRole = Config.Role;
- var secretEnginePath = Config.EnginePath;
+ logger.LogTrace($"Common Name: {commonName}");
+
+ var vaultRole = templateConfig.RoleName;
+
+ logger.LogTrace($"using vault role name {vaultRole}");
- var res = Client.
+ signResponse = await _client.SignCSR(csr, subject, san, vaultRole);
+ // trace logs
+ logger.LogTrace($"back to calling method");
+ logger.LogTrace($"returned values:");
+ logger.LogTrace($"certificate: contains {signResponse.Certificate.Length} characters");
+ logger.LogTrace($"ca_chain: {signResponse.CAChain.Count} certs in chain");
+ logger.LogTrace($"serial number: {signResponse.SerialNumber}");
+ logger.LogTrace($"issuing CA: {signResponse.IssuingCA}"); // skipped???
+ logger.LogTrace($"expiration: {signResponse.Expiration}");
+ logger.LogTrace($"tracking id from serial: {signResponse.SerialNumber}");
+ logger.LogTrace($"successfully enrolled, returning Enrollment result.");
- return new EnrollmentResult()
+ statusMessage = $"Successfully enrolled certificate {commonName}";
+
+ var enrollmentResult = new EnrollmentResult()
{
- CARequestID = response.serial_number.Replace("-", "").Replace(":", ""),
- Status = (int)PKIConstants.Microsoft.RequestDisposition.ISSUED,
- StatusMessage = $"Successfully enrolled for certificate {subject}",
- Certificate = response.certificate
+ CARequestID = signResponse.SerialNumber,
+ Status = (int)EndEntityStatus.GENERATED,
+ StatusMessage = statusMessage,
+ Certificate = signResponse?.Certificate,
};
- // throw new NotImplementedException();
- }
- catch (Exception ex) {
-
+ logger.LogTrace($"returning enrollmentresult: {JsonSerializer.Serialize(enrollmentResult)}");
+ return enrollmentResult;
}
- logger.MethodExit(LogLevel.Trace);
+ catch (Exception ex)
+ {
+ statusMessage = LogHandler.FlattenException(ex);
+ logger.LogError($"Error when performing enrollment: {statusMessage}");
+ }
+ logger.LogTrace($"returning failed wrappedResponse");
+ return new EnrollmentResult()
+ {
+ Status = (int)EndEntityStatus.FAILED,
+ StatusMessage = statusMessage,
+ CARequestID = string.Empty,
+ };
}
///
@@ -103,20 +139,54 @@ public override EnrollmentResult Enroll(ICertificateDataReader certificateDataRe
///
/// The CA request ID for the certificate.
///
- public override CAConnectorCertificate GetSingleRecord(string caRequestID)
+ public async Task GetSingleRecord(string caRequestID)
{
- // example using vaultsharp:
- // var cert = await vaultClient.V1.Secrets.PKI.ReadCertificateAsync("17:67:16:b0:b9:45:58:c0:3a:29:e3:cb:d6:98:33:7a:a6:3b:66:c1", mountpoint);
+ logger.MethodEntry();
+ logger.LogTrace($"preparing to send request to retrieve certificate with id {caRequestID}");
+ try
+ {
+ var cert = await _client.GetCertificate(caRequestID);
+
+ logger.LogTrace($"got a response from the request..");
+ logger.LogTrace($"revocation time: {cert.RevocationTime}");
+
+ var revoked = cert.RevocationTime != null;
- throw new NotImplementedException();
+ var result = new AnyCAPluginCertificate
+ {
+ CARequestID = caRequestID,
+ Certificate = cert.Certificate,
+ Status = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED,
+ RevocationDate = cert.RevocationTime
+ };
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"There was an error retrieving the certificate: {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
}
///
/// Attempts to reach the CA over the network.
///
- public override void Ping()
+ public async Task Ping()
{
- throw new NotImplementedException();
+ logger.MethodEntry();
+ logger.LogTrace("Attempting ping of Vault endpoint");
+ try
+ {
+ var result = await _client.PingServer();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Ping attempt failed with error: {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
}
///
@@ -125,13 +195,23 @@ public override void Ping()
/// The CA request ID.
/// The hex-encoded serial number.
/// The revocation reason.
- ///
- public override int Revoke(string caRequestID, string hexSerialNumber, uint revocationReason)
+ /// The status of the request as an int representing EndEntityStatus
+ public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason)
{
- // example using vaultsharp:
- // Secret revoke = await vaultClient.V1.Secrets.PKI.RevokeCertificateAsync(serialNumber);
-
- throw new NotImplementedException();
+ logger.MethodEntry();
+ logger.LogTrace($"Sending request to revoke certificate with id: {caRequestID}");
+ try
+ {
+ var response = await _client.RevokeCertificate(caRequestID);
+ logger.LogTrace($"returning 'REVOKED' EndEntityStatus ({(int)EndEntityStatus.REVOKED})");
+ return (int)EndEntityStatus.REVOKED;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"revocation failed with error: {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
}
///
@@ -141,64 +221,239 @@ public override int Revoke(string caRequestID, string hexSerialNumber, uint revo
/// Buffer into which certificates are places from the CA.
/// Information about the last CA sync.
/// The cancellation token.
- public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken)
+ public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken)
{
- throw new NotImplementedException();
+ // !! Any certificates issued outside of this CA Gateway will not necessarily be associated with the role name / (product ID) that was used to generate it
+ // !! since that value is not retreivable after the initial generation.
+
+ logger.MethodEntry();
+ logger.LogTrace("Beginning Synchronization Task..");
+
+ var certSerials = new List();
+ var count = 0;
+
+ try
+ {
+ logger.LogTrace("getting all certificate serial numbers from vault");
+ certSerials = await _client.GetAllCertSerialNumbers();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"failed to retreive serial numbers: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+
+ logger.LogTrace($"got {certSerials.Count()} serial numbers. Begin checking status for each...");
+
+ foreach (var certSerial in certSerials)
+ {
+ CertResponse certFromVault = null;
+ var dbStatus = -1;
+
+ // first, retreive the details from Vault
+ try
+ {
+ logger.LogTrace($"Calling GetCertificate on our client, passing serial number: {certSerial}");
+ certFromVault = await _client.GetCertificate(certSerial);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Failed to retreive details for certificate with serial number {certSerial} from Vault. Errors: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ logger.LogTrace($"converting {certSerial} to database trackingId");
+
+ var trackingId = certSerial.Replace(":", "-"); // we store with '-'; hashi stores with ':'
+
+ // then, check for an existing local entry
+ try
+ {
+ logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database");
+ dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId);
+ }
+ catch
+ {
+ logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added.");
+ }
+
+ if (dbStatus == -1 || fullSync) // it's missing and needs added, or a full sync is requested
+ {
+ logger.LogTrace($"adding cert with serial {trackingId} to the database. fullsync is {fullSync}, and the certificate {(dbStatus == -1 ? "does not yet exist" : "already exists")} in the database.");
+
+ var newCert = new AnyCAPluginCertificate
+ {
+ CARequestID = trackingId,
+ Certificate = certFromVault.Certificate,
+ Status = certFromVault.RevocationTime != null ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED,
+ RevocationDate = certFromVault.RevocationTime,
+ };
+
+ try
+ {
+ logger.LogTrace($"writing the result.");
+ blockingBuffer.Add(newCert);
+ logger.LogTrace($"successfully added certificate to the database.");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Failed to add the cert to the database: {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+ }
+ else // the cert exists in the database; just update the status if necessary
+ {
+ var revoked = certFromVault.RevocationTime != null;
+ var vaultStatus = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED;
+ if (vaultStatus != dbStatus) // if there is a mismatch, we need to update
+ {
+ var newCert = new AnyCAPluginCertificate
+ {
+ CARequestID = trackingId,
+ Certificate = certFromVault.Certificate,
+ Status = vaultStatus,
+ RevocationDate = certFromVault.RevocationTime
+ // ProductID is not available via the API after the initial issuance. we do not want to overwrite
+ };
+ }
+ }
+ count++;
+ }
+ logger.LogTrace($"Completed sync of {count} certificates");
+ logger.MethodExit();
}
///
/// Validates that the CA connection info is correct.
///
/// The information used to connect to the CA.
- public override void ValidateCAConnectionInfo(Dictionary connectionInfo)
+ public async Task ValidateCAConnectionInfo(Dictionary connectionInfo)
{
- throw new NotImplementedException();
- }
+ logger.MethodEntry();
- ///
- /// Validates that the product information for the CA is correct
- ///
- /// The product information.
- /// The CA connection information.
- public override void ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo)
- {
- throw new NotImplementedException();
- }
+ // first, we check to see if the CA Gateway is enabled in the configuration
+ if (!(bool)connectionInfo[Constants.CAConfig.ENABLED])
+ {
+ logger.LogWarning($"The CA is currently in the Disabled state. It must be Enabled to perform operations. Skipping validation...");
+ logger.MethodExit(LogLevel.Trace);
+ return;
+ }
+ List errors = new List();
- [Obsolete]
- public override EnrollmentResult Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType)
- {
- throw new NotImplementedException();
- }
+ // then, we make sure required fields are defined..
+ if (string.IsNullOrEmpty(connectionInfo[Constants.CAConfig.HOST] as string))
+ {
+ errors.Add($"The '{Constants.CAConfig.HOST}' is required.");
+ }
- [Obsolete]
- public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken, string logicalName)
- {
- throw new NotImplementedException();
- }
+ if (string.IsNullOrEmpty(connectionInfo[Constants.CAConfig.MOUNTPOINT] as string))
+ {
+ errors.Add($"The '{Constants.CAConfig.MOUNTPOINT}' is required.");
+ }
- #endregion ICAConnector Methods
+ // make sure an authentication mechanism is defined (either certificate or token)
+ var token = connectionInfo[Constants.CAConfig.TOKEN] as string;
+ var cert = connectionInfo[Constants.CAConfig.CLIENTCERT] as string;
- #region ICAConnectorConfigInfoProvider Methods
+ if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(cert))
+ {
+ errors.Add("Either an authentication token or client certificate must be defined for authentication into Vault.");
+ }
+ if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(cert))
+ {
+ logger.LogWarning("Both an authentication token and client certificate are defined. Using the token for authentication.");
+ }
- ///
- /// Returns the default CA connector section of the config file.
- ///
- ///
- public Dictionary GetDefaultCAConnectorConfig()
- {
- return new Dictionary()
+ // if any errors, throw
+ if (errors.Any())
{
- };
+ var allErrors = string.Join("\n", errors);
+ logger.LogError($"validation failed with errors: {allErrors}");
+ throw new AnyCAValidationException(allErrors);
+ }
+
+ // all required fields are present, now let's test the connection
+ HashicorpVaultCAConfig config = null;
+ try
+ {
+ // converting the dictionary of values to our HashicorpVaultCAConfig POCO
+
+ logger.LogTrace("serializing the Dictionary connection info into a JSON string");
+
+ var serializedConfig = JsonSerializer.Serialize(connectionInfo);
+
+ logger.LogTrace($"deserializing the JSON string into a HashicorpVaultCAConfig object: {serializedConfig}");
+
+ config = JsonSerializer.Deserialize(JsonSerializer.Serialize(connectionInfo));
+
+ logger.LogTrace("successfully deserialized the configuration values.");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"There was an error deserializing the configuration values. {LogHandler.FlattenException(ex)}");
+ throw;
+ }
+
+ logger.LogTrace("initializing the Vault client with the configuration parameters.");
+
+ // create an instance of our client with those values
+
+ _client = new HashicorpVaultClient(config);
+
+ // attempt an authenticated request to retreive role names
+ try
+ {
+ logger.LogTrace("making an authenticated request to the Vault server to verify credentials (listing role names)..");
+ var roleNames = await _client.GetRoleNamesAsync();
+ logger.LogTrace($"successfule request: received a response containing {roleNames.Count} role names");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Authenticated request failed. {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
}
///
- /// Gets the default comment on the default product type.
+ /// Validates that the product information for the CA is correct
///
- ///
- public string GetProductIDComment()
+ /// The product information.
+ /// The CA connection information.
+ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo)
{
- return "";
+ logger.MethodEntry();
+ List errors = new List();
+
+ HashicorpVaultCATemplateConfig templateConfig = null;
+ HashicorpVaultCAConfig caConfig = null;
+ // deserialize the values
+ try
+ {
+ templateConfig = JsonSerializer.Deserialize(JsonSerializer.Serialize(productInfo));
+ caConfig = JsonSerializer.Deserialize(JsonSerializer.Serialize(connectionInfo));
+ logger.LogTrace("successfully deserialized the product and CA config values.");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"failed to deserialize configuration values. Please make sure the format is correct.");
+ logger.LogError(LogHandler.FlattenException(ex));
+ throw;
+ }
+ // make sure Role Name is present in the template config
+ if (string.IsNullOrEmpty(productInfo.ProductParameters[Constants.TemplateConfig.ROLENAME] as string))
+ {
+ errors.Add($"The '{Constants.TemplateConfig.ROLENAME}' is required.");
+ }
+
+ // if any errors, throw
+ if (errors.Any())
+ {
+ var allErrors = string.Join("\n", errors);
+ logger.LogError($"Template config validation failed; errors: {allErrors}");
+ throw new AnyCAValidationException(allErrors);
+ }
+ logger.MethodExit();
+ return Task.CompletedTask;
}
///
@@ -207,7 +462,53 @@ public string GetProductIDComment()
///
public Dictionary GetCAConnectorAnnotations()
{
- return new Dictionary();
+ logger.MethodEntry();
+
+ return new Dictionary()
+ {
+ [Constants.CAConfig.HOST] = new PropertyConfigInfo()
+ {
+ Comments = "The hostname URI of the Hashicorp Vault server relative to this gateway host",
+ Hidden = false,
+ DefaultValue = "https://:",
+ Type = "String"
+ },
+ [Constants.CAConfig.NAMESPACE] = new PropertyConfigInfo()
+ {
+ Comments = "Default namespace to use in the path to the Vault PKI secrets engine (Vault Enterprise only). This will only be used if there is no value for Namespace in the Template Parameters.",
+ Hidden = false,
+ DefaultValue = string.Empty,
+ Type = "String"
+ },
+ [Constants.CAConfig.MOUNTPOINT] = new PropertyConfigInfo()
+ {
+ Comments = "The mount point of the PKI secrets engine",
+ Hidden = false,
+ DefaultValue = "pki",
+ Type = "String"
+ },
+ [Constants.CAConfig.TOKEN] = new PropertyConfigInfo()
+ {
+ Comments = "The authentication token to use when authenticating into Vault",
+ Hidden = true,
+ DefaultValue = string.Empty,
+ Type = "String"
+ },
+ //[Constants.CAConfig.CLIENTCERT] = new PropertyConfigInfo()
+ //{
+ // Comments = "The client certificate information used to authenticate with Vault (if configured to use certificate authentication). This can be either a Windows cert store name and location (e.g. 'My' and 'LocalMachine' for the Local Computer personal cert store) and thumbprint, or a PFX file and password.",
+ // Hidden = false,
+ // DefaultValue = string.Empty,
+ // Type = "ClientCertificate"
+ //},
+ [Constants.CAConfig.ENABLED] = new PropertyConfigInfo()
+ {
+ Comments = "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available",
+ Hidden = false,
+ DefaultValue = true,
+ Type = "Boolean"
+ }
+ };
}
///
@@ -216,20 +517,54 @@ public Dictionary GetCAConnectorAnnotations()
///
public Dictionary GetTemplateParameterAnnotations()
{
- throw new NotImplementedException();
+ logger.MethodEntry();
+ return new Dictionary()
+ {
+ [Constants.TemplateConfig.NAMESPACE] = new PropertyConfigInfo()
+ {
+ Comments = "OPTIONAL: The namespace of the path to the PKI engine (Vault Enterprise); use only if different than the Namespace set in the Connector configuration",
+ Hidden = false,
+ DefaultValue = string.Empty,
+ Type = "String"
+ },
+ [Constants.CAConfig.MOUNTPOINT] = new PropertyConfigInfo()
+ {
+ Comments = "The mount point of the PKI secrets engine; if empty, will use value from the Connector configuration.",
+ Hidden = false,
+ DefaultValue = string.Empty,
+ Type = "String"
+ },
+ };
}
///
- /// Gets default template map parameters for GlobalSign Atlas product types.
+ /// The product id's typically correspond to certificate types (TLS, Client Auth, etc.)
+ /// In the case of Hashicorp Vault, there aren't built-in product ID's. We are using the PKI Role name.
///
///
- public Dictionary GetDefaultTemplateParametersConfig()
+ public List GetProductIds()
{
- throw new NotImplementedException();
+ logger.MethodEntry();
+ // Initialize should have been called in order to populate the caConfig and create the client.
+ try
+ {
+ logger.LogTrace("requesting role names from vault..");
+ var roleNames = _client.GetRoleNamesAsync().Result;
+ logger.LogTrace($"got {roleNames.Count} role names from vault:");
+ foreach (var name in roleNames)
+ {
+ logger.LogTrace(name);
+ }
+ return roleNames;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Error retreiving role names: {ex.Message}");
+ throw;
+ }
+ finally { logger.MethodExit(); }
}
- #endregion ICAConnectorConfigInfoProvider Methods
-
#region Helper Methods
private static string ParseSubject(string subject, string rdn)
diff --git a/hashicorp-vault-cagateway/HashicorpVaultCATemplateConfig.cs b/hashicorp-vault-cagateway/HashicorpVaultCATemplateConfig.cs
new file mode 100644
index 0000000..76a5145
--- /dev/null
+++ b/hashicorp-vault-cagateway/HashicorpVaultCATemplateConfig.cs
@@ -0,0 +1,26 @@
+// Copyright 2024 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.Text.Json.Serialization;
+
+namespace Keyfactor.Extensions.CAPlugin.HashicorpVault
+{
+ public class HashicorpVaultCATemplateConfig
+ {
+ [JsonPropertyName(Constants.TemplateConfig.ROLENAME)]
+ public string RoleName { get; set; }
+
+ [JsonPropertyName(Constants.TemplateConfig.NAMESPACE)]
+ public string Namespace { get; set; }
+
+ [JsonPropertyName(Constants.TemplateConfig.MOUNTPOINT)]
+ public string MountPoint { get; set; }
+
+ [JsonPropertyName(Constants.TemplateConfig.TOKEN)]
+ public string Token { get; set; }
+ }
+}
diff --git a/hashicorp-vault-cagateway/Properties/AssemblyInfo.cs b/hashicorp-vault-cagateway/Properties/AssemblyInfo.cs
deleted file mode 100644
index 8b68512..0000000
--- a/hashicorp-vault-cagateway/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("cagateway-template")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("cagateway-template")]
-[assembly: AssemblyCopyright("Copyright © 2022")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("9d2d6ed9-4626-430c-879d-0fe0febed146")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/hashicorp-vault-cagateway/Properties/launchSettings.json b/hashicorp-vault-cagateway/Properties/launchSettings.json
new file mode 100644
index 0000000..e6a1e54
--- /dev/null
+++ b/hashicorp-vault-cagateway/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "hashicorp-vault-caplugin": {
+ "commandName": "Project",
+ "remoteDebugEnabled": true,
+ "authenticationMode": "None",
+ "nativeDebugging": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/hashicorp-vault-cagateway.csproj b/hashicorp-vault-cagateway/hashicorp-vault-cagateway.csproj
deleted file mode 100644
index 3f12e3e..0000000
--- a/hashicorp-vault-cagateway/hashicorp-vault-cagateway.csproj
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- {9D2D6ED9-4626-430C-879D-0FE0FEBED146}
- Library
- Properties
- keyfactor-anygateway-hashicorp-vault
- hashicorp-vault-cagateway
- v4.7.2
- 512
- true
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
- ..\packages\BouncyCastle.1.8.9\lib\BouncyCastle.Crypto.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.AnyGateway.Core.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.Interfaces.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxyDAL.dll
-
-
- ..\packages\Common.Logging.3.4.1\lib\net40\Common.Logging.dll
-
-
- ..\packages\Common.Logging.Core.3.4.1\lib\net40\Common.Logging.Core.dll
-
-
- ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CommonCAProxy.dll
-
-
- ..\packages\CSS.Common.1.7.0\lib\net462\CSS.Common.dll
-
-
- ..\packages\CSS.PKI.2.13.0\lib\net462\CSS.PKI.dll
-
-
- ..\packages\Keyfactor.Logging.1.1.0\lib\netstandard2.0\Keyfactor.Logging.dll
-
-
- ..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll
-
-
- ..\packages\Microsoft.Extensions.Logging.Abstractions.5.0.0\lib\net461\Microsoft.Extensions.Logging.Abstractions.dll
-
-
- ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
-
-
-
- ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll
-
-
-
-
- ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll
-
-
- ..\packages\System.Net.Http.WinHttpHandler.5.0.0\lib\net461\System.Net.Http.WinHttpHandler.dll
-
-
-
- ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll
-
-
- ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll
-
-
- ..\packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll
-
-
- ..\packages\System.Text.Json.8.0.4\lib\net462\System.Text.Json.dll
-
-
- ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll
-
-
- ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll
-
-
-
-
-
-
-
-
- ..\packages\VaultSharp.1.17.5.1\lib\net472\VaultSharp.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj b/hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj
new file mode 100644
index 0000000..95991b8
--- /dev/null
+++ b/hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj
@@ -0,0 +1,66 @@
+
+
+
+ net6.0
+ Keyfactor.Extensions.CAPlugin.HashicorpVault
+ disable
+ warnings
+ HashicorpVaultCAPlugin
+ False
+ True
+ 12.0
+ False
+ False
+
+
+
+ embedded
+ False
+ True
+ bin
+ False
+ False
+
+
+
+ embedded
+ True
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway/packages.config b/hashicorp-vault-cagateway/packages.config
index bd62d6c..6fd6cf0 100644
--- a/hashicorp-vault-cagateway/packages.config
+++ b/hashicorp-vault-cagateway/packages.config
@@ -21,5 +21,4 @@
-
\ No newline at end of file
diff --git a/hashicorp-vault-cagateway.sln b/hashicorp-vault-caplugin.sln
similarity index 79%
rename from hashicorp-vault-cagateway.sln
rename to hashicorp-vault-caplugin.sln
index 593cc10..d64f73c 100644
--- a/hashicorp-vault-cagateway.sln
+++ b/hashicorp-vault-caplugin.sln
@@ -1,14 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31729.503
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "hashicorp-vault-cagateway", "hashicorp-vault-cagateway\hashicorp-vault-cagateway.csproj", "{9D2D6ED9-4626-430C-879D-0FE0FEBED146}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "hashicorp-vault-caplugin", "hashicorp-vault-cagateway\hashicorp-vault-caplugin.csproj", "{9D2D6ED9-4626-430C-879D-0FE0FEBED146}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{431498A1-F30A-4307-9FBF-B1D634326444}"
ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
CHANGELOG.md = CHANGELOG.md
integration-manifest.json = integration-manifest.json
+ manifest.json = manifest.json
readme_source.md = readme_source.md
EndProjectSection
EndProject
diff --git a/hashicorp-vault-caplugin.sln.licenseheader b/hashicorp-vault-caplugin.sln.licenseheader
new file mode 100644
index 0000000..90d8f34
--- /dev/null
+++ b/hashicorp-vault-caplugin.sln.licenseheader
@@ -0,0 +1,36 @@
+extensions: designer.cs generated.cs
+extensions: .cs .cpp .h
+// Copyright %CurrentYear% 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.
+
+extensions: .aspx .ascx
+<%--
+Copyright %CurrentYear% 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.
+--%>
+
+extensions: .vb
+' Copyright %CurrentYear% 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.
+
+extensions: .xml .xsd
+
diff --git a/integration-manifest.json b/integration-manifest.json
index 8c2ac17..e1bd1b7 100644
--- a/integration-manifest.json
+++ b/integration-manifest.json
@@ -4,9 +4,46 @@
"name": "Hashicorp Vault AnyCA REST Gateway Plugin",
"status": "prototype",
"support_level": "community",
- "link_github": false,
+ "link_github": true,
"update_catalog": false,
- "description": "Hashicorp Vault PKI Secrets Engine integration using AnyCA REST Gateway framework",
+ "description": "Hashicorp Vault plugin for the AnyCA REST Gateway Framework",
"gateway_framework": "24.2.0",
- "release_dir": "UPDATE-THIS-WITH-PATH-TO-BINARY-BUILD-FOLDER"
+ "release_dir": "hashicorp-vault-cagateway/bin/Release",
+ "about": {
+ "carest": {
+ "product_ids": [],
+ "ca_plugin_config": [
+ {
+ "name": "Host",
+ "description": "REQUIRED: The host URI of the Hashicorp Vault server relative to this gateway host"
+ },
+ {
+ "name": "MountPoint",
+ "description": "REQUIRED: The mount point of the PKI secrets engine. This will only be used if there is no value for MountPoint in the template parameters."
+ },
+ {
+ "name": "Namespace",
+ "description": "OPTIONAL: Default namespace to use in the path to the Vault PKI secrets engine (Vault Enterprise only). This will only be used if there is no value for Namespace in the Template parameters."
+ },
+ {
+ "name": "Token",
+ "description": "REQUIRED: The authentication token to use when authenticating into Vault"
+ },
+ {
+ "name": "Enabled",
+ "description": "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available"
+ }
+ ],
+ "enrollment_config": [
+ {
+ "name": "MountPoint",
+ "description": "OPTIONAL: The mount point of the PKI secrets engine. If provided, will override values set in the CA configuration for enrollment operations."
+ },
+ {
+ "name": "Namespace",
+ "description": "OPTIONAL: Default namespace to use in the path to the Vault PKI secrets engine (Vault Enterprise only). If provided, will override values set in the CA configuration for enrollment operations."
+ }
+ ]
+ }
+ }
}
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..66ca7df
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,10 @@
+{
+ "extensions": {
+ "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
+ "HashicorpVaultCAPlugin": {
+ "assemblypath": "../HashicorpVaultCAPlugin.dll",
+ "TypeFullName": "Keyfactor.Extensions.CAPlugin.HashicorpVault.HashicorpVaultCAConnector"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/readme_source.md b/readme_source.md
index a0b9c5c..20d51d2 100644
--- a/readme_source.md
+++ b/readme_source.md
@@ -1,147 +1,128 @@
-# Introduction
+# Introduction
This AnyGateway plug-in enables issuance, revocation, and synchronization of certificates from the Hashicorp Vault PKI Secrets Engine.
# Hashicorp Vault Authentication
-This plug-in supports two types of authentication into Hashicorp Vault.
-1. Token
-1. Certificate
-
-When filling in the configuration values, if a value for "AuthToken" is present, it will be used. If not, then the values for certificate location should be populated for Authentication via certificate.
+Currently this plug-in only supports authentication into Vault via Token.
# Prerequisites
+1. An instance of Hashicorp Vault v10.5+ that is accessible from the CA Gateway host
+1. An instance of the CA Gateway Framework (REST version)
## Certificate Chain
In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you create your Root and/or Subordinate CA, make sure to import the certificate chain into the AnyGateway and Command Server certificate store
-# Install
-* Download latest successful build from [GitHub Releases](../../releases/latest)
+# Installation
-* Copy .dll to the Program Files\Keyfactor\Keyfactor AnyGateway directory
+## Requirements
+Make sure the following information is available, as it will be needed to complete the installation.
-* Update the CAProxyServer.config file
- * Update the CAConnection section to point at the DigiCertCAProxy class
- ```xml
-
- ```
+- The fully qualified URI of the instance of Hashicorp Vault
+- The namespace and mountpoint of the instance of the PKI secrets engine running in Vault
+- An authentication token that has sufficient authority to perform operations on the PKI Secrets engine
+- PKI Secrets Engine Roles defined that will correspond to certificate templates to be used when signing certificates with the CA.
-# Configuration
-The following sections will breakdown the required configurations for the AnyGatewayConfig.json file that will be imported to configure the AnyGateway.
-
-## Templates
-The Template section will map the CA's products to an AD template.
-* ```ProductID```
-This is the ID of the product to map to the specified template.
-
- ```json
- "Templates": {
- "WebServer": {
- "ProductID": "",
- "Parameters": {
- }
- }
-}
- ```
-
-## Security
-The security section does not change specifically for the Hashicorp Vault PKI CA Gateway. Refer to the AnyGateway Documentation for more detail.
-```json
- /*Grant permissions on the CA to users or groups in the local domain.
- READ: Enumerate and read contents of certificates.
- ENROLL: Request certificates from the CA.
- OFFICER: Perform certificate functions such as issuance and revocation. This is equivalent to "Issue and Manage" permission on the Microsoft CA.
- ADMINISTRATOR: Configure/reconfigure the gateway.
- Valid permission settings are "Allow", "None", and "Deny".*/
- "Security": {
- "Keyfactor\\Administrator": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- },
- "Keyfactor\\gateway_test": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- },
- "Keyfactor\\SVC_TimerService": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "None"
- },
- "Keyfactor\\SVC_AppPool": {
- "READ": "Allow",
- "ENROLL": "Allow",
- "OFFICER": "Allow",
- "ADMINISTRATOR": "Allow"
- }
- }
-```
-## CerificateManagers
-The Certificate Managers section is optional.
- If configured, all users or groups granted OFFICER permissions under the Security section
- must be configured for at least one Template and one Requester.
- Uses "" to specify all templates. Uses "Everyone" to specify all requesters.
- Valid permission values are "Allow" and "Deny".
-```json
- "CertificateManagers":{
- "DOMAIN\\Username":{
- "Templates":{
- "MyTemplateShortName":{
- "Requesters":{
- "Everyone":"Allow",
- "DOMAIN\\Groupname":"Deny"
- }
- },
- "":{
- "Requesters":{
- "Everyone":"Allow"
- }
- }
- }
- }
- }
-```
-## CAConnection
-The CA Connection section will determine the API endpoint and configuration data used to connect to the API.
+### Steps
+1. Install the AnyCA Gateway Rest per the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/InstallIntroduction.htm).
+1. Download latest successful build from [GitHub Releases](https://github.com/Keyfactor/hashicorp-vault-caplugin/releases/latest)
+1. Copy the contents of the release zip file into the (AnyGatewayRest Installation Folder)\AnyGatewayREST\net6.0\Extensions AnyGateway directory.
+1. The _manifest.json_ tells the Gateway how to locate our plugin. It should be copied to the *Connectors* sub-folder in the AnyCA Gateway Rest installation path.
+1. Restart the gateway service.
+1. Navigate to the AnyCA Gateway Rest portal and verify that the Gateway recognizes the Hashicorp Vault CA plugin by hovering over the ⓘ symbol to the right of the Gateway name.
+#### _manifest.json_
```json
- "CAConnection": {
- "AuthToken":"",
- "ClientCertificate": {
- "StoreName": "My",
- "StoreLocation": "LocalMachine",
- "Thumbprint": "0123456789abcdef"
- },
- "Name": "TestUser",
- "Email": "email@email.invalid",
- "PhoneNumber": "0000000000",
- "IgnoreExpired": "false"
- },
-```
-## GatewayRegistration
-There are no specific Changes for the GatewayRegistration section. Refer to the AnyGateway Documentation for more detail.
-```json
- "GatewayRegistration": {
- "LogicalName": "CASandbox",
- "GatewayCertificate": {
- "StoreName": "CA",
- "StoreLocation": "LocalMachine",
- "Thumbprint": "0123456789abcdef"
+{
+ "extensions": {
+ "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": {
+ "HashicorpVaultCAPlugin": {
+ "assemblypath": "../HashicorpVaultCAPlugin.dll",
+ "TypeFullName": "Keyfactor.Extensions.CAPlugin.HashicorpVault.HashicorpVaultCAConnector"
+ }
}
}
+}
```
-## ServiceSettings
-There are no specific Changes for the ServiceSettings section. Refer to the AnyGateway Documentation for more detail.
-```json
- "ServiceSettings": {
- "ViewIdleMinutes": 8,
- "FullScanPeriodHours": 24,
- "PartialScanPeriodMinutes": 240
- }
-```
+# Configuration
+
+1. Follow the [official AnyCA Gateway REST documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm) to define a new Certificate Authority, and use the notes below to configure the **Gateway Registration** and **CA Connection** tabs:
+
+### Configure the CA in the AnyCA Gateway Rest Portal
+
+
+* **Gateway Registration**
+
+ In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you know your Root and/or Subordinate CA in your Hashicorp Vault instance, make sure to download and import the certificate chain into the Command Server certificate store
+
+ Once the necessary files are copied to the appropriate locations and the AnyCA Gateway Rest is up and running, navigate to the AnyCA Gateway Rest portal and configure the CA.
+
+* **CA Connection**
+
+ Populate using the configuration fields collected in the [requirements](##requirements) section.
+
+ * **Host** - The fully qualified URI including port for the instance of vault. ex: https://127.0.0.1:8001
+ * **Namespace** - If you are utilizing Vault Namespaces (Enterprise feature); the namespace containing the PKI secrets engine containing your CA.
+ * **MountPoint** - The mount point of the PKI secrets engine.
+ * **Token** - The token that will be used by the gateway for authentication. It should have policies defined that ensure it can perform operations on the path defined by `//`
+ * **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.
+
+
+* **Template mapping**
+
+ The product ID's correspond to the role names in the Hashicorp Vault PKI Secrets engine. After the certificate profile is associated with the product ID and imported as a certificate template into Command, requests for certificates will pass the associated role name as part of the request and the issuance policies defined in Vault for that role will be applied as if you were issuing the certificates directly from Vault.
+
+ In order to create create the certificate templates associated with the role names once the CA has been defined in the gateway portal, follow these steps:
+
+ 1. navigate to the "Certificate Profiles" tab
+ 1. Create an entry for each of the PKI secrets engine roles you would like to use for issuing certificates from the Hashicorp Vault CA.
+ 1. Navigate to the "Certificate Authorities" tab and click "Edit"
+ 1. In the "Edit CA" window, navigate to the "Templates" tab.
+ 1. Create an association between each of the certificate profiles we just created with the PKI secrets engine roles retreived from Vault.
+
+### Configure the CA in Keyfactor Command
+
+Now that the AnyCA Gateway Rest is configured with the details of our Hashicorp Vault hosted CA, we will need to define the CA in Keyfactor Command
+
+* **Certificate Authorities**
+ 1. Log into Keyfactor Command with an account that has sufficient permissions to define a new Certificate Authorities.
+ 1. Navigate to "Locations > Certificate Authorities"
+ 1. If the AnyCA Gateway Rest host is Active Directory joined with Command
+ 1. click "Import" to automatically load the details from the Gateway
+ 1. If not Active Directory domain joined, click "Add" in order to manually fill in the details
+ * **Basic**
+ 1. **Logical Name**: The logical name of the CA, as defined in the Gateway Portal.
+ 1. **Host URL**: The host url of the instance of the AnyCA Gateway Rest. This will be the same URL you use to navigate to the Gateway Portal
+ 1. **Configuration Tenant**: this can be any name. It is used by Command to create an Active Directory tenant for the CA.
+ 1. Fill in the rest of the details according to your requirements
+
+ * **Authorization Methods**
+ You will need to have the PFX certificate, including private key, for Keyfactor Command to use when authenticating into the Gateway. This should be the certificate associated with the identifier (thumbprint or serial number) that was provided when the Gateway was installed.
+ 1. Click the "Select authentication certificate" button and choose this PFX file, enter the password if prompted.
+ 1. Click "Save and Test" in order to save the configuration and see the result of Command attempting to authenticate.
+
+
+
+### Troubleshooting
+
+When troubleshooting the Gateway configuration, the log files can be very useful. They are located in the "logs" sub-folder in the gateway installation path.
+
+1. Authentication into the Gateway Portal fails
+
+ - Make sure that the authentication certificate with private key is installed into the "Current User > Personal" certificate store
+ - If you are seeing an error that indicates the gateway is unable to check the CRL for the certificate..
+ - make sure the CRL endpoint is defined on the CA in Vault
+ - If no CRL is available, you can turn off the CRL check on the authentication certificate by the Gateway thusly:
+ - stop the Gateway service on the host
+ - edit the "appsettings.json" file in the Gateway installation directory
+ - Change the value of "CheckClientCRL" to "False"
+ - Restart the gateway
+ - re-attempt login
+
+1. If an error response is returned when attempting to sign or issue certificates via the CA in Command
+ - Check the CA_Gateway_Log.txt file in the "logs" subfolder of the Gateway installation path
+ - Make sure that the Vault PKI Role policies allow issuing certificates with the defined values
+
+