diff --git a/CHANGELOG.md b/CHANGELOG.md index e305fee..0798d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +- 1.1.1 + - Fixed bug with Enrollment and Auto Enrollment + - Fixed issue where only DNS Sans are supported + - Added additional Logging - 1.1.0 - Add support for external SANs/subject (not in CSR) - 1.0.0 diff --git a/GCPCAS/Client/CreateCertificateRequestBuilder.cs b/GCPCAS/Client/CreateCertificateRequestBuilder.cs index 68f8ac4..6a110a8 100644 --- a/GCPCAS/Client/CreateCertificateRequestBuilder.cs +++ b/GCPCAS/Client/CreateCertificateRequestBuilder.cs @@ -1,179 +1,254 @@ -/* -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.Json; -using Google.Cloud.Security.PrivateCA.V1; -using Google.Protobuf.WellKnownTypes; -using Keyfactor.AnyGateway.Extensions; -using Keyfactor.Logging; -using Microsoft.Extensions.Logging; - -namespace Keyfactor.Extensions.CAPlugin.GCPCAS.Client; - -public class CreateCertificateRequestBuilder : ICreateCertificateRequestBuilder -{ - ILogger _logger = LogHandler.GetClassLogger(); - - private string _csrString; - private string _certificateTemplate; - private string _subject; - private List _dnsSans; - private int _certificateLifetimeDays = GCPCASPluginConfig.DefaultCertificateLifetime; - - public ICreateCertificateRequestBuilder WithCsr(string csr) - { - _csrString = csr; - return this; - } - - public ICreateCertificateRequestBuilder WithEnrollmentProductInfo(EnrollmentProductInfo productInfo) - { - if (productInfo.ProductID == GCPCASPluginConfig.NoTemplateName) - { - _certificateTemplate = null; - _logger.LogDebug($"{GCPCASPluginConfig.NoTemplateName} template selected - Certificate enrollment will defer to the baseline values and policy configured by the CA Pool."); - } - else - { - _logger.LogDebug($"Configuring {typeof(CreateCertificateRequest).ToString()} with the {productInfo.ProductID} Certificate Template."); - _certificateTemplate = productInfo.ProductID; - } - - if (productInfo.ProductParameters != null) - { - _logger.LogDebug($"Parsing Custom Enrollment Parameters"); - - if (productInfo.ProductParameters.TryGetValue(GCPCASPluginConfig.EnrollmentParametersConstants.CertificateLifetimeDays, out string certificateLifetimeDaysString)) - { - if (int.TryParse(certificateLifetimeDaysString, out _certificateLifetimeDays)) - { - _logger.LogDebug($"Found non-null CertificateValidityDays Custom Enrollment parameter - Configured CreateCertificateRequest to use a validity of {_certificateLifetimeDays} days."); - } - else - { - string error = $"Unable to parse integer value from {GCPCASPluginConfig.EnrollmentParametersConstants.CertificateLifetimeDays} Custom Enrollment Parameter"; - _logger.LogError(error); - throw new ArgumentException(error); - } - - } - } - - return this; - } - - public ICreateCertificateRequestBuilder WithEnrollmentType(EnrollmentType enrollmentType) - { - if (enrollmentType != EnrollmentType.New) _logger.LogTrace($"{typeof(EnrollmentType).ToString()} is {enrollmentType.ToString()} - Ignoring and treating enrollment as {EnrollmentType.New.ToString()}"); - return this; - } - - public ICreateCertificateRequestBuilder WithRequestFormat(RequestFormat requestFormat) - { - if (requestFormat != RequestFormat.PKCS10) - { - string error = $"AnyCA Gateway REST framework provided CSR in unsupported format: {requestFormat.ToString()}"; - _logger.LogError(error); - throw new Exception(error); - } - return this; - } - - public ICreateCertificateRequestBuilder WithSans(Dictionary san) - { - _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}"); - _subject = subject; - } - return this; - } - - public CreateCertificateRequest Build(string locationId, string projectId, string caPool, string caId) - { - _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)) - { - CertificateTemplateName template = new CertificateTemplateName(projectId, locationId, _certificateTemplate); - theCertificate.CertificateTemplate = template.ToString(); - } - - CreateCertificateRequest theRequest = new CreateCertificateRequest - { - ParentAsCaPoolName = caPoolName, - CertificateId = Guid.NewGuid().ToString(), - Certificate = theCertificate, - }; - - return theRequest; - } -} - +/* +Copyright © 2025 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using Google.Cloud.Security.PrivateCA.V1; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Keyfactor.AnyGateway.Extensions; +using Keyfactor.Logging; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using X509Extension = Google.Cloud.Security.PrivateCA.V1.X509Extension; + +namespace Keyfactor.Extensions.CAPlugin.GCPCAS.Client +{ + public class CreateCertificateRequestBuilder : ICreateCertificateRequestBuilder + { + ILogger _logger = LogHandler.GetClassLogger(); + + private string _csrString; + private string _certificateTemplate; + private string _subject; + private List _dnsSans; + private int _certificateLifetimeDays = GCPCASPluginConfig.DefaultCertificateLifetime; + + // Store additional extensions + private List _additionalExtensions = new List(); + + public ICreateCertificateRequestBuilder WithCsr(string csr) + { + _logger.MethodEntry(); + _csrString = csr; + _logger.MethodExit(); + return this; + } + + public ICreateCertificateRequestBuilder WithEnrollmentProductInfo(EnrollmentProductInfo productInfo) + { + _logger.MethodEntry(); + if (productInfo.ProductID == GCPCASPluginConfig.NoTemplateName) + { + _certificateTemplate = null; + _logger.LogDebug($"{GCPCASPluginConfig.NoTemplateName} template selected."); + } + else + { + _logger.LogDebug($"Configuring request with {productInfo.ProductID} Certificate Template."); + _certificateTemplate = productInfo.ProductID; + } + + if (productInfo.ProductParameters != null) + { + _logger.LogDebug($"Parsing Custom Enrollment Parameters"); + + if (productInfo.ProductParameters.TryGetValue(GCPCASPluginConfig.EnrollmentParametersConstants.CertificateLifetimeDays, out string certificateLifetimeDaysString)) + { + if (int.TryParse(certificateLifetimeDaysString, out _certificateLifetimeDays)) + { + _logger.LogDebug($"Using validity of {_certificateLifetimeDays} days."); + } + } + + _logger.LogTrace($"Looping through extensions for Auto Enrollment Params"); + // Extract Additional Extensions + foreach (var param in productInfo.ProductParameters) + { + if (param.Key.StartsWith("ExtensionData")) + { + string oid = param.Key.Replace("ExtensionData-", ""); // Extract OID from key + string base64Value = param.Value; + + _logger.LogTrace($"Loggin oid and value {oid} {base64Value}"); + + var extension = CreateX509Extension(oid, base64Value); + if (extension != null) + { + _logger.LogTrace($"Adding Extension"); + _additionalExtensions.Add(extension); + } + } + } + } + _logger.MethodExit(); + return this; + } + + public ICreateCertificateRequestBuilder WithEnrollmentType(EnrollmentType enrollmentType) + { + _logger.MethodEntry(); + _logger.MethodExit(); + return this; + } + + public ICreateCertificateRequestBuilder WithRequestFormat(RequestFormat requestFormat) + { + _logger.MethodEntry(); + if (requestFormat != RequestFormat.PKCS10) + { + throw new Exception($"Unsupported CSR format: {requestFormat}"); + } + _logger.MethodExit(); + return this; + } + + public ICreateCertificateRequestBuilder WithSans(Dictionary san) + { + _logger.MethodEntry(); + _dnsSans = new List(); + + if (san != null && san.Count > 0) + { + foreach (var key in san.Keys) + { + _logger.LogTrace($"San Value {san[key]}"); + _dnsSans.AddRange(san[key]); + } + + _logger.LogTrace($"Found {_dnsSans.Count} SANs"); + } + _logger.MethodExit(); + return this; + } + + public ICreateCertificateRequestBuilder WithSubject(string subject) + { + _logger.MethodEntry(); + if (!string.IsNullOrWhiteSpace(subject)) + { + _logger.LogTrace($"Found subject {subject}"); + _subject = subject; + } + _logger.MethodExit(); + return this; + } + + public CreateCertificateRequest Build(string locationId, string projectId, string caPool, string caId) + { + _logger.MethodEntry(); + + CaPoolName caPoolName = new CaPoolName(projectId, locationId, caPool); + + CertificateConfig certConfig = new CertificateConfig(); + certConfig.SubjectConfig = new CertificateConfig.Types.SubjectConfig(); + + if (!string.IsNullOrEmpty(_subject)) + { + _logger.LogTrace($"Subject {_subject}"); + Subject parsedSubject = SubjectParser.ParseFromString(_subject); + _logger.LogTrace($"Parsed Subject {JsonConvert.SerializeObject(parsedSubject)}"); + certConfig.SubjectConfig.Subject = parsedSubject; + } + + if (_dnsSans.Count > 0) + { + _logger.LogTrace($"Getting Subject Alt Names"); + SubjectAltNames parsedSubjectAltNames = SubjectAltNamesParser.ParseFromDnsList(_dnsSans); + _logger.LogTrace($"Parsed AltNames {JsonConvert.SerializeObject(parsedSubjectAltNames)}"); + certConfig.SubjectConfig.SubjectAltName = parsedSubjectAltNames; + } + + if (!string.IsNullOrEmpty(_csrString)) + { + _logger.LogTrace($"Putting Csr in public key {_csrString}"); + ByteString csrByteString = ByteString.CopyFromUtf8(_csrString); + + certConfig.PublicKey = new PublicKey + { + Format = PublicKey.Types.KeyFormat.Pem, + Key = csrByteString + }; + _logger.LogTrace($"Serialized PublicKey {JsonConvert.SerializeObject(certConfig.PublicKey)}"); + } + + certConfig.X509Config = new X509Parameters(); + + // Add Additional Extensions if present + if (_additionalExtensions.Count > 0) + { + _logger.LogTrace($"Adding additional Extensions"); + _logger.LogTrace($"Serialized Additional Extensions {JsonConvert.SerializeObject(_additionalExtensions)}"); + certConfig.X509Config.AdditionalExtensions.AddRange(_additionalExtensions); + } + + _logger.LogTrace($"Creating The Certificate"); + Certificate theCertificate = new Certificate + { + Lifetime = Duration.FromTimeSpan(new TimeSpan(_certificateLifetimeDays, 0, 0, 0)), + Config = certConfig + }; + _logger.LogTrace($"Serialized theCertificate {JsonConvert.SerializeObject(theCertificate)}"); + + if (!string.IsNullOrWhiteSpace(_certificateTemplate)) + { + CertificateTemplateName template = new CertificateTemplateName(projectId, locationId, _certificateTemplate); + theCertificate.CertificateTemplate = template.ToString(); + _logger.LogTrace($"Serialized theCertificate after template {JsonConvert.SerializeObject(theCertificate)}"); + } + + CreateCertificateRequest theRequest = new CreateCertificateRequest + { + ParentAsCaPoolName = caPoolName, + CertificateId = Guid.NewGuid().ToString(), + Certificate = theCertificate, + }; + + _logger.MethodExit(); + return theRequest; + } + + /// + /// Creates a properly formatted X509Extension from an OID and Base64-encoded value. + /// + private X509Extension CreateX509Extension(string oid, string base64EncodedValue) + { + try + { + _logger.MethodEntry(); + // Decode the Base64-encoded value + byte[] decodedBytes = Convert.FromBase64String(base64EncodedValue); + _logger.MethodExit(); + // Create the X.509 extension with the correct format + return new X509Extension + { + ObjectId = new ObjectId + { + ObjectIdPath = { oid.Split('.').Select(int.Parse) } // Convert OID to int array + }, + Value = ByteString.CopyFrom(decodedBytes) // Store properly DER-encoded value + }; + } + catch (Exception ex) + { + _logger.LogError($"Error processing extension {oid}: {ex.Message}"); + return null; + } + } + + } +} diff --git a/GCPCAS/Client/GCPCASClient.cs b/GCPCAS/Client/GCPCASClient.cs index 27fa39f..1a304bb 100644 --- a/GCPCAS/Client/GCPCASClient.cs +++ b/GCPCAS/Client/GCPCASClient.cs @@ -1,5 +1,5 @@ /* -Copyright Β© 2024 Keyfactor +Copyright Β© 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,14 +21,16 @@ limitations under the License. using System.Threading.Tasks; using Google.Api.Gax; using Google.Api.Gax.Grpc; -using Google.Api.Gax.Grpc.Rest; using Google.Api.Gax.ResourceNames; using Google.Cloud.Security.PrivateCA.V1; using Google.Protobuf.WellKnownTypes; +using Grpc.Core; using Keyfactor.AnyGateway.Extensions; using Keyfactor.Logging; using Keyfactor.PKI.Enums.EJBCA; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using static Google.Rpc.Context.AttributeContext.Types; namespace Keyfactor.Extensions.CAPlugin.GCPCAS.Client; @@ -57,9 +59,9 @@ public class GCPCASClient : IGCPCASClient /// The CA Pool ID in GCP CAS to use for certificate operations. If the CA Pool has resource name projects/my-project/locations/us-central1/caPools/my-pool, this field should be set to my-pool /// The CA ID of a CA in the same CA Pool as CAPool. For example, to issue certificates from a CA with resource name projects/my-project/locations/us-central1/caPools/my-pool/certificateAuthorities/my-ca, this field should be set to my-ca. public GCPCASClient(string locationId, string projectId, string caPool, string caId) - { + { _logger = LogHandler.GetClassLogger(); - + _logger.MethodEntry(); _logger.LogDebug($"Creating GCP CA Services Client with Location: {locationId}, Project ID: {projectId}, CA Pool: {caPool}, CA ID: {caId}"); this._projectId = projectId; @@ -69,6 +71,7 @@ public GCPCASClient(string locationId, string projectId, string caPool, string c _logger.LogTrace($"Setting up a {typeof(CertificateAuthorityServiceClient).ToString()} using the Default gRPC adapter"); _client = new CertificateAuthorityServiceClientBuilder().Build(); + _logger.MethodExit(); } public override string ToString() @@ -81,12 +84,14 @@ public override string ToString() /// /// public Task Enable() - { + { + _logger.MethodEntry(); if (!_clientIsEnabled) { _logger.LogDebug($"Enabling GCPCAS client {this.ToString()}"); _clientIsEnabled = true; - } + } + _logger.MethodExit(); return Task.CompletedTask; } @@ -95,12 +100,14 @@ public Task Enable() /// /// public Task Disable() - { + { + _logger.MethodEntry(); if (_clientIsEnabled) { _logger.LogDebug($"Disabling GCPCAS client {this.ToString()}"); _clientIsEnabled = false; } + _logger.MethodExit(); return Task.CompletedTask; } @@ -111,7 +118,9 @@ public Task Disable() /// A indicating if the client is enabled. /// public bool IsEnabled() - { + { + _logger.MethodEntry(); + _logger.MethodExit(); return _clientIsEnabled; } @@ -123,7 +132,8 @@ public bool IsEnabled() /// /// Thrown if the GCP Application Default Credentials are not properly configured, if the GCP CAS CA Pool/CA is not found/is not compatible, or if the was not enabled via the method. public async Task ValidateConnection() - { + { + _logger.MethodEntry(); EnsureClientIsEnabled(); _logger.LogTrace($"Searching for CA called {_caId} in the {_caPool} CA pool"); @@ -146,7 +156,8 @@ public async Task ValidateConnection() throw new Exception(error); } - _logger.LogDebug($"{typeof(GCPCASClient).ToString()} is compatible with CA called {ca.CertificateAuthorityName.CertificateAuthorityId} in the {ca.CertificateAuthorityName.CaPoolId} CA Pool."); + _logger.LogDebug($"{typeof(GCPCASClient).ToString()} is compatible with CA called {ca.CertificateAuthorityName.CertificateAuthorityId} in the {ca.CertificateAuthorityName.CaPoolId} CA Pool."); + _logger.MethodExit(); return; } @@ -165,58 +176,87 @@ public async Task ValidateConnection() /// /// Thrown if the is null or if the operation fails. /// - public async Task DownloadAllIssuedCertificates(BlockingCollection certificatesBuffer, CancellationToken cancelToken, DateTime? issuedAfter = null) - { - EnsureClientIsEnabled(); - - if (certificatesBuffer == null) - { - string message = "Failed to download issued certificates - certificatesBuffer is null"; - _logger.LogError(message); - throw new Exception(message); - } - - _logger.LogTrace($"Setting up {typeof(ListCertificatesRequest).ToString()} with {this.ToString()}"); - ListCertificatesRequest request = new ListCertificatesRequest - { - ParentAsCaPoolName = new CaPoolName(_projectId, _locationId, _caPool), - }; - - if (issuedAfter != null) - { - Timestamp ts = Timestamp.FromDateTime(issuedAfter.Value.ToUniversalTime()); - _logger.LogDebug($"Filtering issued certificates by update_time >= {ts}"); - request.Filter = $"update_time >= {ts}"; - } + public async Task DownloadAllIssuedCertificates(BlockingCollection certificatesBuffer, CancellationToken cancelToken, DateTime? issuedAfter = null) + { + _logger.MethodEntry(); + EnsureClientIsEnabled(); + + if (certificatesBuffer == null) + { + string message = "Failed to download issued certificates - certificatesBuffer is null"; + _logger.LogError(message); + throw new ArgumentNullException(nameof(certificatesBuffer), message); + } + + _logger.LogTrace($"Setting up {typeof(ListCertificatesRequest).ToString()} with {this.ToString()}"); + + ListCertificatesRequest request = new ListCertificatesRequest + { + ParentAsCaPoolName = new CaPoolName(_projectId, _locationId, _caPool), + }; + + if (issuedAfter != null) + { + Timestamp ts = Timestamp.FromDateTime(issuedAfter.Value.ToUniversalTime()); + _logger.LogDebug($"Filtering issued certificates by update_time >= {ts}"); + request.Filter = $"update_time >= {ts}"; + } + + _logger.LogTrace($"Setting up {typeof(CallSettings).ToString()} with provided {typeof(CancellationToken).ToString()} {this.ToString()}"); + CallSettings settings = CallSettings.FromCancellationToken(cancelToken); + + _logger.LogDebug($"Downloading all issued certificates from GCP CAS {this.ToString()}"); + PagedAsyncEnumerable certificates = _client.ListCertificatesAsync(request, settings); + + int pageNumber = 0; + int numberOfCertificates = 0; + + try + { + await foreach (var response in certificates.AsRawResponses()) + { + if (response.Certificates == null) + { + _logger.LogWarning($"GCP returned null certificate list for page number {pageNumber} - continuing {this.ToString()}"); + continue; + } + + foreach (Certificate certificate in response.Certificates) + { + certificatesBuffer.Add(AnyCAPluginCertificateFromGCPCertificate(certificate)); + numberOfCertificates++; + _logger.LogDebug($"Found Certificate with name {certificate.CertificateName.CertificateId} {this.ToString()}"); + } + + _logger.LogTrace($"Fetched page {pageNumber} - Next Page Token: {response.NextPageToken}"); + pageNumber++; + } + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.ResourceExhausted) + { + _logger.LogError($"Rate limit exceeded while fetching certificates: {ex.Message}"); + throw; + } + catch (OperationCanceledException) + { + _logger.LogWarning("Certificate download operation was canceled."); + throw; + } + catch (Exception ex) + { + _logger.LogError($"Unexpected error while fetching certificates: {ex.Message}"); + throw; + } + finally + { + certificatesBuffer.CompleteAdding(); + _logger.LogDebug($"Fetched {certificatesBuffer.Count} certificates from GCP over {pageNumber} pages."); + } + _logger.MethodExit(); + return numberOfCertificates; + } - _logger.LogTrace($"Setting up {typeof(CallSettings).ToString()} with provided {typeof(CancellationToken).ToString()} {this.ToString()}"); - CallSettings settings = CallSettings.FromCancellationToken(cancelToken); - _logger.LogDebug($"Downloading all issued certificates from GCP CAS {this.ToString()}"); - PagedAsyncEnumerable certificates = _client.ListCertificatesAsync(request, settings); - - int pageNumber = 0; - int numberOfCertificates = 0; - await foreach (var response in certificates.AsRawResponses()) - { - if (response.Certificates == null) - { - _logger.LogWarning($"GCP returned null {typeof(Google.Protobuf.Collections.RepeatedField).ToString()} object for page number {pageNumber} - optimistically continuing {this.ToString()}"); - continue; - } - foreach (Certificate certificate in response.Certificates) - { - certificatesBuffer.Add(AnyCAPluginCertificateFromGCPCertificate(certificate)); - numberOfCertificates++; - _logger.LogDebug($"Found Certificate with name {certificate.CertificateName.CertificateId} {this.ToString()}"); - } - _logger.LogTrace($"Fetched page {pageNumber} - Next Page Token: {response.NextPageToken}"); - pageNumber++; - } - _logger.LogDebug($"Fetched {certificatesBuffer.Count} certificates from GCP over {pageNumber} pages of certificates from GCP CAS {this.ToString()}"); - certificatesBuffer.CompleteAdding(); - return numberOfCertificates; - } /// /// Downloads a certificate with the specified in PEM format and stores it in a . @@ -228,7 +268,8 @@ public async Task DownloadAllIssuedCertificates(BlockingCollection and task result as a containing the downloaded certificate. /// public async Task DownloadCertificate(string certificateId) - { + { + _logger.MethodEntry(); EnsureClientIsEnabled(); _logger.LogDebug($"Downloading certificate with ID {certificateId} {this.ToString()}"); @@ -240,12 +281,14 @@ public async Task DownloadCertificate(string certificate }; Certificate certificate = await _client.GetCertificateAsync(request); - _logger.LogTrace("GetCertificateAsync succeeded"); + _logger.LogTrace("GetCertificateAsync succeeded"); + _logger.MethodExit(); return AnyCAPluginCertificateFromGCPCertificate(certificate); } private AnyCAPluginCertificate AnyCAPluginCertificateFromGCPCertificate(Certificate certificate) - { + { + _logger.MethodEntry(); string productId = ""; if (certificate.CertificateTemplateAsCertificateTemplateName == null) { @@ -266,8 +309,8 @@ private AnyCAPluginCertificate AnyCAPluginCertificateFromGCPCertificate(Certific revocationDate = certificate.RevocationDetails.RevocationTime.ToDateTime(); status = EndEntityStatus.REVOKED; revocationReason = (int)certificate.RevocationDetails.RevocationState; - } - + } + _logger.MethodExit(); return new AnyCAPluginCertificate { CARequestID = certificate.CertificateName.CertificateId, @@ -292,21 +335,60 @@ private AnyCAPluginCertificate AnyCAPluginCertificateFromGCPCertificate(Certific /// public async Task Enroll(ICreateCertificateRequestBuilder createCertificateRequestBuilder, CancellationToken cancelToken) { - EnsureClientIsEnabled(); + try + { + _logger.MethodEntry(); + EnsureClientIsEnabled(); + + CreateCertificateRequest request = createCertificateRequestBuilder.Build(_locationId, _projectId, _caPool, _caId); - CreateCertificateRequest request = createCertificateRequestBuilder.Build(_locationId, _projectId, _caPool, _caId); + if (request != null) + { + _logger.LogTrace($"Request Json {JsonConvert.SerializeObject(request)}"); + } - _logger.LogDebug($"Creating Certificate in GCP CAS with ID {request.CertificateId} {this.ToString()}"); - Certificate certificate = await _client.CreateCertificateAsync(request); - _logger.LogDebug($"Created Certificate in GCP CAS with name {certificate.CertificateName} {this.ToString()}"); + Certificate certificate = await _client.CreateCertificateAsync(request, cancelToken); - return new EnrollmentResult + if (certificate != null) + { + _logger.LogTrace($"Response Json {JsonConvert.SerializeObject(certificate)}"); + } + _logger.MethodExit(); + return new EnrollmentResult + { + CARequestID = certificate.CertificateName.CertificateId, + Certificate = certificate.PemCertificate, + Status = (int)EndEntityStatus.GENERATED, + StatusMessage = $"Certificate with ID {certificate.CertificateName} has been issued", + }; + } + catch (RpcException rpcEx) { - CARequestID = certificate.CertificateName.CertificateId, - Certificate = certificate.PemCertificate, - Status = (int)EndEntityStatus.GENERATED, - StatusMessage = $"Certificate with ID {certificate.CertificateName} has been issued", - }; + _logger.LogError(rpcEx, "RPC Exception while creating certificate."); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"RPC Error: {rpcEx.Status.Detail}", + }; + } + catch (OperationCanceledException) + { + _logger.LogWarning("Certificate enrollment operation was canceled."); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.CANCELLED, + StatusMessage = "Certificate enrollment was canceled.", + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error during certificate enrollment."); + return new EnrollmentResult + { + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"Unexpected error: {ex.Message}", + }; + } } /// @@ -320,7 +402,8 @@ public async Task Enroll(ICreateCertificateRequestBuilder crea /// /// public Task RevokeCertificate(string certificateId, RevocationReason reason) - { + { + _logger.MethodEntry(); EnsureClientIsEnabled(); _logger.LogDebug($"Revoking certificate with ID {certificateId} for reason {reason.ToString()} {this.ToString()}"); @@ -331,6 +414,7 @@ public Task RevokeCertificate(string certificateId, RevocationReason reason) Name = new CertificateName(_projectId, _locationId, _caPool, certificateId).ToString(), Reason = reason, }; + _logger.MethodExit(); return _client.RevokeCertificateAsync(request); } @@ -341,7 +425,8 @@ public Task RevokeCertificate(string certificateId, RevocationReason reason) /// A of containing the available s. /// public List GetTemplates() - { + { + _logger.MethodEntry(); EnsureClientIsEnabled(); _logger.LogDebug($"Getting Certificate Templates from GCP CA Service for Project: {_projectId}, Location: {_locationId}"); diff --git a/GCPCAS/Client/ICreateCertificateRequestBuilder.cs b/GCPCAS/Client/ICreateCertificateRequestBuilder.cs index d500e2c..1fd1f54 100644 --- a/GCPCAS/Client/ICreateCertificateRequestBuilder.cs +++ b/GCPCAS/Client/ICreateCertificateRequestBuilder.cs @@ -1,5 +1,5 @@ /* -Copyright Β© 2024 Keyfactor +Copyright Β© 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/GCPCAS/Client/IGCPCASClient.cs b/GCPCAS/Client/IGCPCASClient.cs index 25a3137..9f933d4 100644 --- a/GCPCAS/Client/IGCPCASClient.cs +++ b/GCPCAS/Client/IGCPCASClient.cs @@ -1,5 +1,5 @@ /* -Copyright Β© 2024 Keyfactor +Copyright Β© 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/GCPCAS/GCPCASCAPlugin.cs b/GCPCAS/GCPCASCAPlugin.cs index 02c78f6..7260ff1 100644 --- a/GCPCAS/GCPCASCAPlugin.cs +++ b/GCPCAS/GCPCASCAPlugin.cs @@ -1,5 +1,5 @@ ο»Ώ/* -Copyright Β© 2024 Keyfactor +Copyright Β© 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -43,35 +43,43 @@ public GCPCASCAPlugin() public GCPCASCAPlugin(IGCPCASClient client) { + _logger.MethodEntry(); Client = client; _gcpCasClientWasInjected = true; + _logger.MethodExit(); } public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) { + _logger.MethodEntry(); GCPCASClientFromCAConnectionData(configProvider.CAConnectionData); + _logger.MethodExit(); } public Dictionary GetCAConnectorAnnotations() { - _logger.LogDebug("Returning CA Connector Annotations (Properties)"); + _logger.MethodEntry(); + _logger.MethodExit(); return GCPCASPluginConfig.GetPluginAnnotations(); } public Dictionary GetTemplateParameterAnnotations() { - _logger.LogDebug("Returning Template Parameter Annotations (Custom Enrollment Parameters)"); + _logger.MethodEntry(); + _logger.MethodExit(); return GCPCASPluginConfig.GetTemplateParameterAnnotations(); } public List GetProductIds() { - _logger.LogDebug("Returning available Product IDs as Certificate Templates in the connected GCP CAS"); + _logger.MethodEntry(); + _logger.MethodExit(); return Client.GetTemplates(); } public async Task Ping() { + _logger.MethodEntry(); if (!Client.IsEnabled()) { _logger.LogDebug("GCPCASClient is disabled. Skipping Ping"); @@ -79,16 +87,20 @@ public async Task Ping() } _logger.LogDebug("Pinging GCP CAS to validate connection"); await Client.ValidateConnection(); + _logger.MethodExit(); } public Task ValidateCAConnectionInfo(Dictionary connectionInfo) { + _logger.MethodEntry(); GCPCASClientFromCAConnectionData(connectionInfo); + _logger.MethodExit(); return Ping(); } public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) { + _logger.MethodEntry(); // WithEnrollmentProductInfo() validates that the custom parameters in EnrollmentProductInfo are valid new CreateCertificateRequestBuilder().WithEnrollmentProductInfo(productInfo); // If this method doesn't throw, the product info is valid @@ -97,6 +109,7 @@ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) { + _logger.MethodEntry(); if (fullSync && lastSync != null) { _logger.LogInformation("Performing a full CA synchronization"); @@ -108,15 +121,19 @@ public async Task Synchronize(BlockingCollection blockin } int certificates = await Client.DownloadAllIssuedCertificates(blockingBuffer, cancelToken, lastSync); _logger.LogDebug($"Synchronized {certificates} certificates"); + _logger.MethodExit(); } public Task GetSingleRecord(string caRequestID) { + _logger.MethodEntry(); + _logger.MethodExit(); return Client.DownloadCertificate(caRequestID); } public Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { + _logger.MethodEntry(); ICreateCertificateRequestBuilder ccrBuilder = new CreateCertificateRequestBuilder() .WithCsr(csr) .WithSubject(subject) @@ -125,21 +142,25 @@ public Task Enroll(string csr, string subject, Dictionary Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) { + _logger.MethodEntry(); _logger.LogDebug($"Revoking certificate withKeyfactor.PKI.Enums.EJBCA.EndEntityStatus request ID: {caRequestID}"); // Google.Cloud.Security.PrivateCA.V1.RevocationReason has the same mapping as // Keyfactor.PKI.Enums.EJBCA.EndEntityStatus await Client.RevokeCertificate(caRequestID, (RevocationReason)revocationReason); + _logger.MethodExit(); return (int)EndEntityStatus.REVOKED; } private void GCPCASClientFromCAConnectionData(Dictionary connectionData) { + _logger.MethodEntry(); _logger.LogDebug($"Validating GCP CAS CA Connection properties"); var rawData = JsonSerializer.Serialize(connectionData); GCPCASPluginConfig.Config config = JsonSerializer.Deserialize(rawData); @@ -180,5 +201,6 @@ private void GCPCASClientFromCAConnectionData(Dictionary connect { Client.Disable(); } + _logger.MethodExit(); } } diff --git a/GCPCAS/GCPCASCAPluginConfig.cs b/GCPCAS/GCPCASCAPluginConfig.cs index 7990020..6d5e424 100644 --- a/GCPCAS/GCPCASCAPluginConfig.cs +++ b/GCPCAS/GCPCASCAPluginConfig.cs @@ -1,5 +1,5 @@ /* -Copyright Β© 2024 Keyfactor +Copyright Β© 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/GCPCAS/SubjectAltNamesParser.cs b/GCPCAS/SubjectAltNamesParser.cs new file mode 100644 index 0000000..e30b73f --- /dev/null +++ b/GCPCAS/SubjectAltNamesParser.cs @@ -0,0 +1,60 @@ +ο»Ώ/* +Copyright Β© 2025 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +using System; +using System.Collections.Generic; +using System.Net; +using Google.Cloud.Security.PrivateCA.V1; + +namespace Keyfactor.Extensions.CAPlugin.GCPCAS +{ + + public class SubjectAltNamesParser + { + public static SubjectAltNames ParseFromDnsList(List dnsSans) + { + if (dnsSans == null || dnsSans.Count == 0) + return new SubjectAltNames(); + + var subjectAltNames = new SubjectAltNames(); + + foreach (var entry in dnsSans) + { + if (string.IsNullOrWhiteSpace(entry)) + continue; + + if (entry.Contains("@")) // Email detection + { + subjectAltNames.EmailAddresses.Add(entry); + } + else if (Uri.IsWellFormedUriString(entry, UriKind.Absolute)) // URI detection + { + subjectAltNames.Uris.Add(entry); + } + else if (IPAddress.TryParse(entry, out _)) // IP Address detection + { + subjectAltNames.IpAddresses.Add(entry); + } + else if (entry.Contains(".")) // DNS Name detection + { + subjectAltNames.DnsNames.Add(entry); + } + } + + return subjectAltNames; + } + + } +} diff --git a/GCPCAS/SubjectParser.cs b/GCPCAS/SubjectParser.cs new file mode 100644 index 0000000..f1d84f5 --- /dev/null +++ b/GCPCAS/SubjectParser.cs @@ -0,0 +1,79 @@ +ο»Ώ/* +Copyright Β© 2025 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +using Google.Cloud.Security.PrivateCA.V1; + +namespace Keyfactor.Extensions.CAPlugin.GCPCAS +{ + + + public class SubjectParser + { + public static Subject ParseFromString(string subjectString) + { + if (string.IsNullOrWhiteSpace(subjectString)) + return null; + + var subject = new Subject(); + string[] keyValuePairs = subjectString.Split(','); + + foreach (var pair in keyValuePairs) + { + var parts = pair.Split('='); + if (parts.Length == 2) + { + string key = parts[0].Trim().ToUpper(); // Normalize key case + string value = parts[1].Trim(); + + if (!string.IsNullOrEmpty(value)) + { + switch (key) + { + case "CN": + subject.CommonName = value; + break; + case "C": + subject.CountryCode = value; + break; + case "O": + subject.Organization = value; + break; + case "OU": + subject.OrganizationalUnit = value; + break; + case "L": + subject.Locality = value; + break; + case "ST": + subject.Province = value; + break; + case "STREET": + subject.StreetAddress = value; + break; + case "PC": + subject.PostalCode = value; + break; + default: + // Ignore unknown keys + break; + } + } + } + } + + return subject; + } + } +} diff --git a/GCPCAS/Utilities.cs b/GCPCAS/Utilities.cs deleted file mode 100644 index 747ac11..0000000 --- a/GCPCAS/Utilities.cs +++ /dev/null @@ -1,30 +0,0 @@ -ο»Ώ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 f1386a6..1486275 100644 --- a/README.md +++ b/README.md @@ -142,20 +142,456 @@ Both the Keyfactor Command and AnyCA Gateway REST servers must trust the root CA The GCP CAS AnyCA Gateway REST plugin downloads all Certificate Templates in the configured GCP Region/Project and interprets them as 'Product IDs' in the Gateway Portal. - > For example, if the connected GCP project has the following Certificate Templates: - > - > * `ServerAuth` - > * `ClientAuth` - > - > The `Edit Templates` > `Product ID` dialog dropdown will show the following available 'ProductIDs': - > - > * `Default` -> Don't use a certificate template when enrolling certificates with this Template. - > * `ServerAuth` -> Use the `ServerAuth` certificate template in GCP when enrolling certificates with this Template. - > * `ClientAuth` -> Use the `ClientAuth` certificate template in GCP when enrolling certificates with this Template. + + ### Define Certificate Profiles and Templates + Certificate Profiles and Templates define how certificates are issued through **Google CAS**. + + - Each **Certificate Profile** corresponds to a **Certificate Template** in Google CAS. + - The **AnyCA Gateway REST plugin** fetches all available **Google CAS Certificate Templates** and maps them as **Product IDs** in **Keyfactor Gateway**. + + #### **Example Mapping of Google CAS Templates to Keyfactor Product IDs** + + | Google CAS Certificate Template | Keyfactor Product ID | Usage | + |---------------------------------|----------------------|-------| + | `ServerCertificate` | `ServerCertificate` | Server authentication | + | `ClientAuth` | `ClientAuth` | Client authentication | + | `ClientAuthCert` | `ClientAuthCert` | Custom client authentication | + | `CSROnly` | `CSROnly` | CSR-based issuance | + | **None (No Template Used)** | `Default` | Uses CA-level settings | + + > **Note:** If `Default` is selected, **Google CAS will issue certificates based on CA settings rather than a specific template**. 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. +## Google Certificate Authority Service (CAS) Setup for Keyfactor Integration + +### Overview + +This guide provides a step-by-step approach to setting up **Google Certificate Authority Service (CAS)** and integrating it with **Keyfactor** for certificate enrollment. Since Google CAS does not extract metadata from Certificate Signing Requests (CSRs), certificate templates must be defined in CAS to allow Keyfactor to request certificates correctly. While **templates are preferred**, they are **not required**β€”if the **Default** Product ID is used, certificates will be generated based on the CA settings instead of a template. + +--- + +### Google CAS Setup + +#### **Step 1: Enable Certificate Authority Service API** + +```sh +gcloud services enable privateca.googleapis.com +``` + +#### **Step 2: Create a Root Certificate Authority (CA)** + +```sh +gcloud privateca roots create my-root-ca \ + --location=us-central1 \ + --key-algorithm=rsa-pkcs1-4096-sha256 \ + --subject="CN=My Root CA, O=My Organization, C=US" \ + --use-preset-profile=ROOT_CA_DEFAULT \ + --bucket=my-ca-bucket +``` + +#### **Step 3: Define Certificate Key Usage and Extended Key Usage** + +Certificate Key Usage and Extended Key Usage define how the certificates issued by the CA can be used. These must be set at the **CA policy level** or within a **certificate template**. + +##### **Option 1: Define Key Usage in CA Policy** + +Create a CA policy file (`ca-policy.json`): + +```json +{ + "baselineValues": { + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true, + "clientAuth": true + } + } + } +} +``` + +Apply the policy when creating the CA: + +```sh +gcloud privateca roots create my-root-ca \ + --location=us-central1 \ + --key-algorithm=rsa-pkcs1-4096-sha256 \ + --subject="CN=My Root CA, O=My Organization, C=US" \ + --use-preset-profile=ROOT_CA_DEFAULT \ + --bucket=my-ca-bucket \ + --ca-policy=ca-policy.json +``` + +##### **Option 2: Define Key Usage in a Certificate Template (Preferred but Not Required)** + +If using a certificate template, create a policy file (`cert-template-policy.json`): + +```json +{ + "predefinedValues": { + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true, + "clientAuth": true + } + } + } +} +``` + +Create the template: + +```sh +gcloud privateca templates create my-cert-template \ + --location=us-central1 \ + --policy-file=cert-template-policy.json +``` + +If a **template is not used**, certificates will be generated **directly based on CA settings**. + +--- + +### **Certificate Signing Request (CSR) Handling in Google CAS** + +- **CSR is only used for the private key proof-of-possession.** +- **All certificate metadata (e.g., Subject, SANs) must be provided via configuration files or templates.** +- **Additional fields in the CSR are ignored by Google CAS.** + +#### **Example: Issuing a Certificate with a CSR** + +##### **1. Generate a CSR** + +```sh +openssl req -new -newkey rsa:2048 -nodes -keyout my-key.pem -out my-csr.pem -subj "/CN=ignored.example.com" +``` + +##### **2. Define Certificate Configuration** + +```json +{ + "lifetime": "2592000s", + "subjectConfig": { + "subject": { + "commonName": "mydomain.com", + "organization": "My Organization", + "countryCode": "US" + }, + "subjectAltName": { + "dnsNames": ["mydomain.com", "www.mydomain.com"] + } + }, + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true + } + } +} +``` + +##### **3. Issue the Certificate** + +```sh +gcloud privateca certificates create my-cert \ + --issuer-pool=my-root-ca \ + --csr my-csr.pem \ + --config-file cert-config.json \ + --location=us-central1 +``` + +--- + +### **Integrating Keyfactor with Google CAS** + +#### **Why Use Certificate Templates?** + +- **Google CAS does not extract metadata from CSRs.** +- **Keyfactor prefers enrollment of certificates via predefined templates** to ensure all attributes (e.g., Subject, SANs) are correctly applied. +- **Prevents unauthorized data injection via CSRs.** +- **If no template is used, certificates will be issued based on CA settings using the Default Product ID.** + +#### **Step 1: Create a Certificate Template for Keyfactor (Preferred but Not Required)** + +Create a **certificate template policy file** (`keyfactor-template-policy.json`): + +```json +{ + "predefinedValues": { + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true, + "clientAuth": true + } + } + }, + "identityConstraints": { + "allowSubjectPassthrough": true, + "allowSubjectAltNamesPassthrough": true + } +} +``` + +Create the template: + +```sh +gcloud privateca templates create keyfactor-template \ + --location=us-central1 \ + --policy-file=keyfactor-template-policy.json +``` + +If using the **Default** Product ID in Keyfactor, Google CAS will generate certificates directly from CA settings **without requiring a template**. + +--- + +## Test Case 1: Enrollment from Keyfactor Command with No SANs + +### **Description** +This test validates that a certificate enrollment request from **Keyfactor Command** is successfully processed by **Google CAS** when no **Subject Alternative Names (SANs)** are provided. + +### **Test Steps** +1. Navigate to **Keyfactor Command β†’ Enrollment**. +2. Fill in the following details: + - **Common Name (CN):** `www.nosanstest.com` + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Certificate Authority:** Auto-Select +3. Ensure **no Subject Alternative Names (SANs) are added**. +4. Select **Direct Download** as the Certificate Delivery Format. +5. Click **Enroll**. +6. Verify the certificate issuance in **Keyfactor Command**. +7. Validate the certificate details in **Google CAS**. + +### **Expected Result** +βœ… The certificate should be **issued via Google CAS**. +βœ… The certificate should be **downloaded into Keyfactor Command**. +βœ… The certificate should be **published to Google CAS**. + +### **Actual Result** +βœ… The certificate was successfully issued and downloaded in **Keyfactor Command**. +βœ… The certificate was correctly published and appears in **Google CAS**. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 2: Enroll From Command With Different SANs and SAN Types + +### **Description** +This test validates that **Keyfactor Command** can enroll a certificate with **multiple SAN types**, including DNS, IP, and email, and that it is correctly processed by **Google CAS**. + +### **Test Steps** +1. Navigate to **Keyfactor Command β†’ Enrollment**. +2. Fill in the following details: + - **Common Name (CN):** `www.differentsans.com` + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Certificate Authority:** Auto-Select +3. Add the following **Subject Alternative Names (SANs):** + - DNS: `differentsans.com` + - IP: `127.0.0.1` + - IP: `127.0.0.2` + - Email: `bhill@keyfactor.com` +4. Select **Direct Download** as the Certificate Delivery Format. +5. Click **Enroll**. +6. Verify the certificate issuance in **Keyfactor Command**. +7. Validate the certificate details in **Google CAS**. + +### **Expected Result** +βœ… The certificate should be **issued with the specified SANs**. +βœ… The certificate should be **downloaded into Keyfactor Command**. +βœ… The certificate should be **published to Google CAS**. + +### **Actual Result** +βœ… The certificate was successfully issued and downloaded in **Keyfactor Command**. +βœ… The certificate was correctly published in **Google CAS**, with all SANs properly applied. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 3: Enrollment From Keyfactor Command Using the Google Default Template + +### **Description** +This test validates that when using the **Google Default Template**, the certificate issuance follows **CA-level settings** rather than a specific template. + +### **Test Steps** +1. Navigate to **Keyfactor Command β†’ Enrollment**. +2. Fill in the following details: + - **Common Name (CN):** `www.usecasettings.com` + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Template:** `AnyCA (Default)` + - **Certificate Authority:** Auto-Select +3. Ensure **no Subject Alternative Names (SANs) are added**. +4. Select **Direct Download** as the Certificate Delivery Format. +5. Click **Enroll**. +6. Verify the certificate issuance in **Keyfactor Command**. +7. Validate the certificate details in **Google CAS**. + +### **Expected Result** +βœ… The certificate should be **issued using the CA-level settings**. +βœ… The certificate should be **downloaded into Keyfactor Command**. +βœ… The certificate should be **published to Google CAS**. + +### **Actual Result** +βœ… The certificate was successfully issued and downloaded in **Keyfactor Command**. +βœ… The certificate was correctly published in **Google CAS**, following CA-level settings. + +### **Test Status:** βœ… **Pass** + +## Test Case 4: Auto Enrollment via Keyfactor's Windows Enrollment Gateway using Client Authentication + +### **Description** +This test validates that when using **Keyfactor's Windows Enrollment Gateway**, the certificate issuance follows the expected **Active Directory Enrollment Policy** settings for client authentication. The enrolled certificate should include the correct **template information**, **key usage**, and **extensions** as defined in Active Directory Certificate Services (ADCS). The enrollment process is performed via the **Microsoft Management Console (MMC)**. + +### **Test Steps** +1. Open the **Microsoft Management Console (MMC)** and navigate to **Certificates - Current User β†’ Personal β†’ Certificates**. +2. Right-click on **Certificates**, go to **All Tasks**, and select **Request New Certificate...**. +3. In the **Certificate Enrollment Wizard**, select the **Active Directory Enrollment Policy**. +4. Select the **ClientAuthCert** template. +5. Ensure the following settings are applied: + - **Common Name (CN):** Retrieved from Active Directory (e.g., `kfadmin`) + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Template:** `ClientAuthCert` + - **Certificate Authority:** Auto-Select + - **Application Policies:** + - Secure Email + - Encrypting File System + - Client Authentication + - **Extensions Included:** + - Application Policies + - Basic Constraints + - Certificate Template Information + - Issuance Policies + - Key Usage +6. Click **Enroll**. +7. Verify the certificate issuance in **Keyfactor Command**. +8. Open the issued certificate in **MMC** and validate: + - **Certificate Template Information** matches `ClientAuthCert`. + - **Object Identifier (OID):** `1.3.6.1.4.1.311.21.8.4181979.15981577.14434469.15789051.5877270.183.12847830.8177055` + - **Major Version Number:** 100 + - **Minor Version Number:** 10 + - **Key Usage:** Digital Signature, Key Encipherment + - **Subject Alternative Name (SAN):** Includes `kfadmin@Command.local` and `bhill@keyfactor.com` + - **SHA-256 Fingerprint:** `f917786fa2519d277238cb2da06b457a771562aad3ded1729b6c9ffde0d65ee` +9. Validate the certificate details in **Google Private CA** to confirm it was correctly registered. + +### **Expected Result** +βœ… The certificate should be **issued using the ClientAuthCert template**. +βœ… The certificate should be **downloaded into the Windows Certificate Store via MMC**. +βœ… The certificate should be **published to Keyfactor Command**. +βœ… The certificate should be **registered in Google Private CA**. +βœ… The certificate should include **correct template information, extensions, and metadata**. + +### **Actual Result** +βœ… The certificate was successfully issued and installed in **Windows Certificate Store via MMC**. +βœ… The certificate was correctly published in **Keyfactor Command**. +βœ… The certificate was correctly registered in **Google Private CA**, following the expected template settings. +βœ… The certificate includes the correct **template information, key usage, and extensions**. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 5: Inventory All Certificates from the CA in Google CAS into Keyfactor Command + +### **Description** +This test ensures that all certificates issued by the **Google Private CA** are successfully inventoried into **Keyfactor Command** and that the total number of certificates matches between the two systems. + +### **Test Steps** +1. Log in to **Keyfactor Command**. +2. Navigate to **Inventory β†’ Certificate Authority Synchronization**. +3. Select the configured **Google Private CA** integration. +4. Click **Sync Now** to start the certificate inventory process. +5. Once the sync completes, navigate to **Certificates β†’ Search**. +6. Retrieve the total number of certificates inventoried from Google CAS. +7. Log in to **Google Private CA**. +8. Navigate to **Certificates** and retrieve the total number of issued certificates. +9. Compare the count from Google CAS with the count in Keyfactor Command. + +### **Expected Result** +βœ… The total number of certificates in **Google Private CA** should match the number inventoried in **Keyfactor Command**. +βœ… All certificates should appear in **Keyfactor Command** with the correct metadata. + +### **Actual Result** +βœ… The certificate count in **Keyfactor Command** matches the count in **Google Private CA**. +βœ… All certificates were successfully inventoried with accurate metadata. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 6: Renew Certificate from Keyfactor Command and Ensure a New Certificate is Generated in Google CAS + +### **Description** +This test validates that when a certificate is renewed from **Keyfactor Command**, a new certificate is generated and registered in **Google Private CA**. + +### **Test Steps** +1. Log in to **Keyfactor Command**. +2. Navigate to **Certificates β†’ Search** and locate the certificate that needs renewal. +3. Click on the certificate and select **Renew Certificate**. +4. Choose **Auto-Select CA** and confirm the renewal request. +5. Verify that a new certificate has been issued in **Keyfactor Command**. +6. Log in to **Google Private CA**. +7. Navigate to **Certificates** and ensure a new certificate instance appears with a new serial number. +8. Compare the renewed certificate’s details in **Google CAS** with **Keyfactor Command**. +9. Validate the SHA-256 fingerprint of the renewed certificate to ensure uniqueness. + +### **Expected Result** +βœ… The certificate should be **renewed in Keyfactor Command**. +βœ… A new certificate with a unique serial number should appear in **Google Private CA**. +βœ… The renewed certificate should match the expected template and metadata. + +### **Actual Result** +βœ… The certificate was successfully renewed in **Keyfactor Command**. +βœ… A new certificate was generated in **Google Private CA** with a new serial number. +βœ… The metadata and template settings match expected values. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 7: Revoke Certificate from Keyfactor Command with All Available Reasons + +### **Description** +This test ensures that certificates can be revoked from **Keyfactor Command**, using all available revocation reasons, and that the revocation is correctly applied in **Google Private CA**. + +### **Test Steps** +1. Log in to **Keyfactor Command**. +2. Navigate to **Certificates β†’ Search** and locate the certificate to be revoked. +3. Click on the certificate and select **Revoke Certificate**. +4. Choose each revocation reason and confirm the revocation: + - Reason Unspecified + - Key Compromised + - CA Compromised + - Affiliation Changed + - Superseded + - Cessation Of Operation + - Certificate Hold + - Remove From Hold +5. Verify that the certificate is marked as revoked in **Keyfactor Command**. +6. Log in to **Google Private CA** and ensure the certificate is revoked with the selected reason. + +### **Test Status:** βœ… **Pass** + ## License diff --git a/docsource/configuration.md b/docsource/configuration.md index dcdddb9..fe02049 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -69,25 +69,455 @@ Define [Certificate Profiles](https://software.keyfactor.com/Guides/AnyCAGateway The GCP CAS AnyCA Gateway REST plugin downloads all Certificate Templates in the configured GCP Region/Project and interprets them as 'Product IDs' in the Gateway Portal. -> For example, if the connected GCP project has the following Certificate Templates: -> -> * `ServerAuth` -> * `ClientAuth` -> -> The `Edit Templates` > `Product ID` dialog dropdown will show the following available 'ProductIDs': -> -> * `Default` -> Don't use a certificate template when enrolling certificates with this Template. -> * `ServerAuth` -> Use the `ServerAuth` certificate template in GCP when enrolling certificates with this Template. -> * `ClientAuth` -> Use the `ClientAuth` certificate template in GCP when enrolling certificates with this Template. -## Mechanics +### Define Certificate Profiles and Templates +Certificate Profiles and Templates define how certificates are issued through **Google CAS**. + +- Each **Certificate Profile** corresponds to a **Certificate Template** in Google CAS. +- The **AnyCA Gateway REST plugin** fetches all available **Google CAS Certificate Templates** and maps them as **Product IDs** in **Keyfactor Gateway**. + +#### **Example Mapping of Google CAS Templates to Keyfactor Product IDs** + +| Google CAS Certificate Template | Keyfactor Product ID | Usage | +|---------------------------------|----------------------|-------| +| `ServerCertificate` | `ServerCertificate` | Server authentication | +| `ClientAuth` | `ClientAuth` | Client authentication | +| `ClientAuthCert` | `ClientAuthCert` | Custom client authentication | +| `CSROnly` | `CSROnly` | CSR-based issuance | +| **None (No Template Used)** | `Default` | Uses CA-level settings | + +> **Note:** If `Default` is selected, **Google CAS will issue certificates based on CA settings rather than a specific template**. + +## Google Certificate Authority Service (CAS) Setup for Keyfactor Integration + +### Overview + +This guide provides a step-by-step approach to setting up **Google Certificate Authority Service (CAS)** and integrating it with **Keyfactor** for certificate enrollment. Since Google CAS does not extract metadata from Certificate Signing Requests (CSRs), certificate templates must be defined in CAS to allow Keyfactor to request certificates correctly. While **templates are preferred**, they are **not required**β€”if the **Default** Product ID is used, certificates will be generated based on the CA settings instead of a template. + +--- + +### Google CAS Setup + +#### **Step 1: Enable Certificate Authority Service API** + +```sh +gcloud services enable privateca.googleapis.com +``` + +#### **Step 2: Create a Root Certificate Authority (CA)** + +```sh +gcloud privateca roots create my-root-ca \ + --location=us-central1 \ + --key-algorithm=rsa-pkcs1-4096-sha256 \ + --subject="CN=My Root CA, O=My Organization, C=US" \ + --use-preset-profile=ROOT_CA_DEFAULT \ + --bucket=my-ca-bucket +``` + +#### **Step 3: Define Certificate Key Usage and Extended Key Usage** + +Certificate Key Usage and Extended Key Usage define how the certificates issued by the CA can be used. These must be set at the **CA policy level** or within a **certificate template**. + +##### **Option 1: Define Key Usage in CA Policy** + +Create a CA policy file (`ca-policy.json`): + +```json +{ + "baselineValues": { + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true, + "clientAuth": true + } + } + } +} +``` + +Apply the policy when creating the CA: + +```sh +gcloud privateca roots create my-root-ca \ + --location=us-central1 \ + --key-algorithm=rsa-pkcs1-4096-sha256 \ + --subject="CN=My Root CA, O=My Organization, C=US" \ + --use-preset-profile=ROOT_CA_DEFAULT \ + --bucket=my-ca-bucket \ + --ca-policy=ca-policy.json +``` + +##### **Option 2: Define Key Usage in a Certificate Template (Preferred but Not Required)** + +If using a certificate template, create a policy file (`cert-template-policy.json`): + +```json +{ + "predefinedValues": { + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true, + "clientAuth": true + } + } + } +} +``` + +Create the template: + +```sh +gcloud privateca templates create my-cert-template \ + --location=us-central1 \ + --policy-file=cert-template-policy.json +``` + +If a **template is not used**, certificates will be generated **directly based on CA settings**. + +--- + +### **Certificate Signing Request (CSR) Handling in Google CAS** + +- **CSR is only used for the private key proof-of-possession.** +- **All certificate metadata (e.g., Subject, SANs) must be provided via configuration files or templates.** +- **Additional fields in the CSR are ignored by Google CAS.** + +#### **Example: Issuing a Certificate with a CSR** + +##### **1. Generate a CSR** + +```sh +openssl req -new -newkey rsa:2048 -nodes -keyout my-key.pem -out my-csr.pem -subj "/CN=ignored.example.com" +``` + +##### **2. Define Certificate Configuration** + +```json +{ + "lifetime": "2592000s", + "subjectConfig": { + "subject": { + "commonName": "mydomain.com", + "organization": "My Organization", + "countryCode": "US" + }, + "subjectAltName": { + "dnsNames": ["mydomain.com", "www.mydomain.com"] + } + }, + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true + } + } +} +``` + +##### **3. Issue the Certificate** + +```sh +gcloud privateca certificates create my-cert \ + --issuer-pool=my-root-ca \ + --csr my-csr.pem \ + --config-file cert-config.json \ + --location=us-central1 +``` + +--- + +### **Integrating Keyfactor with Google CAS** + +#### **Why Use Certificate Templates?** + +- **Google CAS does not extract metadata from CSRs.** +- **Keyfactor prefers enrollment of certificates via predefined templates** to ensure all attributes (e.g., Subject, SANs) are correctly applied. +- **Prevents unauthorized data injection via CSRs.** +- **If no template is used, certificates will be issued based on CA settings using the Default Product ID.** + +#### **Step 1: Create a Certificate Template for Keyfactor (Preferred but Not Required)** + +Create a **certificate template policy file** (`keyfactor-template-policy.json`): + +```json +{ + "predefinedValues": { + "keyUsage": { + "baseKeyUsage": { + "digitalSignature": true, + "keyEncipherment": true + }, + "extendedKeyUsage": { + "serverAuth": true, + "clientAuth": true + } + } + }, + "identityConstraints": { + "allowSubjectPassthrough": true, + "allowSubjectAltNamesPassthrough": true + } +} +``` + +Create the template: + +```sh +gcloud privateca templates create keyfactor-template \ + --location=us-central1 \ + --policy-file=keyfactor-template-policy.json +``` + +If using the **Default** Product ID in Keyfactor, Google CAS will generate certificates directly from CA settings **without requiring a template**. + +--- + + +# Test Cases + +## Test Case 1: Enrollment from Keyfactor Command with No SANs + +### **Description** +This test validates that a certificate enrollment request from **Keyfactor Command** is successfully processed by **Google CAS** when no **Subject Alternative Names (SANs)** are provided. + +### **Test Steps** +1. Navigate to **Keyfactor Command β†’ Enrollment**. +2. Fill in the following details: + - **Common Name (CN):** `www.nosanstest.com` + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Certificate Authority:** Auto-Select +3. Ensure **no Subject Alternative Names (SANs) are added**. +4. Select **Direct Download** as the Certificate Delivery Format. +5. Click **Enroll**. +6. Verify the certificate issuance in **Keyfactor Command**. +7. Validate the certificate details in **Google CAS**. + +### **Expected Result** +βœ… The certificate should be **issued via Google CAS**. +βœ… The certificate should be **downloaded into Keyfactor Command**. +βœ… The certificate should be **published to Google CAS**. + +### **Actual Result** +βœ… The certificate was successfully issued and downloaded in **Keyfactor Command**. +βœ… The certificate was correctly published and appears in **Google CAS**. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 2: Enroll From Command With Different SANs and SAN Types + +### **Description** +This test validates that **Keyfactor Command** can enroll a certificate with **multiple SAN types**, including DNS, IP, and email, and that it is correctly processed by **Google CAS**. + +### **Test Steps** +1. Navigate to **Keyfactor Command β†’ Enrollment**. +2. Fill in the following details: + - **Common Name (CN):** `www.differentsans.com` + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Certificate Authority:** Auto-Select +3. Add the following **Subject Alternative Names (SANs):** + - DNS: `differentsans.com` + - IP: `127.0.0.1` + - IP: `127.0.0.2` + - Email: `bhill@keyfactor.com` +4. Select **Direct Download** as the Certificate Delivery Format. +5. Click **Enroll**. +6. Verify the certificate issuance in **Keyfactor Command**. +7. Validate the certificate details in **Google CAS**. + +### **Expected Result** +βœ… The certificate should be **issued with the specified SANs**. +βœ… The certificate should be **downloaded into Keyfactor Command**. +βœ… The certificate should be **published to Google CAS**. + +### **Actual Result** +βœ… The certificate was successfully issued and downloaded in **Keyfactor Command**. +βœ… The certificate was correctly published in **Google CAS**, with all SANs properly applied. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 3: Enrollment From Keyfactor Command Using the Google Default Template + +### **Description** +This test validates that when using the **Google Default Template**, the certificate issuance follows **CA-level settings** rather than a specific template. + +### **Test Steps** +1. Navigate to **Keyfactor Command β†’ Enrollment**. +2. Fill in the following details: + - **Common Name (CN):** `www.usecasettings.com` + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Template:** `AnyCA (Default)` + - **Certificate Authority:** Auto-Select +3. Ensure **no Subject Alternative Names (SANs) are added**. +4. Select **Direct Download** as the Certificate Delivery Format. +5. Click **Enroll**. +6. Verify the certificate issuance in **Keyfactor Command**. +7. Validate the certificate details in **Google CAS**. + +### **Expected Result** +βœ… The certificate should be **issued using the CA-level settings**. +βœ… The certificate should be **downloaded into Keyfactor Command**. +βœ… The certificate should be **published to Google CAS**. + +### **Actual Result** +βœ… The certificate was successfully issued and downloaded in **Keyfactor Command**. +βœ… The certificate was correctly published in **Google CAS**, following CA-level settings. + +### **Test Status:** βœ… **Pass** + +## Test Case 4: Auto Enrollment via Keyfactor's Windows Enrollment Gateway using Client Authentication + +### **Description** +This test validates that when using **Keyfactor's Windows Enrollment Gateway**, the certificate issuance follows the expected **Active Directory Enrollment Policy** settings for client authentication. The enrolled certificate should include the correct **template information**, **key usage**, and **extensions** as defined in Active Directory Certificate Services (ADCS). The enrollment process is performed via the **Microsoft Management Console (MMC)**. + +### **Test Steps** +1. Open the **Microsoft Management Console (MMC)** and navigate to **Certificates - Current User β†’ Personal β†’ Certificates**. +2. Right-click on **Certificates**, go to **All Tasks**, and select **Request New Certificate...**. +3. In the **Certificate Enrollment Wizard**, select the **Active Directory Enrollment Policy**. +4. Select the **ClientAuthCert** template. +5. Ensure the following settings are applied: + - **Common Name (CN):** Retrieved from Active Directory (e.g., `kfadmin`) + - **Key Algorithm:** RSA + - **Key Size:** 2048 + - **Template:** `ClientAuthCert` + - **Certificate Authority:** Auto-Select + - **Application Policies:** + - Secure Email + - Encrypting File System + - Client Authentication + - **Extensions Included:** + - Application Policies + - Basic Constraints + - Certificate Template Information + - Issuance Policies + - Key Usage +6. Click **Enroll**. +7. Verify the certificate issuance in **Keyfactor Command**. +8. Open the issued certificate in **MMC** and validate: + - **Certificate Template Information** matches `ClientAuthCert`. + - **Object Identifier (OID):** `1.3.6.1.4.1.311.21.8.4181979.15981577.14434469.15789051.5877270.183.12847830.8177055` + - **Major Version Number:** 100 + - **Minor Version Number:** 10 + - **Key Usage:** Digital Signature, Key Encipherment + - **Subject Alternative Name (SAN):** Includes `kfadmin@Command.local` and `bhill@keyfactor.com` + - **SHA-256 Fingerprint:** `f917786fa2519d277238cb2da06b457a771562aad3ded1729b6c9ffde0d65ee` +9. Validate the certificate details in **Google Private CA** to confirm it was correctly registered. + +### **Expected Result** +βœ… The certificate should be **issued using the ClientAuthCert template**. +βœ… The certificate should be **downloaded into the Windows Certificate Store via MMC**. +βœ… The certificate should be **published to Keyfactor Command**. +βœ… The certificate should be **registered in Google Private CA**. +βœ… The certificate should include **correct template information, extensions, and metadata**. + +### **Actual Result** +βœ… The certificate was successfully issued and installed in **Windows Certificate Store via MMC**. +βœ… The certificate was correctly published in **Keyfactor Command**. +βœ… The certificate was correctly registered in **Google Private CA**, following the expected template settings. +βœ… The certificate includes the correct **template information, key usage, and extensions**. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 5: Inventory All Certificates from the CA in Google CAS into Keyfactor Command + +### **Description** +This test ensures that all certificates issued by the **Google Private CA** are successfully inventoried into **Keyfactor Command** and that the total number of certificates matches between the two systems. + +### **Test Steps** +1. Log in to **Keyfactor Command**. +2. Navigate to **Inventory β†’ Certificate Authority Synchronization**. +3. Select the configured **Google Private CA** integration. +4. Click **Sync Now** to start the certificate inventory process. +5. Once the sync completes, navigate to **Certificates β†’ Search**. +6. Retrieve the total number of certificates inventoried from Google CAS. +7. Log in to **Google Private CA**. +8. Navigate to **Certificates** and retrieve the total number of issued certificates. +9. Compare the count from Google CAS with the count in Keyfactor Command. + +### **Expected Result** +βœ… The total number of certificates in **Google Private CA** should match the number inventoried in **Keyfactor Command**. +βœ… All certificates should appear in **Keyfactor Command** with the correct metadata. + +### **Actual Result** +βœ… The certificate count in **Keyfactor Command** matches the count in **Google Private CA**. +βœ… All certificates were successfully inventoried with accurate metadata. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 6: Renew Certificate from Keyfactor Command and Ensure a New Certificate is Generated in Google CAS + +### **Description** +This test validates that when a certificate is renewed from **Keyfactor Command**, a new certificate is generated and registered in **Google Private CA**. + +### **Test Steps** +1. Log in to **Keyfactor Command**. +2. Navigate to **Certificates β†’ Search** and locate the certificate that needs renewal. +3. Click on the certificate and select **Renew Certificate**. +4. Choose **Auto-Select CA** and confirm the renewal request. +5. Verify that a new certificate has been issued in **Keyfactor Command**. +6. Log in to **Google Private CA**. +7. Navigate to **Certificates** and ensure a new certificate instance appears with a new serial number. +8. Compare the renewed certificate’s details in **Google CAS** with **Keyfactor Command**. +9. Validate the SHA-256 fingerprint of the renewed certificate to ensure uniqueness. + +### **Expected Result** +βœ… The certificate should be **renewed in Keyfactor Command**. +βœ… A new certificate with a unique serial number should appear in **Google Private CA**. +βœ… The renewed certificate should match the expected template and metadata. + +### **Actual Result** +βœ… The certificate was successfully renewed in **Keyfactor Command**. +βœ… A new certificate was generated in **Google Private CA** with a new serial number. +βœ… The metadata and template settings match expected values. + +### **Test Status:** βœ… **Pass** + +--- + +## Test Case 7: Revoke Certificate from Keyfactor Command with All Available Reasons + +### **Description** +This test ensures that certificates can be revoked from **Keyfactor Command**, using all available revocation reasons, and that the revocation is correctly applied in **Google Private CA**. -### Enrollment/Renewal/Reissuance +### **Test Steps** +1. Log in to **Keyfactor Command**. +2. Navigate to **Certificates β†’ Search** and locate the certificate to be revoked. +3. Click on the certificate and select **Revoke Certificate**. +4. Choose each revocation reason and confirm the revocation: + - Reason Unspecified + - Key Compromised + - CA Compromised + - Affiliation Changed + - Superseded + - Cessation Of Operation + - Certificate Hold + - Remove From Hold +5. Verify that the certificate is marked as revoked in **Keyfactor Command**. +6. Log in to **Google Private CA** and ensure the certificate is revoked with the selected reason. -The GCP CAS AnyCA Gateway REST plugin treats _all_ certificate enrollment as a new enrollment. +### **Test Status:** βœ… **Pass** -### 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)