diff --git a/CHANGELOG.md b/CHANGELOG.md index 22fca6d..e305fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +- 1.1.0 + - Add support for external SANs/subject (not in CSR) - 1.0.0 - First production release of the GCP CAS AnyCA Gateway REST plugin that implements: * CA Sync: diff --git a/GCPCAS/Client/CreateCertificateRequestBuilder.cs b/GCPCAS/Client/CreateCertificateRequestBuilder.cs index 13cf512..68f8ac4 100644 --- a/GCPCAS/Client/CreateCertificateRequestBuilder.cs +++ b/GCPCAS/Client/CreateCertificateRequestBuilder.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json; using Google.Cloud.Security.PrivateCA.V1; using Google.Protobuf.WellKnownTypes; @@ -31,6 +32,8 @@ public class CreateCertificateRequestBuilder : ICreateCertificateRequestBuilder private string _csrString; private string _certificateTemplate; + private string _subject; + private List _dnsSans; private int _certificateLifetimeDays = GCPCASPluginConfig.DefaultCertificateLifetime; public ICreateCertificateRequestBuilder WithCsr(string csr) @@ -94,13 +97,30 @@ public ICreateCertificateRequestBuilder WithRequestFormat(RequestFormat requestF public ICreateCertificateRequestBuilder WithSans(Dictionary san) { - if (san != null & san.Count > 0) _logger.LogTrace($"Found non-zero list of SANs - Ignoring and using SANs from CSR"); + _dnsSans = new List(); + if (san != null & san.Count > 0) + { + var dnsKeys = san.Keys.Where(k => k.Contains("dns", StringComparison.OrdinalIgnoreCase)).ToList(); + foreach (var key in dnsKeys) + { + _dnsSans.AddRange(san[key]); + } + _logger.LogTrace($"Found {_dnsSans.Count} SANs"); + } + else + { + _logger.LogTrace($"Found no external SANs - Using SANs from CSR"); + } return this; } public ICreateCertificateRequestBuilder WithSubject(string subject) { - if (!string.IsNullOrWhiteSpace(subject)) _logger.LogTrace($"Found non-empty subject {subject} - Ignoring and using CSR value"); + if (!string.IsNullOrWhiteSpace(subject)) + { + _logger.LogTrace($"Found non-empty subject {subject}"); + _subject = subject; + } return this; } @@ -109,10 +129,35 @@ public CreateCertificateRequest Build(string locationId, string projectId, strin _logger.LogDebug("Constructing CreateCertificateRequest"); CaPoolName caPoolName = new CaPoolName(projectId, locationId, caPool); + CertificateConfig certConfig = new CertificateConfig(); + certConfig.SubjectConfig = new CertificateConfig.Types.SubjectConfig(); + bool useConfig = false; + if (!string.IsNullOrEmpty(_subject)) + { + certConfig.SubjectConfig.Subject = new Subject + { + CommonName = Utilities.ParseSubject(_subject, "CN=", false), + Organization = Utilities.ParseSubject(_subject, "O=", false), + OrganizationalUnit = Utilities.ParseSubject(_subject, "OU=", false), + CountryCode = Utilities.ParseSubject(_subject, "C=", false), + Locality = Utilities.ParseSubject(_subject, "L=", false) + }; + useConfig = true; + } + if (_dnsSans.Count > 0) + { + certConfig.SubjectConfig.SubjectAltName = new SubjectAltNames + { + DnsNames = { _dnsSans } + }; + useConfig = true; + } + Certificate theCertificate = new Certificate { PemCsr = _csrString, Lifetime = Duration.FromTimeSpan(new TimeSpan(_certificateLifetimeDays, 0, 0, 0)), + Config = (useConfig) ? certConfig : null, }; if (!string.IsNullOrWhiteSpace(_certificateTemplate)) diff --git a/GCPCAS/Utilities.cs b/GCPCAS/Utilities.cs new file mode 100644 index 0000000..747ac11 --- /dev/null +++ b/GCPCAS/Utilities.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.GCPCAS +{ + public class Utilities + { + public static string ParseSubject(string subject, string rdn, bool required = true) + { + string escapedSubject = subject.Replace("\\,", "|"); + string rdnString = escapedSubject.Split(',').ToList().Where(x => x.Contains(rdn)).FirstOrDefault(); + + if (!string.IsNullOrEmpty(rdnString)) + { + return rdnString.Replace(rdn, "").Replace("|", ",").Trim(); + } + else if (required) + { + throw new Exception($"The request is missing a {rdn} value"); + } + else + { + return null; + } + } + } +} diff --git a/README.md b/README.md index 35bb440..f1386a6 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,9 @@ The [Google Cloud Platform (GCP) CA Services (CAS)](https://cloud.google.com/sec * CA Sync: * Download all certificates issued by connected Enterprise tier CAs in GCP CAS (full sync). * Download all certificates issued by connected Enterprise tier CAs in GCP CAS issued after a specified time (incremental sync). -* Certificate enrollment for all published GoDaddy Certificate SKUs: +* Certificate enrollment for all published GCP Certificate SKUs: * Support certificate enrollment (new keys/certificate). + * Support auto-enrollment (subject/SANs outside of the CSR) * Certificate revocation: * Request revocation of a previously issued certificate. @@ -154,21 +155,6 @@ Both the Keyfactor Command and AnyCA Gateway REST servers must trust the root CA 3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates. -4. In Keyfactor Command (v12.3+), for each imported Certificate Template, follow the [official documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm) to define enrollment fields for each of the following parameters: - - * **CertificateLifetimeDays** - The desired lifetime, in days, of the issued certificate. Used by GCP to create the `not_before_time` and `not_after_time` fields in the signed X.509 certificate. If the lifetime extends past the life of any CA in the issuing chain, this value will be truncated. Additionally, if the lifetime extends past the CA Pool's Maximum Lifetime, this value will be truncated accordingly. The default value is 365 days. - - -## Plugin Mechanics -### Enrollment/Renewal/Reissuance - -The GCP CAS AnyCA Gateway REST plugin treats _all_ certificate enrollment as a new enrollment. - -### Synchronization - -The GCP CAS AnyCA Gateway REST plugin uses the [`ListCertificatesRequest` RPC](https://cloud.google.com/certificate-authority-service/docs/reference/rpc/google.cloud.security.privateca.v1#google.cloud.security.privateca.v1.ListCertificatesRequest) when synchronizing certificates from GCP. At the time the latest release, this RPC does not enable granularity to list certificates issued by a particular CA. As such, the CA Synchronization job implemented by the plugin will _always_ download all certificates issued by _any CA_ in the CA Pool. - -> Friendly reminder to always follow the [GCP CAS best practices](https://cloud.google.com/certificate-authority-service/docs/best-practices) ## License diff --git a/docsource/configuration.md b/docsource/configuration.md index 03505e5..dcdddb9 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -5,8 +5,9 @@ The [Google Cloud Platform (GCP) CA Services (CAS)](https://cloud.google.com/sec * CA Sync: * Download all certificates issued by connected Enterprise tier CAs in GCP CAS (full sync). * Download all certificates issued by connected Enterprise tier CAs in GCP CAS issued after a specified time (incremental sync). -* Certificate enrollment for all published GoDaddy Certificate SKUs: +* Certificate enrollment for all published GCP Certificate SKUs: * Support certificate enrollment (new keys/certificate). + * Support auto-enrollment (subject/SANs outside of the CSR) * Certificate revocation: * Request revocation of a previously issued certificate. diff --git a/integration-manifest.json b/integration-manifest.json index 015be2f..ebb80ec 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -41,4 +41,4 @@ ] } } -} +} \ No newline at end of file