diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml new file mode 100644 index 0000000..64919a4 --- /dev/null +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -0,0 +1,20 @@ +name: Keyfactor Bootstrap Workflow + +on: + workflow_dispatch: + pull_request: + types: [opened, closed, synchronize, edited, reopened] + push: + create: + branches: + - 'release-*.*' + +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@v3 + secrets: + token: ${{ secrets.V2BUILDTOKEN}} + APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..6163096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +1.0.0 +Inital Release. Support for Enroll, Sync, and Revocation. \ No newline at end of file diff --git a/cagateway-template/APIProxy/ProductNameBaseCall.cs b/cagateway-template/APIProxy/ProductNameBaseCall.cs deleted file mode 100644 index 1b92523..0000000 --- a/cagateway-template/APIProxy/ProductNameBaseCall.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Newtonsoft.Json; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Keyfactor.Extensions.AnyGateway.Company.Product.APIProxy -{ - public abstract class ProductNameBaseRequest - { - [JsonIgnore] - public string Resource { get; internal set; } - - [JsonIgnore] - public string Method { get; internal set; } - - [JsonIgnore] - public string targetURI { get; set; } - - public string BuildParameters() - { - return ""; - } - } -} \ No newline at end of file diff --git a/cagateway-template/Client/ProductNameClient.cs b/cagateway-template/Client/ProductNameClient.cs deleted file mode 100644 index cbd16ab..0000000 --- a/cagateway-template/Client/ProductNameClient.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Keyfactor.Extensions.AnyGateway.Company.Product.Client -{ - public class ProductNameClient - { - } -} \ No newline at end of file diff --git a/cagateway-template/Constants.cs b/cagateway-template/Constants.cs deleted file mode 100644 index af6c50e..0000000 --- a/cagateway-template/Constants.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Keyfactor.Extensions.AnyGateway.Company.Product -{ - public class Constants - { - //Define any constants needed here (mostly field names for config parameters) - } -} \ No newline at end of file diff --git a/cagateway-template/GatewayNameCAConnector.cs b/cagateway-template/GatewayNameCAConnector.cs deleted file mode 100644 index abbe2ab..0000000 --- a/cagateway-template/GatewayNameCAConnector.cs +++ /dev/null @@ -1,193 +0,0 @@ -using CAProxy.AnyGateway; -using CAProxy.AnyGateway.Interfaces; -using CAProxy.AnyGateway.Models; -using CAProxy.AnyGateway.Models.Configuration; -using CAProxy.Common; - -using CSS.PKI; - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -using GatewayNameConstants = Keyfactor.Extensions.AnyGateway.Company.Product.Constants; - -namespace Keyfactor.Extensions.AnyGateway.Company.Product -{ - public class GatewayNameCAConnector : BaseCAConnector, ICAConnectorConfigInfoProvider - { - #region Fields and Constructors - - /// - /// Provides configuration information for the - /// - private ICAConnectorConfigProvider ConfigProvider { get; set; } - - //Define any additional private fields here - - #endregion Fields and Constructors - - #region ICAConnector Methods - - /// - /// Initialize the - /// - /// The config provider contains information required to connect to the CA. - public override void Initialize(ICAConnectorConfigProvider configProvider) - { - ConfigProvider = configProvider; - } - - /// - /// Enrolls for a certificate through the API. - /// - /// Reads certificate data from the database. - /// The certificate request CSR in PEM format. - /// The subject of the certificate request. - /// Any SANs added to the request. - /// Information about the CA product type. - /// The format of the request. - /// The type of the enrollment, i.e. new, renew, or reissue. - /// - public override EnrollmentResult Enroll(ICertificateDataReader certificateDataReader, string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType) - { - throw new NotImplementedException(); - } - - /// - /// Returns a single certificate record by its serial number. - /// - /// The CA request ID for the certificate. - /// - public override CAConnectorCertificate GetSingleRecord(string caRequestID) - { - throw new NotImplementedException(); - } - - /// - /// Attempts to reach the CA over the network. - /// - public override void Ping() - { - throw new NotImplementedException(); - } - - /// - /// Revokes a certificate by its serial number. - /// - /// The CA request ID. - /// The hex-encoded serial number. - /// The revocation reason. - /// - public override int Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) - { - throw new NotImplementedException(); - } - - /// - /// Synchronizes the gateway with the external CA - /// - /// Provides information about the gateway's certificate database. - /// Buffer into which certificates are places from the CA. - /// Information about the last CA sync. - /// The cancellation token. - public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken) - { - throw new NotImplementedException(); - } - - /// - /// Validates that the CA connection info is correct. - /// - /// The information used to connect to the CA. - public override void ValidateCAConnectionInfo(Dictionary connectionInfo) - { - throw new NotImplementedException(); - } - - /// - /// Validates that the product information for the CA is correct - /// - /// The product information. - /// The CA connection information. - public override void ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) - { - throw new NotImplementedException(); - } - - [Obsolete] - public override EnrollmentResult Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, PKIConstants.X509.RequestFormat requestFormat, RequestUtilities.EnrollmentType enrollmentType) - { - throw new NotImplementedException(); - } - - [Obsolete] - public override void Synchronize(ICertificateDataReader certificateDataReader, BlockingCollection blockingBuffer, CertificateAuthoritySyncInfo certificateAuthoritySyncInfo, CancellationToken cancelToken, string logicalName) - { - throw new NotImplementedException(); - } - - #endregion ICAConnector Methods - - #region ICAConnectorConfigInfoProvider Methods - - /// - /// Returns the default CA connector section of the config file. - /// - /// - public Dictionary GetDefaultCAConnectorConfig() - { - return new Dictionary() - { - }; - } - - /// - /// Gets the default comment on the default product type. - /// - /// - public string GetProductIDComment() - { - return ""; - } - - /// - /// Gets annotations for the CA connector properties. - /// - /// - public Dictionary GetCAConnectorAnnotations() - { - return new Dictionary(); - } - - /// - /// Gets annotations for the template mapping parameters - /// - /// - public Dictionary GetTemplateParameterAnnotations() - { - throw new NotImplementedException(); - } - - /// - /// Gets default template map parameters for GlobalSign Atlas product types. - /// - /// - public Dictionary GetDefaultTemplateParametersConfig() - { - throw new NotImplementedException(); - } - - #endregion ICAConnectorConfigInfoProvider Methods - - #region Helper Methods - - // All private helper methods go here - - #endregion Helper Methods - } -} \ No newline at end of file diff --git a/cagateway-template/Properties/AssemblyInfo.cs b/cagateway-template/Properties/AssemblyInfo.cs deleted file mode 100644 index 8b68512..0000000 --- a/cagateway-template/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("cagateway-template")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("cagateway-template")] -[assembly: AssemblyCopyright("Copyright © 2022")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("9d2d6ed9-4626-430c-879d-0fe0febed146")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/cagateway-template/app.config b/cagateway-template/app.config deleted file mode 100644 index ad48466..0000000 --- a/cagateway-template/app.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cagateway-template/cagateway-template.csproj b/cagateway-template/cagateway-template.csproj deleted file mode 100644 index a71a7f5..0000000 --- a/cagateway-template/cagateway-template.csproj +++ /dev/null @@ -1,93 +0,0 @@ - - - - - Debug - AnyCPU - {9D2D6ED9-4626-430C-879D-0FE0FEBED146} - Library - Properties - cagateway_template - cagateway-template - v4.7.2 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\BouncyCastle.1.8.9\lib\BouncyCastle.Crypto.dll - - - ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.AnyGateway.Core.dll - - - ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxy.Interfaces.dll - - - ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CAProxyDAL.dll - - - ..\packages\Common.Logging.3.4.1\lib\net40\Common.Logging.dll - - - ..\packages\Common.Logging.Core.3.4.1\lib\net40\Common.Logging.Core.dll - - - ..\packages\Keyfactor.AnyGateway.SDK.21.3.2\lib\net462\CommonCAProxy.dll - - - ..\packages\CSS.Common.1.7.0\lib\net462\CSS.Common.dll - - - ..\packages\CSS.PKI.2.13.0\lib\net462\CSS.PKI.dll - - - ..\packages\Keyfactor.Logging.1.1.0\lib\netstandard2.0\Keyfactor.Logging.dll - - - ..\packages\Microsoft.Extensions.Logging.Abstractions.5.0.0\lib\net461\Microsoft.Extensions.Logging.Abstractions.dll - - - ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cagateway-template/packages.config b/cagateway-template/packages.config deleted file mode 100644 index 5fd12f1..0000000 --- a/cagateway-template/packages.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docsource/configuration.md b/docsource/configuration.md new file mode 100644 index 0000000..a5bd7a2 --- /dev/null +++ b/docsource/configuration.md @@ -0,0 +1,12 @@ +## Overview + +The Entrust ECS AnyCA Gateway REST plugin extends the capabilities of Entrust Certificate Services to Keyfactor Command via the Keyfactor AnyCA Gateway REST. The plugin represents a fully featured AnyCA REST Plugin with the following capabilies: +* SSL Certificate Synchronization +* SSL Certificate Enrollment +* SSL Certificate Revocation + +## Requirements + +## Gateway Registration + +In order to enroll for certificates the Keyfactor Command server must trust the trust chain. Once you know your Root and/or Subordinate CA in your Entrust account, make sure to download and import the certificate chain into the Command Server certificate store diff --git a/cagateway-template.sln b/entrust-ecs-caplugin.sln similarity index 63% rename from cagateway-template.sln rename to entrust-ecs-caplugin.sln index b953ecb..588be93 100644 --- a/cagateway-template.sln +++ b/entrust-ecs-caplugin.sln @@ -1,27 +1,27 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31729.503 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35004.147 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cagateway-template", "cagateway-template\cagateway-template.csproj", "{9D2D6ED9-4626-430C-879D-0FE0FEBED146}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{431498A1-F30A-4307-9FBF-B1D634326444}" ProjectSection(SolutionItems) = preProject CHANGELOG.md = CHANGELOG.md + docsource\configuration.md = docsource\configuration.md integration-manifest.json = integration-manifest.json - readme_source.md = readme_source.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "entrust-ecs-caplugin", "entrust-ecs-caplugin\entrust-ecs-caplugin.csproj", "{900B0455-FAC3-4743-B742-23447420677A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D2D6ED9-4626-430C-879D-0FE0FEBED146}.Release|Any CPU.Build.0 = Release|Any CPU + {900B0455-FAC3-4743-B742-23447420677A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {900B0455-FAC3-4743-B742-23447420677A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {900B0455-FAC3-4743-B742-23447420677A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {900B0455-FAC3-4743-B742-23447420677A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/entrust-ecs-caplugin/API/Certificate.cs b/entrust-ecs-caplugin/API/Certificate.cs new file mode 100644 index 0000000..3ceb7fc --- /dev/null +++ b/entrust-ecs-caplugin/API/Certificate.cs @@ -0,0 +1,292 @@ +using Keyfactor.Extensions.CAPlugin.Entrust.Models; + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class PatchCertificateRequest : ECSBaseRequest + { + public PatchCertificateRequest(int trackingId) + { + this.Resource = $"certificates/{trackingId.ToString()}"; + this.Method = "PATCH"; + } + } + + public class PatchCertificateRequestBody + { + [JsonProperty("operation")] + public string Operation { get; set; } + + [JsonProperty("declineReason")] + public string DeclineReason { get; set; } + } + + public class GetCertificatesRequest : ECSBaseRequest + { + private int limit, offset; + private Dictionary queryParams; + + public GetCertificatesRequest(int limit, int offset) : this(limit, offset, new Dictionary()) + { + } + + public GetCertificatesRequest(int limit, int offset, Dictionary queryParams) + { + this.limit = limit; + this.offset = offset; + this.Resource = "certificates"; + this.Method = "GET"; + this.queryParams = queryParams; + } + + public new string BuildParameters() + { + StringBuilder sbParamters = new StringBuilder(); + sbParamters.Append("limit=").Append(this.limit.ToString()); + sbParamters.Append("&offset=").Append(this.offset.ToString()); + + foreach (KeyValuePair k in queryParams) + { + sbParamters.Append("&" + k.Key + "=").Append(k.Value); + } + return sbParamters.ToString(); + } + } + + public class GetCertificateByThumbprintRequest : ECSBaseRequest + { + public GetCertificateByThumbprintRequest(string thumbprint) + { + this.Resource = "certificates/thumbprints/" + thumbprint; + this.Method = "GET"; + } + } + + public class GetCertificateByTrackingIdRequest : ECSBaseRequest + { + public GetCertificateByTrackingIdRequest(int trackingId) + { + this.Resource = "certificates/" + trackingId; + this.Method = "GET"; + } + } + + public class GetCertificatesResponse + { + [JsonProperty("summary")] + public Summary summary { get; set; } + + [JsonProperty("certificates")] + public List certificates { get; set; } + } + + public class Certificate + { + /// + /// Gets or Sets Status + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Gets or Sets TrackingId + /// + [JsonProperty("trackingId")] + public int TrackingId { get; set; } + + /// + /// The URI of the certificate. + /// + [JsonProperty("uri")] + public string URI { get; set; } + + /// + /// Distinguished name + /// + /// Distinguished name + [JsonProperty("dn")] + public string Dn { get; set; } + + /// + /// Serial number in hexadecimal format. + /// + /// Serial number in hexadecimal format. + [JsonProperty("serialNumber")] + public string SerialNumber { get; set; } + + /// + /// The date and time, in RFC3339 format, for when the certificate was issued + /// + /// The date and time, in RFC3339 format, for when the certificate was issued + [JsonProperty("issueDateTime")] + public DateTime? IssueDateTime { get; set; } + + /// + /// The date and time, in RFC3339 format, after which the certificate is no longer valid + /// + /// The date and time, in RFC3339 format, after which the certificate is no longer valid + [JsonProperty("expiresAfter")] + public DateTime? ExpiresAfter { get; set; } + + /// + /// Signing algorithm + /// + /// Signing algorithm + [JsonProperty("signingAlg")] + public string SigningAlg { get; set; } + + /// + /// Extended Key Usage - applicable to all public SSL certificate types + /// + /// Extended Key Usage - applicable to all public SSL certificate types + [JsonProperty("eku")] + public string Eku { get; set; } + + /// + /// Key size + /// + /// Key size + [JsonProperty("keySize")] + public int? KeySize { get; set; } + + /// + /// Organization in DN + /// + /// Organization in DN + [JsonProperty("org")] + public string Org { get; set; } + + /// + /// Organizational unit. + /// + /// Organizational unit. + [JsonProperty("ou")] + public List Ou { get; set; } + + /// + /// Certificate type, for example: * STANDARD_SSL * ADVANTAGE_SSL * UC_SSL * EV_SSL * WILDCARD_SSL * PRIVATE_SSL * SMIME_ENT + /// + /// Certificate type, for example: * STANDARD_SSL * ADVANTAGE_SSL * UC_SSL * EV_SSL * WILDCARD_SSL * PRIVATE_SSL * SMIME_ENT + [JsonProperty("certType")] + public string CertType { get; set; } + + /// + /// Domain used + /// + /// Domain used + [JsonProperty("domainUsed")] + public string DomainUsed { get; set; } + + /// + /// Whether this is a third-party certificate + /// + /// Whether this is a third-party certificate + [JsonProperty("isThirdParty")] + public bool? IsThirdParty { get; set; } + + } + + public class Summary + { + /// + /// The date and time instance, in RFC3339 format, for when we received the request. + /// + /// The date and time instance, in RFC3339 format, for when we received the request. + [JsonProperty("timestamp")] + public DateTime? Timestamp { get; set; } + + /// + /// Elapsed time it took to process the request, in milliseconds + /// + /// Elapsed time it took to process the request, in milliseconds + [JsonProperty("elapsed")] + public int? Elapsed { get; set; } + + /// + /// Offset of the result set + /// + /// Offset of the result set + [JsonProperty("offset")] + public int? Offset { get; set; } + + /// + /// Maximum number of items to return + /// + /// Maximum number of items to return + [JsonProperty("limit")] + public int? Limit { get; set; } + + /// + /// Count of total number of items + /// + /// Count of total number of items + [JsonProperty("total")] + public int? Total { get; set; } + + /// + /// Ordering of the results + /// + /// Ordering of the results + [JsonProperty("sort")] + public string Sort { get; set; } + } + + public class SubjectAltName + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } + + + public class CertificateExt : Certificate + { + + public CertificateExt(Certificate c) + { + Status = c.Status; + TrackingId = c.TrackingId; + Dn = c.Dn; + SerialNumber = c.SerialNumber; + IssueDateTime = c.IssueDateTime; + ExpiresAfter = c.ExpiresAfter; + SigningAlg = c.SigningAlg; + Eku = c.Eku; + KeySize = c.KeySize; + Org = c.Org; + Ou = c.Ou; + CertType = c.CertType; + DomainUsed = c.DomainUsed; + IsThirdParty = c.IsThirdParty; + } + + public CertificateExt() { } + + [JsonProperty("subjectAltName")] + public List SubjectAltName { get; set; } + + [JsonProperty("tracking")] + public Tracking Tracking { get; set; } + + [JsonProperty("endEntityCert")] + public string EndEntityCert { get; set; } + + [JsonProperty("csr")] + public string Csr { get; set; } + + [JsonProperty("chainCerts")] + public string[] ChainCerts { get; set; } + + [JsonProperty("creatorName")] + public string CreatorName { get; set; } + + [JsonIgnore] + public string CertificateTemplate { get; set; } + } +} diff --git a/entrust-ecs-caplugin/API/CertificateResponse.cs b/entrust-ecs-caplugin/API/CertificateResponse.cs new file mode 100644 index 0000000..c6fbee8 --- /dev/null +++ b/entrust-ecs-caplugin/API/CertificateResponse.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class CertificateResponse + { + + /// + /// Gets or Sets TrackingId + /// + [JsonProperty("trackingId")] + public int TrackingId { get; set; } + + /// + /// PEM-encoded certificate + /// + /// PEM-encoded certificate + [JsonProperty("endEntityCert")] + public string EndEntityCert { get; set; } + + /// + /// Gets or Sets ChainCerts + /// + [JsonProperty("chainCerts")] + public List ChainCerts { get; set; } + + /// + /// Serial number in hexadecimal format + /// + /// Serial number in hexadecimal format + [JsonProperty("serialNumber")] + public string SerialNumber { get; set; } + + /// + /// The date and time, in RFC3339 format, after which the certificate is no longer valid. + /// + /// The date and time, in RFC3339 format, after which the certificate is no longer valid. + [JsonProperty("expiresAfter")] + public DateTime? ExpiresAfter { get; set; } + + /// + /// Gets or Sets PickupUrl + /// + [JsonProperty("pickupUrl")] + public string PickupUrl { get; set; } + + /// + /// S/MIME certificate and private key in PKCS12 format protected by the provided password. Only returned for SMIME_ENT certtype and only if no CSR is supplied. + /// + /// S/MIME certificate and private key in PKCS12 format protected by the provided password. Only returned for SMIME_ENT certtype and only if no CSR is supplied. + [JsonProperty("pkcs12")] + public string Pkcs12 { get; set; } + + + } +} diff --git a/entrust-ecs-caplugin/API/Client.cs b/entrust-ecs-caplugin/API/Client.cs new file mode 100644 index 0000000..1f2e0e0 --- /dev/null +++ b/entrust-ecs-caplugin/API/Client.cs @@ -0,0 +1,70 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class GetClientsRequest : ECSBaseRequest + { + public GetClientsRequest() + { + this.Resource = "clients"; + this.Method = "GET"; + } + } + + public class GetClientsResponse + { + [JsonProperty("clients")] + public List Clients { get; set; } + } + + public class ClientInfo + { + /// + /// Gets or Sets VerificationStatus + /// + [JsonProperty("evVerificationStatus")] + public string EVVerificationStatus { get; set; } + + /// + /// Gets or Sets VerificationStatus + /// + [JsonProperty("verificationStatus")] + public string VerificationStatus { get; set; } + + /// + /// Client ID of client. For the primary client, this is 1. + /// + /// Client ID of client. For the primary client, this is 1. + [JsonProperty("clientId")] + public int ClientId { get; set; } + + /// + /// The company name of the client + /// + /// The company name of the client + [JsonProperty("clientName")] + public string ClientName { get; set; } + + /// + /// Gets or Sets FriendlyClientName + /// + [JsonProperty("friendlyClientName")] + public string FriendlyClientName { get; set; } + + /// + /// OV information expiry date - - only present if client has been APPROVED + /// + /// OV information expiry date - - only present if client has been APPROVED + [JsonProperty("ovExpiryDate")] + public DateTime? OvExpiryDate { get; set; } + + [JsonProperty("evExpiryDate")] + public DateTime? EvExpiryDate { get; set; } + } +} diff --git a/entrust-ecs-caplugin/API/ECSAPIBase.cs b/entrust-ecs-caplugin/API/ECSAPIBase.cs new file mode 100644 index 0000000..23f9cd0 --- /dev/null +++ b/entrust-ecs-caplugin/API/ECSAPIBase.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public abstract class ECSBaseRequest + { + [JsonIgnore] + public string Resource { get; internal set; } + + [JsonIgnore] + public string Method { get; internal set; } + + [JsonIgnore] + public string TargetURI { get; set; } + + public string BuildParameters() + { + return ""; + } + } + + public abstract class ECSBaseResponse + { + public enum StatusType + { + SUCCESS, + ERROR, + WARNING + } + + public enum ContentTypes + { + XML, + JSON, + TEXT + } + + [JsonIgnore] + public ContentTypes ContentType { get; internal set; } + + [JsonIgnore] + public StatusType Status { get; set; } + + [JsonIgnore] + public List Errors { get; set; } + + public ECSBaseResponse() + { + Errors = new List(); + Status = StatusType.SUCCESS; + ContentType = ContentTypes.JSON; + } + } +} diff --git a/entrust-ecs-caplugin/API/Error.cs b/entrust-ecs-caplugin/API/Error.cs new file mode 100644 index 0000000..da037cd --- /dev/null +++ b/entrust-ecs-caplugin/API/Error.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class Error + { + [JsonProperty("message")] + public string Message { get; set; } + } + + public class ErrorResponse + { + [JsonProperty("errors")] + public List Errors { get; set; } + + [JsonProperty("status")] + public int Status { get; set; } + } +} diff --git a/entrust-ecs-caplugin/API/Inventory.cs b/entrust-ecs-caplugin/API/Inventory.cs new file mode 100644 index 0000000..5ce6b35 --- /dev/null +++ b/entrust-ecs-caplugin/API/Inventory.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class GetInventoryRequest : ECSBaseRequest + { + public GetInventoryRequest() + { + this.Resource = "inventories"; + this.Method = "GET"; + } + } + + public class InventoryItem + { + + /// + /// Gets or Sets ProductType + /// + [JsonProperty("productType")] + public string ProductType { get; set; } + + /// + /// Total inventory for this product type ever added to the account + /// + /// Total inventory for this product type ever added to the account + [JsonProperty("totalCount")] + public int? TotalCount { get; set; } + + /// + /// Inventory for this product type that has not been used, and has not expired + /// + /// Inventory for this product type that has not been used, and has not expired + [JsonProperty("remainingCount")] + public int? RemainingCount { get; set; } + + /// + /// Count of consumed inventory for this product type. This count does not include expired inventory + /// + /// Count of consumed inventory for this product type. This count does not include expired inventory + [JsonProperty("usedCount")] + public int? UsedCount { get; set; } + } + + public class GetInventoryResponse + { + [JsonProperty("inventories")] + public List Inventories { get; set; } + } +} diff --git a/entrust-ecs-caplugin/API/NewCertificateRequest.cs b/entrust-ecs-caplugin/API/NewCertificateRequest.cs new file mode 100644 index 0000000..d94970e --- /dev/null +++ b/entrust-ecs-caplugin/API/NewCertificateRequest.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class NewCertificateCall : ECSBaseRequest + { + public NewCertificateCall() + { + Resource = "enterprise/v2/certificates"; + Method = "POST"; + } + } + public partial class NewCertificateRequest : RenewCertificateRequestBody + { + /// + /// Certificate type to request Supported types are: * STANDARD_SSL * ADVANTAGE_SSL * UC_SSL * EV_SSL * QWAC_SSL * QWACPSD2_SSL * WILDCARD_SSL * PRIVATE_SSL * PD_SSL * CODE_SIGNING * EV_CODE_SIGNING * CDS_INDIVIDUAL * CDS_GROUP * CDS_ENT_LITE * CDS_ENT_PRO * SMIME_ENT + /// + /// Certificate type to request Supported types are: * STANDARD_SSL * ADVANTAGE_SSL * UC_SSL * EV_SSL * QWAC_SSL * QWACPSD2_SSL * WILDCARD_SSL * PRIVATE_SSL * PD_SSL * CODE_SIGNING * EV_CODE_SIGNING * CDS_INDIVIDUAL * CDS_GROUP * CDS_ENT_LITE * CDS_ENT_PRO * SMIME_ENT + [JsonProperty("certType")] + public string CertType { get; set; } + + /// + /// For all SSL and Document Signing certificate types&#58; * If set to true, certificate request is queued for approval by an administrator. * If set to false (default), the certificate is generated immediately. + /// + /// For all SSL and Document Signing certificate types&#58; * If set to true, certificate request is queued for approval by an administrator. * If set to false (default), the certificate is generated immediately. + [JsonProperty("queueForApproval")] + public bool? QueueForApproval { get; set; } + + } +} diff --git a/entrust-ecs-caplugin/API/Organization.cs b/entrust-ecs-caplugin/API/Organization.cs new file mode 100644 index 0000000..c98d221 --- /dev/null +++ b/entrust-ecs-caplugin/API/Organization.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; + +using Org.BouncyCastle.Asn1.Cmp; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class GetOrganizationsRequest : ECSBaseRequest + { + /// + /// Default constructor. + /// + public GetOrganizationsRequest() + { + Resource = "organizations"; + Method = "GET"; + } + } + + public class GetOrganizationsResponse : ECSBaseResponse + { + /// + /// The collection of organizations returned by Entrust. + /// + [JsonProperty("organizations")] + public List Organizations { get; set; } + } + + public class Organization + { + /// + /// The organization's name. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The status of the organization. + /// + [JsonProperty("verificationStatus")] + public string VerificationStatus { get; set; } + + /// + /// The ID of the client associated with this organization. + /// + [JsonProperty("clientId")] + public int ClientId { get; set; } + } +} diff --git a/entrust-ecs-caplugin/API/ReissueCertificateRequest.cs b/entrust-ecs-caplugin/API/ReissueCertificateRequest.cs new file mode 100644 index 0000000..07bc172 --- /dev/null +++ b/entrust-ecs-caplugin/API/ReissueCertificateRequest.cs @@ -0,0 +1,128 @@ +using Keyfactor.Extensions.CAPlugin.Entrust.Models; + +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class ReissueCertificateRequest : ECSBaseRequest + { + + public ReissueCertificateRequest(int trackingId) + { + this.Resource = $"certificates/{trackingId.ToString()}/reissues"; + this.Method = "POST"; + + } + } + public class ReissueCertificateRequestBody + { + + /// + /// Signing algorithm of certificate (SHA-1 or SHA-2). The account default is used if not specified. - This parameter is only applicable when the account preference is set to \"Select signing algorithm at certificate generation time.\" - As of January 1, 2016 any certificates except Private SSL (PRIVATE_SSL) certificates being issued, reissued, or renewed will use SHA2, even if the SHA1 algorithm is specified in the request. Private SSL certificates can continue to use SHA-1. + /// + /// Signing algorithm of certificate (SHA-1 or SHA-2). The account default is used if not specified. - This parameter is only applicable when the account preference is set to \"Select signing algorithm at certificate generation time.\" - As of January 1, 2016 any certificates except Private SSL (PRIVATE_SSL) certificates being issued, reissued, or renewed will use SHA2, even if the SHA1 algorithm is specified in the request. Private SSL certificates can continue to use SHA-1. + [JsonProperty("signingAlg")] + public string SigningAlg { get; set; } + + + /// + /// Extended Key Usage - applicable to all public SSL certificate types + /// + /// Extended Key Usage - applicable to all public SSL certificate types (SERVER_AUTH, CLIENT_AUTH, SERVER_AND_CLIENT_AUTH) + [JsonProperty("eku")] + public string Eku { get; set; } + + /// + /// Base-64 encoded Certificate Signing Request (CSR). CSR is accepted with or without PEM formatting around the Base-64 string. + /// + /// Base-64 encoded Certificate Signing Request (CSR). CSR is accepted with or without PEM formatting around the Base-64 string. + [JsonProperty("csr")] + public string Csr { get; set; } + + /// + /// The subjectAltName identifier, as an array of values (applies to STANDARD_SSL, ADVANTAGE_SSL, UC_SSL, EV_SSL, QWAC_SSL, QWACPSD2_SSL, WILDCARD_SSL, PRIVATE_SSL, and PD_SSL certificate types). * If you are requesting a new SSL certificate, and you pass a subjectAltName parameter, any SAN names in the CSR are ignored. If no subjectAltName parameter is passed, the SAN names in the CSR are used. * See the requesttype parameter (further in this table) to understand more about SANs during reissues and renewals. * In the case of Standard certificates, if the CN of the certificate is <domain>.<tld> only the www.<domain>.<tld> value is accepted. If the CN of the certificate is www.<domain>.<tld> only the <domain>.<tld> value is accepted. + /// + /// The subjectAltName identifier, as an array of values (applies to STANDARD_SSL, ADVANTAGE_SSL, UC_SSL, EV_SSL, QWAC_SSL, QWACPSD2_SSL, WILDCARD_SSL, PRIVATE_SSL, and PD_SSL certificate types). * If you are requesting a new SSL certificate, and you pass a subjectAltName parameter, any SAN names in the CSR are ignored. If no subjectAltName parameter is passed, the SAN names in the CSR are used. * See the requesttype parameter (further in this table) to understand more about SANs during reissues and renewals. * In the case of Standard certificates, if the CN of the certificate is <domain>.<tld> only the www.<domain>.<tld> value is accepted. If the CN of the certificate is www.<domain>.<tld> only the <domain>.<tld> value is accepted. + [JsonProperty("subjectAltName")] + public List SubjectAltName { get; set; } + + + + /// + /// In compliance with browser requirements, this certificate may be posted to the Certificate Transparency (CT) logs. This is a best practice technique that helps domain owners monitor certificates issued to their domains. Note that not all certificates are eligible for CT logging. * If ctLog is not specified, the certificate uses the account default. * If ctLog is specified and the account settings allow it, ctLog overrides the account default. * If ctLog is set to *false*, but the account settings is set to \"always log\", the certificate generation will fail. + /// + /// In compliance with browser requirements, this certificate may be posted to the Certificate Transparency (CT) logs. This is a best practice technique that helps domain owners monitor certificates issued to their domains. Note that not all certificates are eligible for CT logging. * If ctLog is not specified, the certificate uses the account default. * If ctLog is specified and the account settings allow it, ctLog overrides the account default. * If ctLog is set to *false*, but the account settings is set to \"always log\", the certificate generation will fail. + [JsonProperty("ctLog")] + public bool? CtLog { get; set; } + + /// + /// Common Name (CN) attribute in the DN. Applicable to S/MIME and Document Signing only. + /// + /// Common Name (CN) attribute in the DN. Applicable to S/MIME and Document Signing only. + [JsonProperty("cn")] + public string Cn { get; set; } + + /// + /// email attribute in the DN. Applicable to S/MIME and Document Signing only. + /// + /// email attribute in the DN. Applicable to S/MIME and Document Signing only. + [JsonProperty("certEmail")] + public string CertEmail { get; set; } + + /// + /// User Principal Name. Applicable to the SMIME_ENT certificate types only. If specified, it must be a valid email address and its domain must be the approved domain for that client. + /// + /// User Principal Name. Applicable to the SMIME_ENT certificate types only. If specified, it must be a valid email address and its domain must be the approved domain for that client. + [JsonProperty("upn")] + public string Upn { get; set; } + + /// + /// The client ID. The ID of the primary client is 1. If the clientId is not specified, 1 is used. + /// + /// The client ID. The ID of the primary client is 1. If the clientId is not specified, 1 is used. + [JsonProperty("clientId")] + public int? ClientId { get; set; } + + /// + /// When there is an org parameter specified in the request, it is used in the certificate created, even if there is already an Org in the CSR (O=). When there is no org parameter specified in the request, the organization from the Client is used when creating all certificate types except for Private Dedicated SSL (PDSSL). In the case of PDSSL certificates only: if there is no org parameter in the request, the organization in the CSR is used, when it is available. If there is no org value in the CSR, then the client organization is used. Note that the org parameter is valid for use only with clientId=1, for all certificate types except for PDSSL. When requesting PDSSL certificates, the org parameter can be used in requests for any clientId. + /// + /// When there is an org parameter specified in the request, it is used in the certificate created, even if there is already an Org in the CSR (O=). When there is no org parameter specified in the request, the organization from the Client is used when creating all certificate types except for Private Dedicated SSL (PDSSL). In the case of PDSSL certificates only: if there is no org parameter in the request, the organization in the CSR is used, when it is available. If there is no org value in the CSR, then the client organization is used. Note that the org parameter is valid for use only with clientId=1, for all certificate types except for PDSSL. When requesting PDSSL certificates, the org parameter can be used in requests for any clientId. + [JsonProperty("org")] + public string Org { get; set; } + + /// + /// The organizational unit. This parameter can be set to the name of the 'ou' or '' (i.e. ignore CSR ou and do not set the OU). See the behavior below. This parameter is valid for SSL and S/MIME certificate types. 'ou' behavior is dependent on whether organizational units are enabled for your account. If ou is disabled for your account: * New certificates- OUs from CSRs or the 'ou' input parameters are ignored. * Reissued certificates- OUs from CSRs, or the 'ou' input parameters are ignored. * Renewed certificates- OUs from CSRs, or the 'ou' input parameters are ignored. If OUs are enabled for your account: * New certificates- Valid OUs from CSRs are used. Invalid OUs from CSRs are ignored. The OU in the CSR is overridden by a valid \"ou\" from the input parameter, however if the OU is invalid, an \"Unapproved OU\" error is generated. * Reissued certificates- If the CSR is not specified when reissuing, then the OU from the CSR of the original certificate is used as the default OU. The OU is ignored if it is invalid. If a new CSR is used when the certificate is reissued, the OU from the CSR is used as the default OU. If a new CSR with no OU is used, the certificate is reissued without an OU. The original OU in the CSRis overridden by a valid 'ou' or '' from the input parameter, however if the OU is invalid, an \"Unapproved OU\" error is generated. * Renewed certificates- If no CSR is specified when the certificate is renewed, the OU of the CSR from the original certificate is used. The OU is ignored if it is invalid. If a new CSR is used and contains a valid OU, the OU from the CSR is used. If the CSR is replaced and contains no OU, the certificate is renewed without an OU. The original OU in the certificate is overridden by a valid 'ou' or '' (i.e. no OU) from the input parameter, or by the OU in a replacement CSR, however if the OU is invalid an \"Unapproved OU\" error is generated. Multiple OUs are reserved for future products. A maximum of one OU may be specified for current products. + /// + /// The organizational unit. This parameter can be set to the name of the 'ou' or '' (i.e. ignore CSR ou and do not set the OU). See the behavior below. This parameter is valid for SSL and S/MIME certificate types. 'ou' behavior is dependent on whether organizational units are enabled for your account. If ou is disabled for your account: * New certificates- OUs from CSRs or the 'ou' input parameters are ignored. * Reissued certificates- OUs from CSRs, or the 'ou' input parameters are ignored. * Renewed certificates- OUs from CSRs, or the 'ou' input parameters are ignored. If OUs are enabled for your account: * New certificates- Valid OUs from CSRs are used. Invalid OUs from CSRs are ignored. The OU in the CSR is overridden by a valid \"ou\" from the input parameter, however if the OU is invalid, an \"Unapproved OU\" error is generated. * Reissued certificates- If the CSR is not specified when reissuing, then the OU from the CSR of the original certificate is used as the default OU. The OU is ignored if it is invalid. If a new CSR is used when the certificate is reissued, the OU from the CSR is used as the default OU. If a new CSR with no OU is used, the certificate is reissued without an OU. The original OU in the CSRis overridden by a valid 'ou' or '' from the input parameter, however if the OU is invalid, an \"Unapproved OU\" error is generated. * Renewed certificates- If no CSR is specified when the certificate is renewed, the OU of the CSR from the original certificate is used. The OU is ignored if it is invalid. If a new CSR is used and contains a valid OU, the OU from the CSR is used. If the CSR is replaced and contains no OU, the certificate is renewed without an OU. The original OU in the certificate is overridden by a valid 'ou' or '' (i.e. no OU) from the input parameter, or by the OU in a replacement CSR, however if the OU is invalid an \"Unapproved OU\" error is generated. Multiple OUs are reserved for future products. A maximum of one OU may be specified for current products. + [JsonProperty("ou")] + public List Ou { get; set; } + + /// + /// The certificate pickup password. * A password must be used if it is set to \"required\" in the account under Options > Certificate Pickup Password. A password is used to protect SMIME_ENT certificates without CSRs. The password protects the returned PKCS12 containing the private key and certificate. * If a password and CSR are provided in an SMIME_ENT certificate request, the CSR will be used, and the password will be ignored. + /// + /// The certificate pickup password. * A password must be used if it is set to \"required\" in the account under Options > Certificate Pickup Password. A password is used to protect SMIME_ENT certificates without CSRs. The password protects the returned PKCS12 containing the private key and certificate. * If a password and CSR are provided in an SMIME_ENT certificate request, the CSR will be used, and the password will be ignored. + [JsonProperty("password")] + public string Password { get; set; } + + /// + /// Gets or Sets Tracking + /// + [JsonProperty("tracking")] + public Tracking Tracking { get; set; } + + /// + /// The end user of the Code Signing certificate must generate and store the private key for this request on cryptographically secure hardware to be compliant with the Entrust CSP and Subscription agreement. You must set the endUserKeyStorageAgreement flag to true, to acknowledge that you will inform the user of this requirement. Applicable to Code Signing certificate types only. + /// + /// The end user of the Code Signing certificate must generate and store the private key for this request on cryptographically secure hardware to be compliant with the Entrust CSP and Subscription agreement. You must set the endUserKeyStorageAgreement flag to true, to acknowledge that you will inform the user of this requirement. Applicable to Code Signing certificate types only. + [JsonProperty("endUserKeyStorageAgreement")] + public bool? EndUserKeyStorageAgreement { get; set; } + + + } +} diff --git a/entrust-ecs-caplugin/API/RenewCertificateRequest.cs b/entrust-ecs-caplugin/API/RenewCertificateRequest.cs new file mode 100644 index 0000000..da1291a --- /dev/null +++ b/entrust-ecs-caplugin/API/RenewCertificateRequest.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class RenewCertificateRequest : ECSBaseRequest + { + public RenewCertificateRequest(int trackingId) + { + this.Resource = $"certificates/{trackingId}/renewals"; + this.Method = "POST"; + } + } + public class RenewCertificateRequestBody : ReissueCertificateRequestBody + { + /// + /// If the validateOnly flag is set to true, the request contents will be validated for correctness but will not otherwise be processed. No inventory will be consumed and no certificate will be generated. + /// + /// If the validateOnly flag is set to true, the request contents will be validated for correctness but will not otherwise be processed. No inventory will be consumed and no certificate will be generated. + [JsonProperty("validateOnly")] + public bool? ValidateOnly { get; set; } + + /// + /// The date the certificate is set to expire (pooling accounts only). An RFC3339 compliant date, for example&#58; YYYY-MM-DD Note that only the date (day, month, year) is supported for specifying expiry date. If you choose to specify an expiry time with the expiry date, the time will be adjusted to Eastern Standard Time (EST). This could have the unintended effect of moving your expiry date to the previous day. + /// + /// The date the certificate is set to expire (pooling accounts only). An RFC3339 compliant date, for example&#58; YYYY-MM-DD Note that only the date (day, month, year) is supported for specifying expiry date. If you choose to specify an expiry time with the expiry date, the time will be adjusted to Eastern Standard Time (EST). This could have the unintended effect of moving your expiry date to the previous day. + [JsonProperty("certExpiryDate")] + public DateTime? CertExpiryDate { get; set; } + + /// + /// The lifetime of the certificate. Applies to all non-pooling accounts and to CDS_INDIVIDUAL, CDS_GROUP, CDS_ENT_LITE, CDS_ENT_PRO, and SMIME_ENT certificates, regardless of account type. This value is specified as an ISO 8601 duration. Allowed values are: 'P1Y', 'P2Y', and 'P3Y'. + /// + /// The lifetime of the certificate. Applies to all non-pooling accounts and to CDS_INDIVIDUAL, CDS_GROUP, CDS_ENT_LITE, CDS_ENT_PRO, and SMIME_ENT certificates, regardless of account type. This value is specified as an ISO 8601 duration. Allowed values are: 'P1Y', 'P2Y', and 'P3Y'. + [JsonProperty("certLifetime")] + public string CertLifetime { get; set; } + } +} diff --git a/entrust-ecs-caplugin/API/RevokeCertificateRequest.cs b/entrust-ecs-caplugin/API/RevokeCertificateRequest.cs new file mode 100644 index 0000000..98ba22f --- /dev/null +++ b/entrust-ecs-caplugin/API/RevokeCertificateRequest.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public partial class RevokeCertificateRequest + { + /// + /// Gets or Sets CrlReason + /// + [JsonProperty("crlReason")] + public string CrlReason { get; set; } + + /// + /// Comment field to explain the reason for revocation + /// + /// Comment field to explain the reason for revocation + [JsonProperty("revocationComment")] + public string RevocationComment { get; set; } + + } + + public class RevokeCertificateCall : ECSBaseRequest + { + + + public RevokeCertificateCall(int trackingId) + { + this.Resource = "certificates/" + trackingId.ToString() + "/revocations"; + this.Method = "POST"; + } + } +} diff --git a/entrust-ecs-caplugin/API/Version.cs b/entrust-ecs-caplugin/API/Version.cs new file mode 100644 index 0000000..58cb966 --- /dev/null +++ b/entrust-ecs-caplugin/API/Version.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.API +{ + public class VersionRequest : ECSBaseRequest + { + public VersionRequest() + { + this.Resource = "application/version"; + this.Method = "GET"; + } + + public class VersionResponse + { + /// + /// Gets or Sets Status + /// + [JsonProperty("version")] + public string Version { get; set; } + } + } +} diff --git a/entrust-ecs-caplugin/Client/ECSClient.cs b/entrust-ecs-caplugin/Client/ECSClient.cs new file mode 100644 index 0000000..e97f28a --- /dev/null +++ b/entrust-ecs-caplugin/Client/ECSClient.cs @@ -0,0 +1,528 @@ +using Keyfactor.Extensions.CAPlugin.Entrust.API; +using Keyfactor.Extensions.CAPlugin.Entrust.Models; +using Keyfactor.Logging; + +using Microsoft.Extensions.Logging; + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +using static Keyfactor.Extensions.CAPlugin.Entrust.API.VersionRequest; + +using Certificate = Keyfactor.Extensions.CAPlugin.Entrust.API.Certificate; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.Client +{ + public class ECSClient + { + private static ILogger Logger => LogHandler.GetClassLogger(); + + private string UserName { get; set; } + private string Password { get; set; } + private string BaseUrl { get; set; } + private X509Certificate2 AuthCert { get; set; } + + public ECSClient(string username, string password, X509Certificate2 authCert, string baseUrl) + { + UserName = username; + Password = password; + AuthCert = authCert; + BaseUrl = baseUrl; + } + + public ECSClient(string username, string password, X509Certificate2 authCert) + : this(username, password, authCert, "https://api.entrust.net/enterprise/v2/") + { + } + + private class ECSResponse + { + public ECSResponse() + { + Success = true; + Response = ""; + } + + public bool Success { get; set; } + public string Response { get; set; } + } + + private ECSResponse Request(ECSBaseRequest request) + { + return Request(request, ""); + } + + private ECSResponse Request(ECSBaseRequest request, string parameters) + { + ECSResponse response = new ECSResponse(); + bool rateLimited = true; + int retryAfter = 0; + + while (rateLimited) + { + System.Threading.Thread.Sleep(retryAfter * 1000); + try + { + string targetUri; + if (request.Method == "POST" || request.Method == "PUT" || request.Method == "PATCH") + { + targetUri = BaseUrl + request.Resource; + } + else + { + if (string.IsNullOrEmpty(parameters)) + { + targetUri = BaseUrl + request.Resource; + } + else + { + targetUri = BaseUrl + request.Resource + "?" + parameters; + } + } + Logger.LogTrace($"Entered Entrust request method: {request.Method} - URL: {targetUri}"); + + HttpWebRequest objRequest = (HttpWebRequest)WebRequest.Create(targetUri); + objRequest.Method = request.Method; + objRequest.ContentType = "application/json"; + objRequest.Headers["Authorization"] = "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes(UserName + ":" + Password)); + if (AuthCert != null) + { + objRequest.ClientCertificates.Add(AuthCert); + } + + + if (!String.IsNullOrEmpty(parameters) && (objRequest.Method == "POST" || objRequest.Method == "PUT" || objRequest.Method == "PATCH")) + { + byte[] postBytes = Encoding.UTF8.GetBytes(parameters); + objRequest.ContentLength = postBytes.Length; + Stream requestStream = objRequest.GetRequestStream(); + requestStream.Write(postBytes, 0, postBytes.Length); + requestStream.Close(); + } + + Stopwatch watch = new Stopwatch(); + watch.Start(); + + using (HttpWebResponse objResponse = (HttpWebResponse)objRequest.GetResponse()) + { + response.Response = new StreamReader(objResponse.GetResponseStream()).ReadToEnd(); + + Logger.LogTrace($"Entrust API returned response {objResponse.StatusCode} ({response.Response.Length} characters) in {watch.ElapsedMilliseconds}ms"); + } + + Logger.LogTrace("Full Response Body: " + response.Response); + rateLimited = false; + } + catch (WebException wex) + { + if (wex.Response != null) + { + using (HttpWebResponse errorResponse = (HttpWebResponse)wex.Response) + { + using (StreamReader reader = new StreamReader(errorResponse.GetResponseStream())) + { + response.Response = reader.ReadToEnd(); + string retrySeconds = errorResponse.Headers["Retry-After"]; + + if (!Int32.TryParse(retrySeconds, out retryAfter)) + { + rateLimited = false; + } + else + { + retryAfter += 1; // Add one second to ensure we're not losing a decimal place. + Logger.LogTrace("Rate Limit exceeded. Resubmitting request after {0} seconds.", retryAfter); + } + } + } + } + else + { + Logger.LogError($"Entrust Response Error: {wex.Message}"); + throw new Exception($"Unable to establish connection to Entrust web service: {wex.Message}", wex); + } + } + catch (Exception ex) + { + Logger.LogError($"Entrust Response Error: {ex.Message}"); + throw new Exception($"Unable to establish connection to Entrust web service: {ex.Message}", ex); + } + } + + return response; + } + + private bool IsError(string response) + { + return response.Contains("errors"); + } + + public VersionResponse GetApplicationVersion() + { + VersionRequest oRequest = new VersionRequest(); + ECSResponse oResponse = Request(oRequest, oRequest.BuildParameters()); + VersionResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred requesting application version from the Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response; + } + + public List GetOrganizations() + { + GetOrganizationsRequest request = new GetOrganizationsRequest(); + ECSResponse apiResponse = Request(request, string.Empty); + + if (IsError(apiResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(apiResponse.Response); + + Logger.LogError($"Error occurred requesting organizations from the Entrust REST API: {e.Errors.First().Message}"); + + throw new Exception(e.Errors.First().Message); + } + else + { + GetOrganizationsResponse response = JsonConvert.DeserializeObject(apiResponse.Response); + return response.Organizations; + } + } + + public List GetClients() + { + GetClientsRequest oRequest = new GetClientsRequest(); + ECSResponse oResponse = Request(oRequest, oRequest.BuildParameters()); + GetClientsResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + + Logger.LogError($"Error occurred requesting client list from the Entrust REST API - {e.Errors.First().Message}"); + + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response.Clients; + } + + public List GetAllCertificates() + { + List result = new List(); + int limit = 1000; + int received = 0; + int? total = 0; + bool requestStarted = false; + + while (!requestStarted || received != total) + { + GetCertificatesRequest oRequest = new GetCertificatesRequest(limit, received); + ECSResponse oResponse = Request(oRequest, oRequest.BuildParameters()); + GetCertificatesResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred requesting certificate list from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + total = response.summary.Total; + received += response.certificates.Count; + result.AddRange(response.certificates); + } + requestStarted = true; + } + + return result; + } + + public CertificateExt GetCertificateByTrackingId(int trackingId) + { + GetCertificateByTrackingIdRequest oRequest = new GetCertificateByTrackingIdRequest(trackingId); + ECSResponse oResponse = Request(oRequest, oRequest.BuildParameters()); + CertificateExt response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred requesting certificate for trackingId {trackingId} from the Entrust REST API - Error status code {e.Status} : {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response; + } + + public CertificateExt GetCertificateByThumbprint(string thumbprint) + { + GetCertificateByThumbprintRequest oRequest = new GetCertificateByThumbprintRequest(thumbprint); + ECSResponse oResponse = Request(oRequest, oRequest.BuildParameters()); + CertificateExt response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError("Error occurred requesting certificate for thumbprint {0} from the Entrust REST API - Error status code {1} : {2}", thumbprint, e.Status, e.Errors.First().Message); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response; + } + + public CertificateResponse RequestNewCertificate(NewCertificateRequest request) + { + NewCertificateCall call = new NewCertificateCall(); + ECSResponse oResponse = Request(call, JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + CertificateResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred requesting new certificate from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response; + } + + public CertificateResponse ReissueCertificate(ReissueCertificateRequestBody request, int trackingId) + { + ECSResponse oResponse = Request(new ReissueCertificateRequest(trackingId), JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + CertificateResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred reissuing certificate with trackingId {trackingId} from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response; + } + + public CertificateResponse RenewCertificate(RenewCertificateRequestBody request, int trackingId) + { + ECSResponse oResponse = Request(new RenewCertificateRequest(trackingId), JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + CertificateResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred renewing certificate with trackingId {trackingId} from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response; + } + + public ValueTuple ValidateRequestNewCertificate(NewCertificateRequest request) + { + // We switch the value here so that callers don't have to create a new request. + bool? originalValue = request.ValidateOnly; + request.ValidateOnly = true; + ECSResponse oResponse = Request(new NewCertificateCall(), JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + request.ValidateOnly = originalValue; + + if (IsError(oResponse.Response)) + { + ErrorResponse response = JsonConvert.DeserializeObject(oResponse.Response); + Error requestError = response.Errors[0]; + return (false, requestError.Message); + } + return (true, oResponse.Response); + } + + public Certificate GetCertificateBySerialNumber(string serialNumber) + { + string trimmedSerialNumber = serialNumber.TrimStart('0'); + List result = new List(); + + Dictionary qParams = new Dictionary(); + qParams.Add("serialNumber", trimmedSerialNumber); + GetCertificatesRequest oRequest = new GetCertificatesRequest(1, 0, qParams); + ECSResponse oResponse = Request(oRequest, oRequest.BuildParameters()); + GetCertificatesResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + if (e.Status == 404) + { + return null; + } + Logger.LogError($"Error occurred requesting certificate with serial number {trimmedSerialNumber} from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + if (response.certificates.Count > 0) + { + return response.certificates[0]; + } + else + { + return null; + } + } + + public void RevokeCertificate(int trackingId, string reason, string comment) + { + RevokeCertificateCall oRequest = new RevokeCertificateCall(trackingId); + RevokeCertificateRequest request = new RevokeCertificateRequest() + { + CrlReason = reason, + RevocationComment = comment + }; + + ECSResponse oResponse = Request(oRequest, JsonConvert.SerializeObject(request, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred revoking certificate with trackingId {trackingId} from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + } + + public List GetInventories() + { + GetInventoryRequest oRequest = new GetInventoryRequest(); + ECSResponse oResponse = Request(oRequest, oRequest.BuildParameters()); + GetInventoryResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred requesting inventory from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response.Inventories; + } + + public CertificateResponse ApproveCertificate(int trackingId) + { + PatchCertificateRequest oRequest = new PatchCertificateRequest(trackingId); + PatchCertificateRequestBody body = new PatchCertificateRequestBody() + { + Operation = CertificateOperation.APPROVE + }; + ECSResponse oResponse = Request(oRequest, JsonConvert.SerializeObject(body, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + CertificateResponse response; + + if (IsError(oResponse.Response)) + { + ErrorResponse e = JsonConvert.DeserializeObject(oResponse.Response); + Logger.LogError($"Error occurred approving certificate with trackingId {trackingId} from Entrust REST API - {e.Errors.First().Message}"); + throw new Exception(e.Errors.First().Message); + } + else + { + response = JsonConvert.DeserializeObject(oResponse.Response); + } + + return response; + } + + public static ECSClient InitializeClient(ECSConfig config) + { + Logger.MethodEntry(LogLevel.Debug); + X509Certificate2 clientCert = null; + if (!string.IsNullOrEmpty(config.ClientCertificate.Thumbprint)) + { + //Cert auth, cert in Windows store + StoreName sn; + StoreLocation sl; + string thumbprint = config.ClientCertificate.Thumbprint; + + if (string.IsNullOrEmpty(thumbprint) || + !Enum.TryParse(config.ClientCertificate.StoreName, out sn) || + !Enum.TryParse(config.ClientCertificate.StoreLocation, out sl)) + { + throw new Exception("Unable to find client authentication certificate"); + } + + X509Certificate2Collection foundCerts; + using (X509Store currentStore = new X509Store(sn, sl)) + { + Logger.LogTrace($"Search for client auth certificates with Thumprint {thumbprint} in the {sn}{sl} certificate store"); + + currentStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + foundCerts = currentStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true); + Logger.LogTrace($"Found {foundCerts.Count} certificates in the {currentStore.Name} store"); + currentStore.Close(); + } + if (foundCerts.Count > 1) + { + throw new Exception($"Multiple certificates with Thumprint {thumbprint} found in the {sn}{sl} certificate store"); + } + if (foundCerts.Count > 0) + clientCert = foundCerts[0]; + } + else if (!string.IsNullOrEmpty(config.ClientCertificate.CertificatePath)) + { + //Cert auth, cert in pfx file + try + { + X509Certificate2 cert = new X509Certificate2(config.ClientCertificate.CertificatePath, config.ClientCertificate.CertificatePassword); + clientCert = cert; + } + catch (Exception ex) + { + throw new Exception($"Unable to open the client certificate file with the given password. Error: {ex.Message}"); + } + } + + return new ECSClient(config.AuthUsername, config.AuthPassword, clientCert); + } + } +} diff --git a/entrust-ecs-caplugin/Constants.cs b/entrust-ecs-caplugin/Constants.cs new file mode 100644 index 0000000..66a2f3e --- /dev/null +++ b/entrust-ecs-caplugin/Constants.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust +{ + public class Constants + { + public class Config + { + public const string USERNAME = "AuthUsername"; + public const string PASSWORD = "AuthPassword"; + public const string CLIENTCERT = "ClientCertificate"; + public const string NAME = "Name"; + public const string EMAIL = "Email"; + public const string PHONE = "PhoneNumber"; + public const string IGNOREEXPIRED = "IgnoreExpired"; + public const string ENABLED = "Enabled"; + + public const string LIFETIME = "LifetimeMonths"; + public const string ORGANIZATION = "Organization"; + public const string CERTUSAGE = "CertificateUsage"; + public const string RENEWAL_WINDOW = "RenewalWindowDays"; + + public class ClientCert + { + public const string STORE_NAME = "StoreName"; + public const string STORE_LOC = "StoreLocation"; + public const string THUMBPRINT = "Thumbprint"; + public const string CERT_PATH = "CertificatePath"; + public const string CERT_PASS = "CertificatePassword"; + } + } + } +} diff --git a/entrust-ecs-caplugin/ECSCAPlugin.cs b/entrust-ecs-caplugin/ECSCAPlugin.cs new file mode 100644 index 0000000..73503ec --- /dev/null +++ b/entrust-ecs-caplugin/ECSCAPlugin.cs @@ -0,0 +1,994 @@ +using Keyfactor.AnyGateway.Extensions; +using Keyfactor.Extensions.CAPlugin.Entrust.API; +using Keyfactor.Extensions.CAPlugin.Entrust.Client; +using Keyfactor.Extensions.CAPlugin.Entrust.Models; +using Keyfactor.Logging; +using Keyfactor.PKI.Enums.EJBCA; + +using Microsoft.Extensions.Logging; + +using Newtonsoft.Json; + +using Org.BouncyCastle.Asn1.X509; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using static Keyfactor.PKI.PKIConstants.Microsoft; + +namespace Keyfactor.Extensions.CAPlugin.Entrust +{ + public class ECSCAPlugin : IAnyCAPlugin + { + private ECSConfig _config; + private readonly ILogger _logger; + private ICertificateDataReader _certificateDataReader; + + public ECSCAPlugin() + { + _logger = LogHandler.GetClassLogger(); + } + + public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) + { + _certificateDataReader = certificateDataReader; + string rawConfig = JsonConvert.SerializeObject(configProvider.CAConnectionData); + _config = JsonConvert.DeserializeObject(rawConfig); + } + + /// + /// Enroll for a certificate + /// + /// The CSR for the certificate request + /// The subject string + /// The list of SANs + /// Collection of product information and options. Includes both product-level config options as well as custom enrollment fields. + /// The format of the request + /// The type of enrollment (new, renew, reissue) + /// The result of the enrollment + /// + public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) + { + ECSClient client = ECSClient.InitializeClient(_config); + _logger.LogTrace("Entrust Client Created"); + X509Name subjectParsed = new X509Name(subject); + _logger.LogTrace($"Parsed Subject with {subject}"); + + string underscoreErrorMessage = "Underscore is not allowed in DNSName."; + string requestEmail; + string requestNumber; + string requestName; + string commonName = ""; + string organization = ""; + string checkingSanVariable = ""; + int trackingId = 0; + int clientId = -1; + + // Check tracking ID if we're doing a renewal or reissuance. + if (enrollmentType == EnrollmentType.Reissue || enrollmentType == EnrollmentType.Renew || enrollmentType == EnrollmentType.RenewOrReissue) + { + _logger.LogTrace("This is a renew or reissue"); + trackingId = GetTrackingId(productInfo); + _logger.LogTrace($"With trackingId {trackingId}"); + // Check now if the trackingId is 0 to fail early. + if (trackingId == 0) + { + throw new Exception("The tracking ID of the certificate requested for renewal or reissue is 0. This certificate must be renewed or reissued through the Entrust portal."); + } + } + + try + { + checkingSanVariable = "Common Name"; + string cn = subjectParsed.GetValueList(X509Name.CN).Cast().LastOrDefault(); + if (!string.IsNullOrEmpty(cn)) + { + if (cn.Contains("_")) + { + throw new Exception(underscoreErrorMessage); + } + commonName = cn; + } + + _logger.LogTrace($"Common Name of {commonName}"); + + checkingSanVariable = "Organization"; + string org = subjectParsed.GetValueList(X509Name.O).Cast().LastOrDefault(); + if (productInfo.ProductParameters.ContainsKey(Constants.Config.ORGANIZATION) && !string.IsNullOrEmpty(productInfo.ProductParameters[Constants.Config.ORGANIZATION])) + { + organization = productInfo.ProductParameters[Constants.Config.ORGANIZATION]; + } + else if (!string.IsNullOrEmpty(org)) + { + organization = org; + } + + _logger.LogTrace($"Organization of {organization}"); + + checkingSanVariable = "Email"; + string subjectEmail = subjectParsed.GetValueList(X509Name.EmailAddress).Cast().LastOrDefault(); + if (productInfo.ProductParameters.ContainsKey(Constants.Config.EMAIL) && !string.IsNullOrEmpty(productInfo.ProductParameters[Constants.Config.EMAIL])) + { + requestEmail = productInfo.ProductParameters[Constants.Config.EMAIL]; + } + else if (!string.IsNullOrEmpty(subjectEmail)) + { + requestEmail = subjectEmail; + } + else if (!string.IsNullOrEmpty(_config.Email)) + { + requestEmail = _config.Email; + } + else + { + requestEmail = "email@email.invalid"; + } + + _logger.LogTrace($"Email of {requestEmail}"); + + checkingSanVariable = "Telephone Number"; + if (productInfo.ProductParameters.ContainsKey(Constants.Config.PHONE) && !string.IsNullOrEmpty(productInfo.ProductParameters[Constants.Config.PHONE])) + { + requestNumber = productInfo.ProductParameters[Constants.Config.PHONE]; + } + else if (!string.IsNullOrEmpty(_config.PhoneNumber)) + { + requestNumber = _config.PhoneNumber; + } + else + { + requestNumber = "0000000000"; + } + + _logger.LogTrace($"Telephone Number of {requestNumber}"); + + checkingSanVariable = "Name"; + if (productInfo.ProductParameters.ContainsKey(Constants.Config.NAME) && !string.IsNullOrEmpty(productInfo.ProductParameters[Constants.Config.NAME])) + { + requestName = productInfo.ProductParameters[Constants.Config.NAME]; + } + else if (!string.IsNullOrEmpty(_config.Name)) + { + requestName = _config.Name; + } + else + { + requestName = "TestUser"; + } + + _logger.LogTrace($"Name of {requestName}"); + } + catch (Exception ex) + { + if (ex.Message == underscoreErrorMessage) + { + _logger.LogError($"Error occurred trying to validate the SAN information. {ex.Message}"); + throw new Exception(ex.Message); + } + else + { + _logger.LogError($"Error occurred trying to validate the request information. Required attributes {checkingSanVariable} may be missing."); + throw new Exception("Error occurred trying to validate the request information. Required attributes " + checkingSanVariable + " may be missing."); + } + } + + List dnsNames = new List(); + if (san.ContainsKey("Dns")) + { + dnsNames = new List(san["Dns"]); + _logger.LogTrace($"First DNS SAN: {dnsNames[0]}"); + } + + if (!commonName.Contains('.')) + { + throw new Exception($"Domain cannot be determined from Common Name."); + } + + IEnumerable approvedOrgs = client.GetOrganizations().Where(x => x.VerificationStatus.Equals("APPROVED", StringComparison.OrdinalIgnoreCase)); + if (string.IsNullOrEmpty(organization)) // If the organization is empty, use the default client. + { + clientId = 1; + } + else + { + Organization org = approvedOrgs.FirstOrDefault(x => x.Name.Equals(organization, StringComparison.OrdinalIgnoreCase)); + if (org != null) + { + clientId = org.ClientId; + } + } + + _logger.LogTrace($"ClientId of {clientId}"); + + if (clientId == -1) + { + throw new Exception($"Organization {organization} is not a valid Entrust organization for this account. The following organizations are approved: {string.Join(", ", approvedOrgs.Select(x => x.Name))}."); + } + + string usageType = (productInfo.ProductParameters.ContainsKey("CertificateUsage")) ? productInfo.ProductParameters["CertificateUsage"] : ""; + _logger.LogTrace($"usageType of {usageType}"); + string eku = ""; + if (usageType.Equals("SERVERCLIENT", StringComparison.OrdinalIgnoreCase)) + { + eku = "SERVER_AND_CLIENT_AUTH"; + } + else if (usageType.Equals("SERVER", StringComparison.OrdinalIgnoreCase)) + { + eku = "SERVER_AUTH"; + } + else if (usageType.Equals("CLIENT", StringComparison.OrdinalIgnoreCase)) + { + eku = "CLIENT_AUTH"; + } + else + { + eku = ""; + } + + _logger.LogTrace($"Getting Tracking Info"); + Tracking trackingInfo = new Tracking() + { + TrackingInfo = "", + RequesterEmail = requestEmail, + RequesterName = requestName, + RequesterPhone = requestNumber, + Deactivated = false + }; + _logger.LogTrace($"Got Tracking Info"); + + if (!EntrustCertType.InventoryExists(client, productInfo.ProductID)) + { + _logger.LogError($"Inventory for certificate type '{productInfo.ProductID}' has been used up. To perform the operation, revoke existing certificates or contact Entrust to acquire new inventory."); + throw new Exception($"Inventory for certificate type '{productInfo.ProductID}' has been used up. To perform the operation, revoke existing certificates or contact Entrust to acquire new inventory."); + } + + var months = (productInfo.ProductParameters.ContainsKey("Lifetime")) ? int.Parse(productInfo.ProductParameters["Lifetime"]) : 12; + + _logger.LogTrace($"Months of {months}"); + if (enrollmentType == EnrollmentType.RenewOrReissue) + { + _logger.LogTrace($"Determining if request is a renew or a reissue"); + var priorCertSnString = productInfo.ProductParameters["PriorCertSN"]; + int renewalWindowDays = productInfo.ProductParameters.ContainsKey("RenewalWindowDays") ? int.Parse(productInfo.ProductParameters["RenewalWindowDays"]) : 90; + var reqId = _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString).Result; + if (string.IsNullOrEmpty(reqId)) + { + throw new Exception($"No certificate with serial number '{priorCertSnString}' could be found."); + } + var expDate = _certificateDataReader.GetExpirationDateByRequestId(reqId); + + var renewCutoff = DateTime.Now.AddDays(renewalWindowDays * -1); + + if (expDate > renewCutoff) + { + _logger.LogTrace($"Certificate with serial number {priorCertSnString} is within renewal window"); + enrollmentType = EnrollmentType.Renew; + } + else + { + _logger.LogTrace($"Certificate with serial number {priorCertSnString} is not within renewal window. Reissuing..."); + enrollmentType = EnrollmentType.Reissue; + } + } + + _logger.LogTrace($"Switch Statement for Enrollment Type of {enrollmentType}"); + CertificateResponse response; + switch (enrollmentType) + { + case EnrollmentType.New: + + _logger.LogTrace($"Csr is {csr}"); + _logger.LogTrace($"ClientId is {clientId}"); + _logger.LogTrace($"Org is {organization}"); + _logger.LogTrace($"CertType is {productInfo.ProductID.ToUpper()}"); + _logger.LogTrace($"CertExpiryDate is {DateTime.Now.AddMonths(months)}"); + _logger.LogTrace($"CertLifetime is {"P" + Math.Round(months / 12.0).ToString() + "Y"}"); + _logger.LogTrace($"Tracking is {trackingInfo}"); + _logger.LogTrace($"QueueForApproval is false"); + _logger.LogTrace($"CertEmail is {requestEmail}"); + _logger.LogTrace($"SubjectAltName is {(dnsNames.Count > 0 ? dnsNames[0] : "empty")}"); + _logger.LogTrace($"Password is ''"); + _logger.LogTrace($"SigningAlg is SHA-2"); + _logger.LogTrace($"Eku is {eku}"); + _logger.LogTrace($"Cn is {commonName}"); + _logger.LogTrace($"Upn is {requestEmail}"); + _logger.LogTrace($"Ou is empty string list"); + _logger.LogTrace($"EndUserKeyStorageAgreement is true"); + _logger.LogTrace($"ValidateOnly is false"); + + NewCertificateRequest request = new NewCertificateRequest() + { + Csr = csr, + ClientId = clientId, + Org = organization, + CertType = productInfo.ProductID.ToUpper(), + CertExpiryDate = DateTime.Now.AddMonths(months), + CertLifetime = "P" + Math.Round(months / 12.0).ToString() + "Y", + Tracking = trackingInfo, + QueueForApproval = false, + CertEmail = requestEmail, + SubjectAltName = dnsNames, + Password = "", + SigningAlg = "SHA-2", + Eku = eku, + Cn = commonName, + //email from userInfo + Upn = requestEmail, + Ou = new List(), + EndUserKeyStorageAgreement = true, + //When true, this causes the api to only validate the submitted info and not actually register a cert. + ValidateOnly = false + }; + _logger.LogTrace($"Before Validation Request: {JsonConvert.SerializeObject(request)}"); + (bool validResponse, string messageResponse) = client.ValidateRequestNewCertificate(request); + _logger.LogTrace($"ValidResponse?: {validResponse}"); + _logger.LogTrace($"messageResponse: {messageResponse}"); + + if (!validResponse) + { + _logger.LogError($"Request validation failed. {messageResponse}"); + throw new Exception($"Request validation failed. {messageResponse}"); + } + + response = client.RequestNewCertificate(request); + _logger.LogTrace($"New Cert Request Response: {JsonConvert.SerializeObject(response)}"); + break; + + case EnrollmentType.Reissue: + + _logger.LogTrace($"Csr is {csr}"); + _logger.LogTrace($"ClientId is {clientId}"); + _logger.LogTrace($"Org is {organization}"); + _logger.LogTrace($"Tracking is {trackingInfo}"); + _logger.LogTrace($"CertEmail is {requestEmail}"); + _logger.LogTrace($"SubjectAltName is {(dnsNames.Count > 0 ? dnsNames[0] : "empty")}"); + _logger.LogTrace($"Password is ''"); + _logger.LogTrace($"SigningAlg is SHA-2"); + _logger.LogTrace($"Eku is {eku}"); + _logger.LogTrace($"Cn is {commonName}"); + _logger.LogTrace($"Upn is {requestEmail}"); + _logger.LogTrace($"Ou is empty string list"); + _logger.LogTrace($"EndUserKeyStorageAgreement is true"); + + ReissueCertificateRequestBody reissueRequest = new ReissueCertificateRequestBody() + { + Csr = csr, + ClientId = clientId, + Org = string.Empty, + Tracking = trackingInfo, + CertEmail = requestEmail, + SubjectAltName = dnsNames, + Password = string.Empty, + SigningAlg = "SHA-2", + Eku = eku, + Cn = commonName, + //email from userInfo + Upn = requestEmail, + Ou = new List(), + EndUserKeyStorageAgreement = true, + }; + _logger.LogTrace($"reissueRequest: {JsonConvert.SerializeObject(reissueRequest)}"); + response = client.ReissueCertificate(reissueRequest, trackingId); + _logger.LogTrace($"reissueResponse: {JsonConvert.SerializeObject(response)}"); + break; + + case EnrollmentType.Renew: + + _logger.LogTrace($"Csr is {csr}"); + _logger.LogTrace($"ClientId is {clientId}"); + _logger.LogTrace($"Org is {organization}"); + _logger.LogTrace($"CertExpiryDate is {DateTime.Now.AddMonths(months)}"); + _logger.LogTrace($"CertLifetime is {"P" + Math.Round(months / 12.0).ToString() + "Y"}"); + _logger.LogTrace($"Tracking is {trackingInfo}"); + _logger.LogTrace($"CertEmail is {requestEmail}"); + _logger.LogTrace($"SubjectAltName is {(dnsNames.Count > 0 ? dnsNames[0] : "empty")}"); + _logger.LogTrace($"Password is ''"); + _logger.LogTrace($"SigningAlg is SHA-2"); + _logger.LogTrace($"Eku is {eku}"); + _logger.LogTrace($"Cn is {commonName}"); + _logger.LogTrace($"Upn is {requestEmail}"); + _logger.LogTrace($"Ou is empty string list"); + _logger.LogTrace($"EndUserKeyStorageAgreement is true"); + + RenewCertificateRequestBody renewRequest = new RenewCertificateRequestBody() + { + Csr = csr, + ClientId = clientId, + Org = "", + CertExpiryDate = DateTime.Now.AddMonths(months), + CertLifetime = "P" + Math.Round(months / 12.0).ToString() + "Y", + Tracking = trackingInfo, + CertEmail = requestEmail, + SubjectAltName = dnsNames, + Password = "", + SigningAlg = "SHA-2", + Eku = eku, + Cn = commonName, + //email from userInfo + Upn = requestEmail, + Ou = new List(), + EndUserKeyStorageAgreement = true, + }; + _logger.LogTrace($"reissueRequest: {JsonConvert.SerializeObject(renewRequest)}"); + //Validation is not supported for Renewals so validateOnly flag does not apply + response = client.RenewCertificate(renewRequest, trackingId); + _logger.LogTrace($"renewResponse: {JsonConvert.SerializeObject(response)}"); + break; + + default: + throw new Exception($"The enrollment type {enrollmentType} is not recognized."); + } + _logger.LogTrace($"Getting Cert By Tracking Id {response.TrackingId}"); + CertificateExt enrolledCert = client.GetCertificateByTrackingId(response.TrackingId); + _logger.LogTrace($"Got Cert By Tracking Id {response.TrackingId} with status of {enrolledCert.Status}"); + int status = ConvertStatus(enrolledCert.Status, response.TrackingId.ToString()); + string statusMessage; + switch (status) + { + case (int)EndEntityStatus.GENERATED: + statusMessage = $"Certificate with trackingId {enrolledCert.TrackingId} issued successfully"; + break; + + case (int)EndEntityStatus.EXTERNALVALIDATION: + // Attempt to approve the cert. If still pending, return External validation + (int statusPending, string statusPendingMessage) statusTuple = ApproveCert(response.TrackingId); + status = statusTuple.statusPending; + statusMessage = statusTuple.statusPendingMessage; + break; + + case (int)EndEntityStatus.FAILED: + statusMessage = $"Certificate with trackingId {enrolledCert.TrackingId} is denied"; + break; + + default: + statusMessage = $"Certificate with trackingId {enrolledCert.TrackingId} has an unknown status"; + break; + } + + _logger.LogTrace($"Returning Result of CARequestId={response.TrackingId}, Certificate={response.EndEntityCert}, Status={status}, StatusMessage={statusMessage}"); + return new EnrollmentResult + { + CARequestID = response.TrackingId.ToString(), + Certificate = response.EndEntityCert, + Status = status, + StatusMessage = statusMessage + }; + + } + + /// + /// Gets the annotations for the CA Connector-level configuration fields + /// + /// + public Dictionary GetCAConnectorAnnotations() + { + return new Dictionary() + { + [Constants.Config.USERNAME] = new PropertyConfigInfo() + { + Comments = "Username for the gateway to authenticate with Entrust", + Hidden = false, + DefaultValue = "", + Type = "String" + }, + [Constants.Config.PASSWORD] = new PropertyConfigInfo() + { + Comments = "Password for the account used to authenticate with Entrust", + Hidden = true, + DefaultValue = "", + Type = "String" + }, + [Constants.Config.CLIENTCERT] = new PropertyConfigInfo() + { + Comments = "The client certificate information used to authenticate with Entrust, only if configured to use certificate authentication", + Hidden = false, + DefaultValue = "", + Type = "ClientCertificate" + }, + [Constants.Config.NAME] = new PropertyConfigInfo() + { + Comments = "The default requester name", + Hidden = false, + DefaultValue = "TestUser", + Type = "String" + }, + [Constants.Config.EMAIL] = new PropertyConfigInfo() + { + Comments = "The default requester email address", + Hidden = false, + DefaultValue = "email@email.invalid", + Type = "String" + }, + [Constants.Config.PHONE] = new PropertyConfigInfo() + { + Comments = "The default requester phone number", + Hidden = false, + DefaultValue = "0000000000", + Type = "String" + }, + [Constants.Config.IGNOREEXPIRED] = new PropertyConfigInfo() + { + Comments = "If set to true, will not sync expired certs from Entrust", + Hidden = false, + DefaultValue = false, + Type = "Boolean" + }, + [Constants.Config.ENABLED] = new PropertyConfigInfo() + { + Comments = "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available.", + Hidden = false, + DefaultValue = true, + Type = "Boolean" + } + }; + } + + public List GetProductIds() + { + try + { + ECSClient client = ECSClient.InitializeClient(_config); + var certTypes = EntrustCertType.GetCustomerAccountTypes(client); + List productIds = new List(); + productIds.AddRange(certTypes.Select(c => c.ProductCode)); + return productIds; + } + catch (Exception ex) + { + _logger.LogError($"Unable to retrieve cert types from Entrust: {ex.Message}"); + return new List(); + } + } + + public async Task GetSingleRecord(string caRequestID) + { + // Get status of cert and the cert itself from Digicert + ECSClient client = ECSClient.InitializeClient(_config); + + // Split string to see what kind of ID we have. + string[] parts = caRequestID.Split('-'); + + // Get the cert by tracking ID or thumbprint. + CertificateExt entrustCert = parts.Length == 1 ? client.GetCertificateByTrackingId(Int32.Parse(caRequestID)) : client.GetCertificateByThumbprint(parts[1]); + int status = ConvertStatus(entrustCert.Status, caRequestID); + if (status == (int)RequestDisposition.PENDING) + { + status = (int)EndEntityStatus.EXTERNALVALIDATION; + } + return new AnyCAPluginCertificate + { + CARequestID = caRequestID, + Certificate = !string.IsNullOrEmpty(entrustCert.EndEntityCert) ? entrustCert.EndEntityCert : null, + Status = status, + ProductID = entrustCert.CertType + }; + } + + public Dictionary GetTemplateParameterAnnotations() + { + return new Dictionary() + { + [Constants.Config.LIFETIME] = new PropertyConfigInfo() + { + Comments = "OPTIONAL: The number of months of validity to use when requesting certs. If not provided, default is 12.", + Hidden = false, + DefaultValue = 12, + Type = "Number" + }, + [Constants.Config.ORGANIZATION] = new PropertyConfigInfo() + { + Comments = "OPTIONAL: For requests that will not have a subject (such as ACME) you can use this field to provide an organization name. Value supplied here will override any CSR values, so do not include this field if you want the organization from the CSR to be used.", + Hidden = false, + DefaultValue = "", + Type = "String" + }, + [Constants.Config.CERTUSAGE] = new PropertyConfigInfo() + { + Comments = "Required for public SSL certificate types. Represents the key usage for the certificates enrolled against this template. Valid values are 'server', 'client', or 'serverclient'. Do not provide a value for cert types that are not public SSL.", + Hidden = false, + DefaultValue = "server", + Type = "String" + }, + [Constants.Config.RENEWAL_WINDOW] = new PropertyConfigInfo() + { + Comments = "OPTIONAL: The number of days from certificate expiration that the gateway should do a renewal rather than a reissue. If not provided, default is 90.", + Hidden = false, + DefaultValue = 90, + Type = "Number" + } + }; + } + + public async Task Ping() + { + try + { + ECSClient client = ECSClient.InitializeClient(_config); + + _logger.LogDebug("Attempting to ping Entrust API."); + + _ = client.GetClients(); + + _logger.LogDebug("Successfully pinged Entrust API."); + } + catch (Exception e) + { + _logger.LogError($"There was an error contacting Entrust: {e.Message}."); + throw new Exception($"Error attempting to ping Entrust: {e.Message}.", e); + } + } + + public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) + { + _logger.LogTrace("Entered Entrust Revoke method"); + + ECSClient client = ECSClient.InitializeClient(_config); + string reason = RevokeReasonToString(revocationReason); + string comment = $"Revoked by Entrust Gateway for the following reason: {reason}"; + var cert = await GetSingleRecord(caRequestID); + + if (!string.Equals(reason, "keyCompromise")) + { + // Entrust no longer accepts any reason codes other than keyCompromise and unspecified. + reason = "unspecified"; + } + + if (!(cert.Status == (int)EndEntityStatus.GENERATED)) + { + string errorMessage = String.Format("Request {0} was not found in Entrust database or is not in a valid state to perform a revocation", caRequestID); + _logger.LogError(errorMessage); + throw new Exception(errorMessage); + } + client.RevokeCertificate(Int32.Parse(caRequestID), reason, comment); + + return (int)EndEntityStatus.REVOKED; + } + + public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) + { + int deniedCerts = 0; + int totalSkipped = 0; + ECSClient client = ECSClient.InitializeClient(_config); + List allCerts = client.GetAllCertificates(); + bool ignoreExpired = false; + if (_config.IgnoreExpired.HasValue) + { + ignoreExpired = _config.IgnoreExpired.Value; + } + foreach (Certificate entrustCert in allCerts) + { + cancelToken.ThrowIfCancellationRequested(); + + if (entrustCert.ExpiresAfter.GetValueOrDefault() <= DateTime.UtcNow && ignoreExpired) + { + _logger.LogTrace($"The certificate with serial number '{entrustCert.SerialNumber}' is expired and IgnoreExpired is true. Skipping."); + continue; + } + + // Set up request ID. + string caRequestId = entrustCert.TrackingId.ToString(); + + // If the tracking ID is 0, log it and modify the request ID. + if (entrustCert.TrackingId == 0) + { + _logger.LogWarning($"The certificate with serial number '{entrustCert.SerialNumber}' has a tracking ID of 0. Will attempt to sync using thumbprint."); + + string thumbprint = GetThumbprint(entrustCert); + if (string.IsNullOrEmpty(thumbprint)) + { + _logger.LogWarning("The thumbprint could not be found. Skipping certificate."); + ++totalSkipped; + continue; + } + + caRequestId = $"0-{thumbprint}"; + } + + try + { + // Find cert within the database + int dbCertStatus; + try + { + dbCertStatus = await _certificateDataReader.GetStatusByRequestID(caRequestId); + } + catch + { + //Record not found in database + dbCertStatus = -1; + } + + // Get status and check to see if we need to skip it. + int entrustStatus = ConvertStatus(entrustCert.Status, caRequestId); + if (entrustStatus == (int)EndEntityStatus.FAILED) + { + _logger.LogWarning($"Certificate with tracking ID '{entrustCert.TrackingId}' has a status of FAILED and will be skipped, as it has no certificate record."); + ++deniedCerts; + continue; + } + + // If the cert exists, check the status and see if it's different from the cert from Entrust + // If doing a full sync, update the record anyway (in case other fields have changed) + if (dbCertStatus >= 0) + { + if (dbCertStatus != entrustStatus || fullSync) + { + AnyCAPluginCertificate newCert = entrustCert.TrackingId != 0 ? GetRecordByTrackingId(entrustCert.TrackingId) : GetRecordByThumbprint(GetThumbprint(entrustCert)); + blockingBuffer.Add(newCert); + } + } + else + { + AnyCAPluginCertificate newCert = entrustCert.TrackingId != 0 ? GetRecordByTrackingId(entrustCert.TrackingId) : GetRecordByThumbprint(GetThumbprint(entrustCert)); + blockingBuffer.Add(newCert); + } + } + catch (Exception e) + { + _logger.LogError($"An error occurred while processing certificate with tracking ID '{entrustCert.TrackingId}', skipping.", e); + ++totalSkipped; + } + } + + _logger.LogDebug($"Synchronization skipped a total of {deniedCerts} certificates with the 'DECLINED' status."); + } + + public async Task ValidateCAConnectionInfo(Dictionary connectionInfo) + { + _logger.MethodEntry(LogLevel.Trace); + + try + { + if (!(bool)connectionInfo[Constants.Config.ENABLED]) + { + _logger.LogWarning($"The CA is currently in the Disabled state. It must be Enabled to perform operations. Skipping validation..."); + _logger.MethodExit(LogLevel.Trace); + return; + } + } + catch (Exception ex) + { + _logger.LogError($"Exception: {LogHandler.FlattenException(ex)}"); + } + + List errors = new List(); + + _logger.LogTrace("Checking the Username"); + string username = connectionInfo.ContainsKey(Constants.Config.USERNAME) ? (string)connectionInfo[Constants.Config.USERNAME] : string.Empty; + if (string.IsNullOrWhiteSpace(username)) + { + errors.Add("The username is required"); + } + + _logger.LogTrace("Checking the Password"); + string password = connectionInfo.ContainsKey(Constants.Config.PASSWORD) ? (string)connectionInfo[Constants.Config.PASSWORD] : string.Empty; + if (string.IsNullOrWhiteSpace(password)) + { + errors.Add("The password is required"); + } + + _logger.LogTrace("Checking the user information"); + string name = connectionInfo.ContainsKey(Constants.Config.NAME) ? (string)connectionInfo[Constants.Config.NAME] : string.Empty; + if (string.IsNullOrWhiteSpace(name)) + { + errors.Add("The name is required"); + } + + string email = connectionInfo.ContainsKey(Constants.Config.EMAIL) ? (string)connectionInfo[Constants.Config.EMAIL] : string.Empty; + if (string.IsNullOrWhiteSpace(email)) + { + errors.Add("The email is required"); + } + + string number = connectionInfo.ContainsKey(Constants.Config.PHONE) ? (string)connectionInfo[Constants.Config.PHONE] : string.Empty; + if (string.IsNullOrWhiteSpace(number)) + { + errors.Add("The phone number is required"); + } + + ECSConfig tempConfig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(connectionInfo)); + + ECSClient client = ECSClient.InitializeClient(tempConfig); + try + { + List clients = client.GetClients(); + if (clients.Count <= 0) + { + errors.Add($"Checking clients to determine Entrust connection failed."); + } + } + catch (Exception e) + { + errors.Add($"An error occured when trying to connect to Entrust. {e.Message}"); + } + _logger.LogTrace("Leaving 'ValidateCAConnectionInfo' method."); + + // We cannot proceed if there are any errors. + if (errors.Any()) + { + ThrowValidationException(errors); + } + } + + private void ThrowValidationException(List errors) + { + throw new AnyCAValidationException(string.Join("\n", errors)); + } + + public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary connectionInfo) + { + string productId = productInfo.ProductID; + ECSConfig tempConfig = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(connectionInfo)); + + ECSClient client = ECSClient.InitializeClient(tempConfig); + _logger.LogTrace("Checking inventory"); + + bool inventory = EntrustCertType.ProductIDValid(client, productId); + if (!inventory) + { + throw new Exception($"The product ID '{productId}' could not be validated."); + } + else + { + _logger.LogTrace($"Validation for product ID '{productId}' successful"); + } + } + + private int ConvertStatus(string status, string certId) + { + switch (status.ToLower()) + { + case "active": + case "ready": + case "reissued": + case "renewed": + case "expired": + return (int)EndEntityStatus.GENERATED; + + case "pending": + return (int)EndEntityStatus.EXTERNALVALIDATION; + + case "deactivated": + case "suspended": + case "revoked": + return (int)EndEntityStatus.REVOKED; + + case "declined": + return (int)EndEntityStatus.FAILED; + + default: + _logger.LogError($"Order {certId} has unexpected status {status}"); + throw new Exception($"Order {certId} has unknown status {status}"); + } + } + + public static string RevokeReasonToString(UInt32 revokeType) + { + switch (revokeType) + { + case 1: + case 2: // Entrust doesn't accept CA Compromised, since they get to decide that, not us + return "keyCompromise"; + case 3: + return "affiliationChanged"; + case 4: + return "superseded"; + case 5: + case 6: // Entrust doesn't accept Certificate Hold + return "cessationOfOperation"; + default: + return "affiliationChanged"; + } + } + + private string GetThumbprint(Certificate entrustCert) + { + // It seems as if this URL is the only place we can actually get the thumbprint. + if (entrustCert.URI.Contains("/thumbprints/")) + { + string[] parts = entrustCert.URI.Split(new string[] { "/thumbprints/" }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length > 1) + { + // Trim just in case some URIs come back with trailing slash. + return parts.Last().Trim('/').ToUpper(); + } + } + + // If the URL doesn't contain thumbprint, we return nothing. + return null; + } + + /// + /// Gets a single record by its tracking ID. + /// + /// The Entrust REST API client. + /// The tracking ID of the cert we want. + /// + private AnyCAPluginCertificate GetRecordByTrackingId(int trackingId) + { + ECSClient client = ECSClient.InitializeClient(_config); + CertificateExt entrustCertDetail = client.GetCertificateByTrackingId(trackingId); + string cert = !string.IsNullOrEmpty(entrustCertDetail.EndEntityCert) ? entrustCertDetail.EndEntityCert : null; + int statusCode = ConvertStatus(entrustCertDetail.Status, trackingId.ToString()); + + AnyCAPluginCertificate newCert = new AnyCAPluginCertificate + { + CARequestID = trackingId.ToString(), + Certificate = cert, + Status = statusCode, + CSR = !string.IsNullOrEmpty(entrustCertDetail.Csr) ? entrustCertDetail.Csr : null, + RevocationDate = entrustCertDetail.Tracking.Deactivated ? entrustCertDetail.Tracking.DeactivatedOn ?? DateTime.UtcNow : (DateTime?)null, + ProductID = entrustCertDetail.CertType + }; + return newCert; + } + + /// + /// Gets a single record by its thumbprint. + /// + /// The Entrust REST API client. + /// The thumbprint of the cert we want. + /// + private AnyCAPluginCertificate GetRecordByThumbprint(string thumbprint) + { + ECSClient client = ECSClient.InitializeClient(_config); + CertificateExt entrustCertDetail = client.GetCertificateByThumbprint(thumbprint); + string cert = !string.IsNullOrEmpty(entrustCertDetail.EndEntityCert) ? entrustCertDetail.EndEntityCert : null; + int statusCode = entrustCertDetail.Status.Equals("UNKNOWN", StringComparison.OrdinalIgnoreCase) ? (int)EndEntityStatus.EXTERNALVALIDATION : ConvertStatus(entrustCertDetail.Status, $"0-{thumbprint}"); + AnyCAPluginCertificate newCert = new AnyCAPluginCertificate + { + CARequestID = $"0-{thumbprint}", + Certificate = cert, + Status = statusCode, + CSR = !string.IsNullOrEmpty(entrustCertDetail.Csr) ? entrustCertDetail.Csr : null, + RevocationDate = entrustCertDetail.Tracking.Deactivated ? entrustCertDetail.Tracking.DeactivatedOn ?? DateTime.UtcNow : (DateTime?)null, + ProductID = entrustCertDetail.CertType + }; + return newCert; + } + + private int GetTrackingId(EnrollmentProductInfo enrollmentProductInfo) + { + ECSClient client = ECSClient.InitializeClient(_config); + if (enrollmentProductInfo.ProductParameters.ContainsKey("PriorCertSN")) + { + //get prior cert serial number + string attrPriorCertSN = enrollmentProductInfo.ProductParameters["PriorCertSN"]; + + //requesting certificate by serial number + Certificate priorCertTemp = client.GetCertificateBySerialNumber(attrPriorCertSN); + if (priorCertTemp != null) + { + return priorCertTemp.TrackingId; + } + else + { + _logger.LogTrace($"No certificate found with serial number {enrollmentProductInfo.ProductParameters["PriorCertSN"]}."); + } + } + + throw new Exception($"Reissue requested, but certificate with serial number {enrollmentProductInfo.ProductParameters["PriorCertSN"]} not found."); + } + + private ValueTuple ApproveCert(int trackingId) + { + var client = ECSClient.InitializeClient(_config); + CertificateResponse approveResult = client.ApproveCertificate(trackingId); + CertificateExt changedCert = client.GetCertificateByTrackingId(trackingId); + int newStatus = ConvertStatus(changedCert.Status, trackingId.ToString()); + + if (newStatus == (int)EndEntityStatus.EXTERNALVALIDATION) + { + return ((int)EndEntityStatus.EXTERNALVALIDATION, $"Certificate with trackingId {trackingId} is still pending after approval attempt. External validation is required."); + } + else if (newStatus == (int)EndEntityStatus.GENERATED) + { + return (newStatus, $"Certificate with trackingId {trackingId} has been issued after Entrust returned it with a pending status."); + } + + throw new Exception($"Unable to approve certificate with trackingId {trackingId}. Status is neither issued or pending. "); + } + } +} diff --git a/entrust-ecs-caplugin/ECSConfig.cs b/entrust-ecs-caplugin/ECSConfig.cs new file mode 100644 index 0000000..6e8d2ac --- /dev/null +++ b/entrust-ecs-caplugin/ECSConfig.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Transactions; + +namespace Keyfactor.Extensions.CAPlugin.Entrust +{ + public class ECSConfig + { + public string AuthUsername { get; set; } + public string AuthPassword { get; set; } + public AuthCert ClientCertificate { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string PhoneNumber { get; set; } + public bool? IgnoreExpired { get; set; } + public bool Enabled { get; set; } = true; + } + + public class AuthCert + { + public string StoreName { get; set; } + public string StoreLocation { get; set; } + public string Thumbprint { get; set; } + public string CertificatePath { get; set; } + public string CertificatePassword { get; set; } + } +} diff --git a/entrust-ecs-caplugin/Models/CertTypes.cs b/entrust-ecs-caplugin/Models/CertTypes.cs new file mode 100644 index 0000000..d8eff2f --- /dev/null +++ b/entrust-ecs-caplugin/Models/CertTypes.cs @@ -0,0 +1,126 @@ +using Keyfactor.Extensions.CAPlugin.Entrust.API; +using Keyfactor.Extensions.CAPlugin.Entrust.Client; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.Models +{ + public class EntrustCertType + { + public string ShortName { get; set; } + public string ProductCode { get; set; } + public string DisplayName { get; set; } + + #region Product Types + + public static EntrustCertType Standard = new EntrustCertType() { ShortName = "STANDARD_SSL", ProductCode = "STANDARD_SSL", DisplayName = "Standard SSL" }; + public static EntrustCertType Advantage = new EntrustCertType() { ShortName = "ADVANTAGE_SSL", ProductCode = "ADVANTAGE_SSL", DisplayName = "Advantage SSL" }; + public static EntrustCertType UC = new EntrustCertType() { ShortName = "UC_SSL", ProductCode = "UC_SSL", DisplayName = "UC SSL" }; + + public static EntrustCertType EV = new EntrustCertType() { ShortName = "EV_SSL", ProductCode = "EV_SSL", DisplayName = "EV SSL" }; + public static EntrustCertType QWAC = new EntrustCertType() { ShortName = "QWAC_SSL", ProductCode = "QWAC_SSL", DisplayName = "QWAC SSL" }; + public static EntrustCertType PSD2 = new EntrustCertType() { ShortName = "PSD2_SSL", ProductCode = "PSD2_SSL", DisplayName = "PSD2 SSL" }; + + public static EntrustCertType Wildcard = new EntrustCertType() { ShortName = "WILDCARD_SSL", ProductCode = "WILDCARD_SSL", DisplayName = "Wildcard SSL" }; + public static EntrustCertType Private = new EntrustCertType() { ShortName = "PRIVATE_SSL", ProductCode = "PRIVATE_SSL", DisplayName = "Private SSL" }; + public static EntrustCertType PD = new EntrustCertType() { ShortName = "PD_SSL", ProductCode = "PD_SSL", DisplayName = "PD SSL" }; + public static EntrustCertType CodeSigning = new EntrustCertType() { ShortName = "CODE_SIGNING", ProductCode = "CODE_SIGNING", DisplayName = "Code Signing" }; + public static EntrustCertType EVCodeSigning = new EntrustCertType() { ShortName = "EV_CODE_SIGNING", ProductCode = "EV_CODE_SIGNING", DisplayName = "EV Code Signing" }; + public static EntrustCertType CDSIndividual = new EntrustCertType() { ShortName = "CDS_INDIVIDUAL", ProductCode = "CDS_INDIVIDUAL", DisplayName = "CDS Individual" }; + public static EntrustCertType CDSGroup = new EntrustCertType() { ShortName = "CDS_GROUP", ProductCode = "CDS_GROUP", DisplayName = "CDS Group" }; + public static EntrustCertType CDSEntLite = new EntrustCertType() { ShortName = "CDS_ENT_LITE", ProductCode = "CDS_ENT_LITE", DisplayName = "CDS Ent Lite" }; + public static EntrustCertType CDSEntPro = new EntrustCertType() { ShortName = "CDS_ENT_PRO", ProductCode = "CDS_ENT_PRO", DisplayName = "CDS Ent Pro" }; + public static EntrustCertType SMIMEEnt = new EntrustCertType() { ShortName = "SMIME_ENT", ProductCode = "SMIME_ENT", DisplayName = "SMIME Ent" }; + + /// + /// Master list of all product types. + /// + public static new List AllTypes = new List() { Standard, Advantage, UC, EV, QWAC, PSD2, Wildcard, Private, PD, CodeSigning, EVCodeSigning, CDSIndividual, CDSGroup, CDSEntLite, CDSEntPro, SMIMEEnt }; + + /// + /// The certificate types that are available through FLEX inventory. + /// + private static List FlexTypes => AllTypes.Where(x => x.ShortName.Contains("SSL") && x.ShortName != "PD_SSL").ToList(); + + #endregion Product Types + + + #region Methods + + /// + /// Checks if inventory exists for the product type provided. + /// + /// The is used to call out to the Entrust API. + /// The product type we seek to check the inventory of. + /// + public static bool InventoryExists(ECSClient client, string productType) + { + // Gets an inventory with all of the product types. + List inventoryItems = client.GetInventories(); + + InventoryItem inventory = inventoryItems.FirstOrDefault(x => x.ProductType.Equals(productType, StringComparison.CurrentCultureIgnoreCase)); + if (inventory == null) + { + inventory = inventoryItems.FirstOrDefault(x => x.ProductType.Equals("FLEX", StringComparison.CurrentCultureIgnoreCase)); + } + + return inventory != null && inventory.RemainingCount > 0; + } + + /// + /// Checks if the product ID exists in Entrust. + /// + /// The is used to call out to the Entrust API. + /// The product type we seek to check the inventory of. + /// + public static bool ProductIDValid(ECSClient client, string productType) + { + // Client should always be valid, but sanity check it anyway. + if (client == null) + { + return false; + } + + // Try to find the product they asked for. + EntrustCertType inventory = GetCustomerAccountTypes(client) + ?.FirstOrDefault(x => x.ProductCode.Equals(productType, StringComparison.CurrentCultureIgnoreCase)); + + return inventory != null; + } + + /// + /// Gets a complete list of product types for a customer's account, including both the base types and the FLEX product types. + /// + /// The making the API call to get the product types. + /// + public static List GetCustomerAccountTypes(ECSClient client) + { + if (client == null) + { + throw new Exception("The client does not have a value, and therefore the customer account types cannot be retrieved."); + } + + // Gets an inventory with all of the product types. + List inventoryItems = client.GetInventories(); + + // Add the base types. + List customerTypes = AllTypes + .Where(x => inventoryItems.Any(y => y.ProductType == x.ProductCode)) + .ToList(); + + // Add any types we get through FLEX inventory. + if (inventoryItems.Any(x => x.ProductType == "FLEX")) + { + customerTypes.AddRange(FlexTypes.Where(x => !customerTypes.Any(y => y.ShortName == x.ShortName))); + } + + return customerTypes; + } + + #endregion Methods + } +} diff --git a/entrust-ecs-caplugin/Models/CertificateOperation.cs b/entrust-ecs-caplugin/Models/CertificateOperation.cs new file mode 100644 index 0000000..94c158e --- /dev/null +++ b/entrust-ecs-caplugin/Models/CertificateOperation.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.Models +{ + public class CertificateOperation + { + public static string APPROVE = "APPROVE"; + public static string DECLINE = "DECLINE"; + public static string SUSPEND = "SUSPEND"; + public static string UNSUSPEND = "UNSUSPEND"; + } +} diff --git a/entrust-ecs-caplugin/Models/CertificateRequest.cs b/entrust-ecs-caplugin/Models/CertificateRequest.cs new file mode 100644 index 0000000..b1aae18 --- /dev/null +++ b/entrust-ecs-caplugin/Models/CertificateRequest.cs @@ -0,0 +1,71 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.Models +{ + public class CertificateRequest + { + [JsonProperty("csr")] + public string CSR { get; set; } + + [JsonProperty("subjectAltName")] + public List SubjectAltName { get; set; } + + [JsonProperty("signingAlg")] + public string SigningAlg { get; set; } = "SHA-2"; + + [JsonProperty("eku")] + public string EKU { get; set; } + + [JsonProperty("ctLog")] + public bool? CTLog { get; set; } + + [JsonProperty("cn")] + public string CN { get; set; } + + [JsonProperty("certEmail")] + public string CertEmail { get; set; } + + [JsonProperty("upn")] + public string UPN { get; set; } + + [JsonProperty("clientId")] + public int? ClientId { get; set; } + + [JsonProperty("org")] + public string Org { get; set; } + + [JsonProperty("ou")] + public List OU { get; set; } + + [JsonProperty("password")] + public string Password { get; set; } + + [JsonProperty("tracking")] + public Tracking Tracking { get; set; } + + [JsonProperty("endUserKeyStorageAgreement")] + public bool? EndUserKeyStorageAgreement { get; set; } + + [JsonProperty("queueForApproval")] + public bool? QueueForApproval { get; set; } + + [JsonProperty("certExpiryDate")] + public DateTime? CertExpiryDate { get; set; } + + [JsonProperty("certLifetime")] + public string CertLifetime { get; set; } + + [JsonProperty("validateOnly")] + public bool? ValidateOnly { get; set; } + + [JsonProperty("certType")] + public string CertType { get; set; } + } +} diff --git a/entrust-ecs-caplugin/Models/Tracking.cs b/entrust-ecs-caplugin/Models/Tracking.cs new file mode 100644 index 0000000..a434e9b --- /dev/null +++ b/entrust-ecs-caplugin/Models/Tracking.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.CAPlugin.Entrust.Models +{ + public class Tracking + { + [JsonProperty("trackingInfo")] + public string TrackingInfo { get; set; } + + [JsonProperty("requesterName")] + public string RequesterName { get; set; } + + [JsonProperty("requesterEmail")] + public string RequesterEmail { get; set; } + + [JsonProperty("requesterPhone")] + public string RequesterPhone { get; set; } + + [JsonProperty("deactivated")] + public bool Deactivated { get; set; } + + [JsonProperty("deactivatedOn")] + public DateTime? DeactivatedOn { get; set; } + } +} diff --git a/entrust-ecs-caplugin/entrust-ecs-caplugin.csproj b/entrust-ecs-caplugin/entrust-ecs-caplugin.csproj new file mode 100644 index 0000000..ceb2a3c --- /dev/null +++ b/entrust-ecs-caplugin/entrust-ecs-caplugin.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + Keyfactor.Extensions.CAPlugin.Entrust + disable + disable + EntrustECSCAPlugin + + + + + + + + + + + + + Always + + + + + diff --git a/entrust-ecs-caplugin/manifest.json b/entrust-ecs-caplugin/manifest.json new file mode 100644 index 0000000..27b73aa --- /dev/null +++ b/entrust-ecs-caplugin/manifest.json @@ -0,0 +1,10 @@ +{ + "extensions": { + "Keyfactor.AnyGateway.Extensions.IAnyCAPlugin": { + "EntrustECSCAPlugin": { + "assemblypath": "EntrustECSCAPlugin.dll", + "TypeFullName": "Keyfactor.Extensions.CAPlugin.Entrust.ECSCAPlugin" + } + } + } +} diff --git a/integration-manifest.json b/integration-manifest.json index 4beca57..a6b473d 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,12 +1,12 @@ { - "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", - "integration_type": "ca-gateway", - "name": "", - "status": "prototype", - "support_level": "community", - "link_github": false, - "update_catalog": false, - "description": "", - "gateway_framework": "10.x.x", - "release_dir": "UPDATE-THIS-WITH-PATH-TO-BINARY-BUILD-FOLDER" + "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json", + "integration_type": "anyca-plugin", + "name": "Entrust ECS AnyCA REST Gateway Plugin", + "status": "production", + "support_level": "kf-supported", + "link_github": true, + "update_catalog": true, + "description": "Entrust ECS plugin for the AnyCA REST Gateway framework", + "gateway_framework": "24.2.0", + "release_dir": "entrust-ecs-caplugin/bin/Release/net6.0" }