diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe7d44b..6371b2c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ + ## Useful Gists related to VaultSharp usage * Helper code to get Kerberos Negotiation Token using keytab and krb5 config file: https://gist.github.com/rajanadar/28c86d967695262bfe1f17ae82fb3d3d @@ -10,6 +11,7 @@ * auth/azure: Add the ```resource_id``` parameter to the login method. * [GH-365](https://github.com/rajanadar/VaultSharp/issues/365) Add new ```transit key type``` values like ```hmac-key```, ```cmac-key``` etc. * [GH-366](https://github.com/rajanadar/VaultSharp/issues/366) PKI Secrets Engine - return raw cert data, including revocation date + * [GH-373](https://github.com/rajanadar/VaultSharp/issues/373) PKI Secrets Engine - added methods for interacting with PKI Roles, and method for generating a self-signed root CA. ## 1.17.5.1 (September 18, 2024) diff --git a/README.md b/README.md index f0c01cc3..68b53162 100644 --- a/README.md +++ b/README.md @@ -1304,6 +1304,59 @@ var caCert = await vaultClient.V1.Secrets.PKI.ReadCACertificateAsync(Certificate Assert.NotNull(caCert.CertificateContent); ``` +##### Generate Root + - This endpoint generates a new self-signed CA certificate and private key. If the 'type' is `exported`, the private key will be returned in the response; + - if it is `internal` the private key will not be returned and cannot be retrieved later; + - if it is `existing`, the key specified by key_ref will be reused for this root; + - if it is `kms`, a managed key will be used. + + ```cs +var newRoot = new GenerateRootRequest() +{ + CommonName = "test.com", + IssuerName = "testroot_test_com", + KeyName = "testroot_key", +}; +var type = "exported"; +var caCert = await vaultClient.V1.Secrets.PKI.GenerateRootAsync(type, newRoot, mountpoint) +Assert.NotEmpty(newRootResponse.Data.PrivateKey); +``` + +##### List Roles + + - This endpoint lists all of the role names that have been created for the PKI secrets engine. + +```cs +var roleNames = await vaultClient.V1.Secrets.PKI.ListRolesAsync(mountpoint); +Assert.IsList(roleNames); +``` + +##### Read Role + + - This endpoint retreives the details for a role with the specified name. + +```cs +var roleDetails = await vaultClient.V1.Secrets.PKI.ReadRoleAsync(roleName, mountpoint); +``` + +##### Write Role + + - This endpoint writes a new role definition + - If the role name already exists, it will be completely overwritten + +```cs +var roleDetails = await vaultClient.V1.Secrets.PKI.ReadRoleAsync(roleName, roleDetails, mountpoint); +``` + +##### Patch Role + - This endpoint will update an existing role with provided values + - Null values in the request will not overwritten existing values in the role definition + +```cs +var updatedRoleDetails = await vaultClient.V1.Secrets.PKI.PatchRoleAsync(roleName, roleDetails, mountpoint); +``` + + #### RabbitMQ Secrets Engine ##### Generate dynamic DB credentials diff --git a/src/VaultSharp/V1/SecretsEngines/PKI/GenerateRootRequest.cs b/src/VaultSharp/V1/SecretsEngines/PKI/GenerateRootRequest.cs new file mode 100644 index 00000000..291ac2ea --- /dev/null +++ b/src/VaultSharp/V1/SecretsEngines/PKI/GenerateRootRequest.cs @@ -0,0 +1,218 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace VaultSharp.V1.SecretsEngines.PKI +{ + public class GenerateRootRequest + { + /// + /// The requested Subject Alternative Names, if any, in a comma-delimited list. May contain both DNS names and email addresses. + /// + [JsonPropertyName("alt_names")] + public string AltNames { get; set; } + + /// + /// The requested common name; if you want more than one, specify the alternative names in the alt_names map. If not specified when signing, + /// the common name will be taken from the CSR; other names must still be specified in alt_names or ip_sans. + /// + [JsonPropertyName("common_name")] + public string CommonName { get; set; } + + /// + /// If set, Country will be set to this value. + /// + [JsonPropertyName("country")] + public List Country { get; set; } + + /// + /// If true, the Common Name will not be included in DNS or Email Subject Alternate Names. Defaults to false (CN is included). + /// + [JsonPropertyName("exclude_cn_from_sans")] + public bool ExcludeCNFromSans { get; set; } + + /// + /// Format for returned data. Can be "pem", "der", or "pem_bundle". If "pem_bundle", any private key and issuing cert will be appended to the certificate pem. + /// If "der", the value will be base64 encoded. + /// Defaults to "pem". + /// + [JsonPropertyName("format")] + public string Format { get; set; } + + /// + /// The requested IP SANs, if any + /// + [JsonPropertyName("ip_sans")] + public List IpSans { get; set; } + + /// + /// Provide a name to the generated or existing issuer, the name must be unique across all issuers and not be the reserved value 'default' + /// + [JsonPropertyName("issuer_name")] + public string IssuerName { get; set; } + + /// + /// The number of bits to use. Allowed values are 0 (universal default); + /// with rsa key_type: 2048 (default), 3072, 4096 or 8192; + /// with ec key_type: 224, 256 (default), 384, or 521; + /// ignored with ed25519. + /// + [JsonPropertyName("key_bits")] + public int KeyBits { get; set; } + + /// + /// Provide a name to the generated or existing key, the name must be unique across all keys and not be the reserved value 'default' + /// + [JsonPropertyName("key_name")] + public string KeyName { get; set; } + + /// + /// Reference to a existing key; either "default" for the configured default key, an identifier or the name assigned to the key. + /// + [JsonPropertyName("key_ref")] + public string KeyRef { get; set; } + + /// + /// The type of key to use; + /// defaults to RSA. + /// "rsa" "ec" and "ed25519" are the only valid values. + /// + [JsonPropertyName("key_type")] + public string KeyType { get; set; } + + /// + /// If set, Locality will be set to this value. + /// + [JsonPropertyName("locality")] + public List Locality { get; set; } + + /// + /// The name of the managed key to use when the exported type is kms. When kms type is the key type, this field or managed_key_name is required. + /// Ignored for other types. + /// + [JsonPropertyName("managed_key_id")] + public string ManagedKeyId { get; set; } + + /// + /// The name of the managed key to use when the exported type is kms. When kms type is the key type, this field or managed_key_id is required. + /// Ignored for other types. + /// + [JsonPropertyName("managed_key_name")] + public string ManagedKeyName { get; set; } + + /// + /// The maximum allowable path length + /// + [JsonPropertyName("max_path_length")] + public int MaxPathLength { 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 + /// + [JsonPropertyName("not_after")] + public string NotAfter { get; set; } + + /// + /// The duration before now which the certificate needs to be backdated by. + /// + [JsonPropertyName("not_before_duration")] + public string NotBeforeDuration { get; set; } + + /// + /// If set, O (Organization) will be set to this value. + /// + [JsonPropertyName("organization")] + public List Organization { get; set; } + + /// + /// Requested other SANs, in an array with the format ;UTF8: for each entry. + /// + [JsonPropertyName("other_sans")] + public List OtherSans { get; set; } + + /// + /// If set, OU (OrganizationalUnit) will be set to this value. + /// + [JsonPropertyName("ou")] + public List OrganizationalUnit { get; set; } + + /// + /// Domains for which this certificate is allowed to sign or issue child certificates. + /// If set, all DNS names (subject and alt) on child certs must be exact matches or subsets of the given domains + /// (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10). + /// + [JsonPropertyName("permitted_dns_domains")] + public List PermittedDnsDomains { get; set; } + + /// + /// If set, Postal Code will be set to this value. + /// + [JsonPropertyName("postal_code")] + public List PostalCode { get; set; } + + /// + /// Format for the returned private key. + /// Generally the default will be controlled by the "format" parameter as either base64-encoded DER or PEM-encoded DER. + /// However, this can be set to "pkcs8" to have the returned private key contain base64-encoded pkcs8 or PEM-encoded pkcs8 instead. + /// Defaults to "der". + /// + [JsonPropertyName("private_key_format")] + public string PrivateKeyFormat { get; set; } + + /// + /// If set, Province will be set to this value. + /// + [JsonPropertyName("province")] + public List Province { get; set; } + + /// + /// The Subject's requested serial number, if any. See RFC 4519 Section 2.31 'serialNumber' for a description of this field. + /// If you want more than one, specify alternative names in the alt_names map using OID 2.5.4.5. + /// This has no impact on the final certificate's Serial Number field. + /// + [JsonPropertyName("serial_number")] + public string SerialNumber { get; set; } + + /// + /// The number of bits to use in the signature algorithm; accepts 256 for SHA-2-256, 384 for SHA-2-384, and 512 for SHA-2-512. + /// Defaults to 0 to automatically detect based on key length (SHA-2-256 for RSA keys, and matching the curve size for NIST P-Curves). + /// + [JsonPropertyName("signature_bits")] + public int SignatureBits { get; set; } + + /// + /// If set, Street Address will be set to this value. + /// + [JsonPropertyName("street_address")] + public List StreetAddress { get; set; } + + /// + /// The requested Time To Live for the certificate; sets the expiration date. + /// If not specified the role default, backend default, or system default TTL is used, in that order. Cannot be larger than the mount max TTL. + /// Note: this only has an effect when generating a CA cert or signing a CA cert, not when generating a CSR for an intermediate CA. + /// + [JsonPropertyName("ttl")] + public string TTL { get; set; } + + /// + /// The requested URI SANs, if any, in a comma-delimited list. + /// + [JsonPropertyName("uri_sans")] + public List UriSans { get; set; } + + /// + /// Whether or not to use PSS signatures when using a RSA key-type issuer. Defaults to false. + /// + [JsonPropertyName("use_pss")] + public bool UsePss { get; set; } + + public GenerateRootRequest() + { + //set defaults + Format = "pem"; + KeyType = "rsa"; + IssuerName = string.Empty; + KeyName = string.Empty; + } + } +} diff --git a/src/VaultSharp/V1/SecretsEngines/PKI/GenerateRootResponse.cs b/src/VaultSharp/V1/SecretsEngines/PKI/GenerateRootResponse.cs new file mode 100644 index 00000000..16c7af9e --- /dev/null +++ b/src/VaultSharp/V1/SecretsEngines/PKI/GenerateRootResponse.cs @@ -0,0 +1,61 @@ +using System.Text.Json.Serialization; + +namespace VaultSharp.V1.SecretsEngines.PKI +{ + public class GenerateRootResponse + { + /// + /// The generated self-signed CA certificate. + /// + [JsonPropertyName("certificate")] + public string Certificate { get; set; } + + /// + /// The expiration of the given issuer. + /// + [JsonPropertyName("expiration")] + public long Expiration { get; set; } + + /// + /// The ID of the issuer + /// + [JsonPropertyName("issuer_id")] + public string IssuerId { get; set; } + + /// + /// The name of the issuer. + /// + [JsonPropertyName("issuer_name")] + public string IssuerName { get; set; } + + /// + /// The issuing certificate authority. + /// + [JsonPropertyName("issuing_ca")] + public string IssuingCA { get; set; } + + /// + /// The ID of the key. + /// + [JsonPropertyName("key_id")] + public string KeyId { get; set; } + + /// + /// The key name if given. + /// + [JsonPropertyName("key_name")] + public string KeyName { get; set; } + + /// + /// The private key if exported was specified + /// + [JsonPropertyName("private_key")] + public string PrivateKey { get; set; } + + /// + /// The requested Subject's named serial number. + /// + [JsonPropertyName("serial_number")] + public string SerialNumber { get; set; } + } +} diff --git a/src/VaultSharp/V1/SecretsEngines/PKI/IPKISecretsEngine.cs b/src/VaultSharp/V1/SecretsEngines/PKI/IPKISecretsEngine.cs index 0a3fca95..0ae8e894 100644 --- a/src/VaultSharp/V1/SecretsEngines/PKI/IPKISecretsEngine.cs +++ b/src/VaultSharp/V1/SecretsEngines/PKI/IPKISecretsEngine.cs @@ -200,5 +200,90 @@ public interface IPKISecretsEngine /// The secret with the certificate chain data /// Task> ReadDefaultIssuerCertificateChainAsync(CertificateFormat certificateFormat, string pkiBackendMountPoint = null); + + /// + /// Generates a new self-signed CA certificate and private key. If the path ends with exported, the private key will be returned in the response; + /// if it is internal the private key will not be returned and cannot be retrieved later; + /// if it is existing, the key specified by key_ref will be reused for this root; + /// if it is kms, a managed key will be used. + /// + /// valid values are "exported", "internal", and "existing" + /// The configuration values to use for the newly generated root certificate + /// + /// The mount point for the PKI backend. Defaults to + /// Provide a value only if you have customized the PKI mount point. + /// + /// The secret with the generated root response + /// if the type was "exported", the private key will be included. + /// + Task> GenerateRootAsync(string type, GenerateRootRequest generateRootRequest, string pkiBackendMountPoint = null); + + /// + /// Retrieves a list of all PKI role names. + /// + /// + /// The mount point for the PKI backend. Defaults to + /// Provide a value only if you have customized the PKI mount point. + /// + /// + /// The secret with the list of role names + /// + Task> ListRolesAsync(string pkiBackendMountPoint = null); + + /// + /// Retrieves details for a role by name. + /// + /// + /// The name of the role to be retrieved. + /// + /// + /// The mount point for the PKI backend. Defaults to + /// Provide a value only if you have customized the PKI mount point. + /// + /// + /// The secret with the role data + /// + Task> ReadRoleAsync(string pkiRoleName, string pkiBackendMountPoint = null); + + /// + /// Writes a role definition with the provided pkiRoleName. + /// if the role already exists, it will overwrite it with all values (including elided parameters) + /// To update a role with only the provided parameters, use 'PatchRoleAsync' + /// + /// + /// The name of the role to be retrieved. + /// + /// + /// The PKIRole object containing the values. + /// + /// + /// The mount point for the PKI backend. Defaults to + /// Provide a value only if you have customized the PKI mount point. + /// + /// + /// The secret with the role data + /// + Task> WriteRoleAsync(string pkiRoleName, PKIRole pkiRoleDetails, string pkiBackendMountPoint = null); + + /// + /// Patches an existing role definition with the provided pkiRoleName. + /// Use this method for updating (not replacing) a role definition with the non-null values provided in the pkiRoleDetails object. + /// This method requires the role definition to already exist, and will update it with any non-null values set in pkiRoleDetails. + /// To replace a role definition, use the 'WriteRoleAsync' method. + /// + /// + /// The name of the role to be retrieved. + /// + /// + /// The PKIRole object containing the values. + /// + /// + /// The mount point for the PKI backend. Defaults to + /// Provide a value only if you have customized the PKI mount point. + /// + /// + /// The secret with the role data + /// + Task> PatchRoleAsync(string pkiRoleName, PKIRole pkiRoleDetails, string pkiBackendMountPoint = null); } } \ No newline at end of file diff --git a/src/VaultSharp/V1/SecretsEngines/PKI/PKIRole.cs b/src/VaultSharp/V1/SecretsEngines/PKI/PKIRole.cs new file mode 100644 index 00000000..33aa4855 --- /dev/null +++ b/src/VaultSharp/V1/SecretsEngines/PKI/PKIRole.cs @@ -0,0 +1,322 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace VaultSharp.V1.SecretsEngines.PKI +{ + public class PKIRole + { + /// + /// If set, clients can request certificates for any domain, regardless of allowed_domains restrictions. See the documentation for more information. + /// + [JsonPropertyName("allow_any_name")] + public bool AllowAnyName { get; set; } + + /// + /// If set, clients can request certificates for the base domains themselves, e.g. "example.com" of domains listed in allowed_domains. + /// This is a separate option as in some cases this can be considered a security threat. See the documentation for more information. + /// + [JsonPropertyName("allow_bare_domains")] + public bool AllowBareDomains { get; set; } + + /// + /// If set, domains specified in allowed_domains can include shell-style glob patterns, e.g. "ftp*.example.com". + /// See the documentation for more information. + /// + [JsonPropertyName("allow_glob_domains")] + public bool AllowGlobDomains { get; set; } + + /// + /// If set, IP Subject Alternative Names are allowed. Any valid IP is accepted and No authorization checking is performed. + /// + [JsonPropertyName("allow_ip_sans")] + public bool AllowIpSans { get; set; } + + /// + /// Whether to allow "localhost" and "localdomain" as a valid common name in a request, independent of allowed_domains value. + /// + [JsonPropertyName("allow_localhost")] + public bool AllowLocalhost { get; set; } + + /// + /// If set, clients can request certificates for subdomains of domains listed in allowed_domains, including wildcard subdomains. + /// See the documentation for more information. + /// + [JsonPropertyName("allow_subdomains")] + public bool AllowSubdomains { get; set; } + + /// + /// If set, allows certificates with wildcards in the common name to be issued, conforming to RFC 6125's Section 6.4.3; e.g., + /// ".example.net" or "bz.example.net". See the documentation for more information. + /// + [JsonPropertyName("allow_wildcard_certificates")] + public bool AllowWildcardIdentifiers { get; set; } + + /// + /// Specifies the domains this role is allowed to issue certificates for. This is used with the allow_bare_domains, allow_subdomains, + /// and allow_glob_domains to determine matches for the common name, DNS-typed SAN entries, and Email-typed SAN entries of certificates. + /// See the documentation for more information. This parameter accepts a comma-separated string or list of domains. + /// + [JsonPropertyName("allowed_domains")] + public List AllowedDomains { get; set; } + + /// + /// If set, Allowed domains can be specified using identity template policies. Non-templated domains are also permitted. + /// + [JsonPropertyName("allowed_domains_template")] + public bool AllowedDomainsTemplate { get; set; } + + /// + /// If set, an array of allowed serial numbers to put in Subject. These values support globbing. + /// + [JsonPropertyName("allowed_serial_numbers")] + public List AllowedSerialNumbers { get; set; } + + /// + /// If set, an array of allowed URIs for URI Subject Alternative Names. Any valid URI is accepted, these values support globbing. + /// + [JsonPropertyName("allowed_uri_sans")] + public List AllowedUriSans { get; set; } + + /// + /// If set, Allowed URI SANs can be specified using identity template policies. Non-templated URI SANs are also permitted. + /// + [JsonPropertyName("allowed_uri_sans_template")] + public bool AllowedUriSansTemplate { get; set; } + + /// + /// If set, an array of allowed user-ids to put in user system login name specified here: https://www.rfc-editor.org/rfc/rfc1274#section-9.3.1 + /// + [JsonPropertyName("allowed_user_ids")] + public List AllowedUserIds { get; set; } + + /// + /// Mark Basic Constraints valid when issuing non-CA certificates. + /// + [JsonPropertyName("basic_constraints_valid_for_non_ca")] + public bool BasicConstraintsValidForNonCA { get; set; } + + /// + /// If set, certificates are flagged for client auth use. Defaults to true. See also RFC 5280 Section 4.2.1.12. + /// + [JsonPropertyName("client_flag")] + public bool ClientFlag { get; set; } + + /// + /// List of allowed validations to run against the Common Name field. Values can include 'email' to validate the CN is a email address, + /// 'hostname' to validate the CN is a valid hostname (potentially including wildcards). When multiple validations are specified, + /// these take OR semantics (either email OR hostname are allowed). The special value 'disabled' allows disabling all CN name validations, + /// allowing for arbitrary non-Hostname, non-Email address CNs. + /// + [JsonPropertyName("cn_validations")] + public List CNVAlidations { get; set; } + + /// + /// If set, certificates are flagged for code signing use. Defaults to false. See also RFC 5280 Section 4.2.1.12. + /// + [JsonPropertyName("code_signing_flag")] + public bool CodeSigningFlag { get; set; } + + /// + /// If set, Country will be set to this value in certificates issued by this role. + /// + [JsonPropertyName("country")] + public List Country { get; set; } + + /// + /// If set, certificates are flagged for email protection use. + /// Defaults to false. + /// See also RFC 5280 Section 4.2.1.12. + /// + [JsonPropertyName("email_protection_flag")] + public bool EmailProtectionFlag { get; set; } + + /// + /// If set, only valid host names are allowed for CN and DNS SANs, and the host part of email addresses. + /// Defaults to true. + /// + [JsonPropertyName("enforce_hostnames")] + public bool EnforceHostnames { get; set; } + + /// + /// A comma-separated string or list of extended key usages. Valid values can be found at https://golang.org/pkg/crypto/x509/#ExtKeyUsage -- + /// simply drop the "ExtKeyUsage" part of the name. To remove all key usages from being set, set this value to an empty list. + /// See also RFC 5280 Section 4.2.1.12. + /// + [JsonPropertyName("ext_key_usage")] + public List ExtKeyUsage { get; set; } + + /// + /// If set, certificates issued/signed against this role will have Vault leases attached to them. Defaults to "false". + /// Certificates can be added to the CRL by "vault revoke " when certificates are associated with leases. + /// It can also be done using the "pki/revoke" endpoint. However, when lease generation is disabled, invoking "pki/revoke" would be the only + /// way to add the certificates to the CRL. When large number of certificates are generated with long lifetimes, it is recommended that lease generation be disabled, + /// as large amount of leases adversely affect the startup time of Vault. + /// + [JsonPropertyName("generate_lease")] + public bool GenerateLease { get; set; } + + /// + /// Reference to the issuer used to sign requests serviced by this role. + /// + [JsonPropertyName("issuer_ref")] + public string IssuerRef { get; set; } + + /// + /// The number of bits to use. Allowed values are: + /// 0 (universal default); + /// with rsa key_type: 2048 (default), 3072, or 4096; + /// with ec key_type: 224, 256 (default), 384, or 521; + /// ignored with ed25519. + /// + [JsonPropertyName("key_bits")] + public int KeyBits { get; set; } + + /// + /// The type of key to use; + /// defaults to RSA. + /// "rsa" "ec", "ed25519" and "any" are the only valid values. + /// + [JsonPropertyName("key_type")] + public string KeyType { get; set; } + + /// + /// A comma-separated string or list of key usages (not extended key usages). + /// Valid values can be found at https://golang.org/pkg/crypto/x509/#KeyUsage -- simply drop the "KeyUsage" part of the name. + /// To remove all key usages from being set, set this value to an empty list. See also RFC 5280 Section 4.2.1.3. + /// + [JsonPropertyName("key_usage")] + public List KeyUsage { get; set; } + + /// + /// If set, Locality will be set to this value in certificates issued by this role. + /// + [JsonPropertyName("locality")] + public List Locality { get; set; } + + /// + /// The maximum allowed lease duration. If not set, defaults to the system maximum lease TTL. + /// + [JsonPropertyName("max_ttl")] + public long MaxTTL { get; set; } + + /// + /// If set, certificates issued/signed against this role will not be stored in the storage backend. + /// This can improve performance when issuing large numbers of certificates. However, certificates issued in this way cannot be enumerated or revoked, + /// so this option is recommended only for certificates that are non-sensitive, or extremely short-lived. + /// This option implies a value of "false" for "generate_lease". + /// + [JsonPropertyName("no_store")] + public bool NoStore { get; set; } + + /// + /// If set, if a client attempts to issue or sign a certificate with attached cert_metadata to store, the issuance / signing instead fails. + /// + [JsonPropertyName("no_store_metadata")] + public bool NoStoreMetadata { 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. + /// + [JsonPropertyName("not_after")] + public string NotAfter { get; set; } + + /// + /// The duration in seconds before now which the certificate needs to be backdated by. + /// + [JsonPropertyName("not_before_duration")] + public long NotBeforeDuration { get; set; } + + /// + /// If set, O (Organization) will be set to this value in certificates issued by this role. + /// + [JsonPropertyName("organization")] + public List Organization { get; set; } + + /// + /// If set, OU (OrganizationalUnit) will be set to this value in certificates issued by this role. + /// + [JsonPropertyName("ou")] + public List OrganizationalUnit { get; set; } + + /// + /// A comma-separated string or list of policy OIDs, or a JSON list of qualified policy information, + /// which must include an oid, and may include a notice and/or cps url, + /// using the form [{"oid"="1.3.6.1.4.1.7.8","notice"="I am a user Notice"}, {"oid"="1.3.6.1.4.1.44947.1.2.4 ","cps"="https://example.com"}]. + /// + [JsonPropertyName("policy_identifiers")] + public List PolicyIdentifiers { get; set; } + + /// + /// If set, Postal Code will be set to this value in certificates issued by this role. + /// + [JsonPropertyName("postal_code")] + public List PostalCode { get; set; } + + /// + /// If set, Province will be set to this value in certificates issued by this role. + /// + [JsonPropertyName("province")] + public List Province { get; set; } + + /// + /// If set to false, makes the 'common_name' field optional while generating a certificate. + /// + [JsonPropertyName("require_cn")] + public bool RequireCN { get; set; } + + /// + /// If set, certificates are flagged for server auth use. Defaults to true. See also RFC 5280 Section 4.2.1.12. + /// + [JsonPropertyName("server_flag")] + public bool ServerFlag { get; set; } + + /// + /// The number of bits to use in the signature algorithm; accepts 256 for SHA-2-256, 384 for SHA-2-384, and 512 for SHA-2-512. + /// Defaults to 0 to automatically detect based on key length (SHA-2-256 for RSA keys, and matching the curve size for NIST P-Curves). + /// + [JsonPropertyName("signature_bits")] + public int SignatureBits { get; set; } + + /// + /// If set, Street Address will be set to this value in certificates issued by this role. + /// + [JsonPropertyName("street_address")] + public List StreetAddress { get; set; } + + /// + /// The lease duration (validity period of the certificate) if no specific lease duration is requested. + /// The lease duration controls the expiration of certificates issued by this backend. Defaults to the system default value or the value of max_ttl, + /// whichever is shorter. + /// + [JsonPropertyName("ttl")] + public long TTL { get; set; } + + /// + /// If set, when used with a signing profile, the common name in the CSR will be used. This does not include any requested Subject Alternative Names; + /// use use_csr_sans for that. + /// Defaults to true. + /// + [JsonPropertyName("use_csr_common_name")] + public bool UseCsrCommonName { get; set; } + + /// + /// If set, when used with a signing profile, the SANs in the CSR will be used. This does not include the Common Name (cn); use use_csr_common_name for that. + /// Defaults to true. + /// + [JsonPropertyName("use_csr_sans")] + public bool UseCsrSans { get; set; } + + /// + /// Whether or not to use PSS signatures when using a RSA key-type issuer. Defaults to false. + /// + [JsonPropertyName("use_pss")] + public bool UsePss { get; set; } + + + public PKIRole() + { + // set defaults + KeyType = "rsa"; + } + } +} diff --git a/src/VaultSharp/V1/SecretsEngines/PKI/PKIRoleNames.cs b/src/VaultSharp/V1/SecretsEngines/PKI/PKIRoleNames.cs new file mode 100644 index 00000000..709f716d --- /dev/null +++ b/src/VaultSharp/V1/SecretsEngines/PKI/PKIRoleNames.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace VaultSharp.V1.SecretsEngines.PKI +{ + public class PKIRoleNames + /// + /// Vault Response Model containing list of role keys (role names) + /// + { + /// + /// Gets or sets list of certificate keys (serial numbers) + /// + /// + /// List of certificate keys (serial numbers) + /// + [JsonPropertyName("keys")] + public List Keys { get; set; } + } +} diff --git a/src/VaultSharp/V1/SecretsEngines/PKI/PKISecretsEngineProvider.cs b/src/VaultSharp/V1/SecretsEngines/PKI/PKISecretsEngineProvider.cs index 2e89fca9..a0cfde22 100644 --- a/src/VaultSharp/V1/SecretsEngines/PKI/PKISecretsEngineProvider.cs +++ b/src/VaultSharp/V1/SecretsEngines/PKI/PKISecretsEngineProvider.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using VaultSharp.Core; @@ -26,7 +28,7 @@ public async Task> GetCredentialsAsync(string pki return result; } - + public async Task> SignCertificateAsync(string pkiRoleName, SignCertificatesRequestOptions signCertificatesRequestOptions, string pkiBackendMountPoint = null, string wrapTimeToLive = null) { Checker.NotNull(pkiRoleName, "pkiRoleName"); @@ -77,7 +79,7 @@ public async Task ReadCACertificateAsync(CertificateFormat c : CertificateFormat.der; var result = await _polymath.MakeVaultApiRequest(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, "/ca" + format, HttpMethod.Get, rawResponse: true).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); - + return new RawCertificateData { CertificateContent = result, @@ -89,7 +91,7 @@ public async Task> ReadCertificateAsync(string serial { Checker.NotNull(serialNumber, "serialNumber"); - var certificateDataSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, "/cert/" + serialNumber , HttpMethod.Get, unauthenticated: true).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); + var certificateDataSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, "/cert/" + serialNumber, HttpMethod.Get, unauthenticated: true).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); return certificateDataSecret; } @@ -106,7 +108,7 @@ public async Task> ListRevokedCertificatesAsync(string p return result; } - + public async Task> ReadDefaultIssuerCertificateChainAsync(CertificateFormat certificateFormat, string pkiBackendMountPoint = null) { if (certificateFormat != CertificateFormat.json @@ -126,5 +128,54 @@ public async Task> ReadDefaultIssuerCertificateChainAsyn var certificateDataSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, path, HttpMethod.Get, unauthenticated: true).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); return certificateDataSecret; } + + public async Task> GenerateRootAsync(string type, GenerateRootRequest generateRootRequest, string pkiBackendMountPoint = null) + { + var path = $"/root/generate/{type}"; + var generatedRootDataSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, path, HttpMethod.Post, generateRootRequest).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); + return generatedRootDataSecret; + } + + public async Task> ListRolesAsync(string pkiBackendMountPoint = null) + { + var path = "/roles?list=true"; + var roleNamesSecret = new Secret() { Data = new PKIRoleNames() { Keys = new List() } }; + + try + { + roleNamesSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, path, HttpMethod.Get).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); + } + catch (VaultApiException ex) + { + // if the role list is empty, we will get a 404 response with { "errors": [] } + // make sure it's that error only, and return the empty list if so + if (ex.StatusCode != 404) + { + throw; + } + } + return roleNamesSecret; + } + + public async Task> ReadRoleAsync(string pkiRoleName, string pkiBackendMountPoint = null) + { + var path = $"/roles/{pkiRoleName}"; + var roleDetailsSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, path, HttpMethod.Get).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); + return roleDetailsSecret; + } + + public async Task> WriteRoleAsync(string pkiRoleName, PKIRole pkiRoleDetails, string pkiBackendMountPoint = null) + { + var path = $"/roles/{pkiRoleName}"; + var roleDetailsSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, path, HttpMethod.Post, pkiRoleDetails).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); + return roleDetailsSecret; + } + + public async Task> PatchRoleAsync(string pkiRoleName, PKIRole pkiRoleDetails, string pkiBackendMountPoint = null) + { + var path = $"/roles/{pkiRoleName}"; + var roleDetailsSecret = await _polymath.MakeVaultApiRequest>(pkiBackendMountPoint ?? _polymath.VaultClientSettings.SecretsEngineMountPoints.PKI, path, new HttpMethod("PATCH"), pkiRoleDetails).ConfigureAwait(_polymath.VaultClientSettings.ContinueAsyncTasksOnCapturedContext); + return roleDetailsSecret; + } } } \ No newline at end of file diff --git a/test/VaultSharp.Samples/Backends/Secrets/PKISecretsBackendSamples.cs b/test/VaultSharp.Samples/Backends/Secrets/PKISecretsBackendSamples.cs new file mode 100644 index 00000000..2096ddac --- /dev/null +++ b/test/VaultSharp.Samples/Backends/Secrets/PKISecretsBackendSamples.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using VaultSharp.V1.SecretsEngines; +using VaultSharp.V1.SecretsEngines.PKI; +using Xunit; + +namespace VaultSharp.Samples +{ + partial class Program + { + private static void RunPKISecretsBackendSamples() + { + var pkiMountPath = Guid.NewGuid().ToString(); + + // mount a new v1 kv + var pkiSecretsEngine = new SecretsEngine + { + Type = SecretsEngineType.PKI, + Config = new Dictionary(), + Path = pkiMountPath + }; + + _authenticatedVaultClient.V1.System.MountSecretBackendAsync(pkiSecretsEngine).Wait(); + + var secretBackends = _authenticatedVaultClient.V1.System.GetSecretBackendsAsync().Result; + DisplayJson(secretBackends); + Assert.True(secretBackends.Data.Any()); + var pkiPath = Guid.NewGuid().ToString(); + + // verify the role list is initially empty + var roleListResponse = _authenticatedVaultClient.V1.Secrets.PKI.ListRolesAsync(pkiMountPath).Result; + + Assert.Empty(roleListResponse.Data?.Keys); + + // write a new role + var allowedDomain = "test.com"; + var roleName = "testRole"; + + var newRole = new PKIRole() + { + AllowedDomains = new List() { allowedDomain }, + AllowSubdomains = true, + }; + + var newRoleResponse = _authenticatedVaultClient.V1.Secrets.PKI.WriteRoleAsync(roleName, newRole, pkiMountPath).Result; + Assert.True(newRoleResponse.Data.AllowedDomains.Exists(d => d == allowedDomain)); + + // verify it shows up in the list now + roleListResponse = _authenticatedVaultClient.V1.Secrets.PKI.ListRolesAsync(pkiMountPath).Result; + + Assert.Single(roleListResponse.Data.Keys); + + // generate a new root CA + var newRoot = new GenerateRootRequest() + { + CommonName = "test.com", + IssuerName = "testroot_test_com", + KeyName = "testroot_key", + }; + + var newRootResponse = _authenticatedVaultClient.V1.Secrets.PKI.GenerateRootAsync("exported", newRoot, pkiMountPath).Result; + + Assert.NotEmpty(newRootResponse.Data.PrivateKey); + + // issue a cert + + var signCertOptions = new SignCertificatesRequestOptions() + { + CommonName = "my.test.com", + Csr = "-----BEGIN CERTIFICATE REQUEST-----\r\nMIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLbXkudGVzdC5jb20wggEiMA0GCSqGSIb3\r\nDQEBAQUAA4IBDwAwggEKAoIBAQCQj/loI/Us7ayc/GOQlDWWv/lH+pcJ9g3w2Q/U\r\nzl8LBR7CD6Lve7TzBHxXU77gpg/lrCksr9LfE85FofhMy2WdEDTQw2BqdA/xwphh\r\nGsmaV+gfniZT96KzTOTRfMLE8Lf88bw5us7ha12MdJEhVX72kqXs7r/Hx5wz6gyw\r\niKcuezeTjp3r0qBPUIgHDgSg2TCVPs+THuhozQd4InQFU4HIWrDnR8unm5udRWId\r\nXOfxcQcgPp+UhzHzj+H/TfhqVeDzSvjVyir0llRAg7mNZctb/8lyOFPOTZkm4dWW\r\nzWVowYctnWgL05KcFK2wOJry16+gyzJaDE4EoxA/+YXVlnsfAgMBAAGgKTAnBgkq\r\nhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC215LnRlc3QuY29tMA0GCSqGSIb3DQEB\r\nCwUAA4IBAQA41h0RFx8NLNSq92sgQwAh+FjZNqcjq8sLj7P0jwYi0CJrGf2fFfw1\r\nRhcZefwc3uGPsa/LXyGeBO4k88q8hA6x0B3yUhH26nC+OI1jhfj97x5pl+JFGkYT\r\nAP5Su9vwDKc22T8cv9K7Yzor3mfGc+Vs4HPH/pasg5cTqTageXNiBMd9VZKXzvrb\r\nhQyjW1uomsKCTlqCcjkRSA6eppxdAYCOKR/cfNNai2cIhZXuLZ4Y3gLosk/J8MGP\r\npRgz1SBls41Vk9gP8XF9e8eAKXVvnxSoCbifdNfjBkLaQgfaYpCV+NKBwQBmz7ad\r\nHjirOTU4elr/qUq91gyYuakOCZGWKm0k\r\n-----END CERTIFICATE REQUEST-----", + TimeToLive = "24h" + }; + + var newCertResponse = _authenticatedVaultClient.V1.Secrets.PKI.SignCertificateAsync(roleName, signCertOptions, pkiMountPath).Result; + + Assert.NotEmpty(newCertResponse.Data.CertificateContent); + + _authenticatedVaultClient.V1.System.UnmountSecretBackendAsync(pkiMountPath).Wait(); + } + } +} diff --git a/test/VaultSharp.Samples/Backends/SecretsBackendSamples.cs b/test/VaultSharp.Samples/Backends/SecretsBackendSamples.cs index 23e5684a..7f78e2c4 100644 --- a/test/VaultSharp.Samples/Backends/SecretsBackendSamples.cs +++ b/test/VaultSharp.Samples/Backends/SecretsBackendSamples.cs @@ -24,6 +24,9 @@ private static void RunSecretsEngineSamples() Console.WriteLine("\n RunTransitSecretsBackendSamples \n"); RunTransitSecretsBackendSamples(); + + Console.WriteLine("\n RunPkiSecretsBackendSamples \n"); + RunPKISecretsBackendSamples(); } } } diff --git a/test/VaultSharp.Samples/Program.cs b/test/VaultSharp.Samples/Program.cs index f8d01b80..eea5c034 100644 --- a/test/VaultSharp.Samples/Program.cs +++ b/test/VaultSharp.Samples/Program.cs @@ -8,7 +8,7 @@ namespace VaultSharp.Samples { partial class Program { - private const string ExpectedVaultVersion = "1.17.5"; + private const string ExpectedVaultVersion = "1.17.6+ent"; private static IVaultClient _unauthenticatedVaultClient; private static IVaultClient _authenticatedVaultClient; @@ -103,7 +103,7 @@ private static VaultClientSettings GetVaultClientSettings(IAuthMethodInfo authMe Console.WriteLine(_responseContent); } }, - Namespace = "bhjk", + //Namespace = "bhjk", // ?? MyHttpClientProviderFunc = handler => new HttpClient(handler) };