Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/keyfactor-bootstrap-workflow-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ on:

jobs:
call-starter-workflow:
uses: keyfactor/actions/.github/workflows/starter.yml@v3
uses: keyfactor/actions/.github/workflows/starter.yml@v5
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 }}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
1.0.0
* Initial Release

1.1.0
* Added support for renew/reissue
* Made CARequestID more consistent for pending/external validation certs
* Allow for the specifying of custom lifetimes for certs, and comparing the provided value to the allowed values
25 changes: 11 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<!-- TOC -->
<a href="#support">
<b>Support</b>
</a>
</a>
·
<a href="#requirements">
<b>Requirements</b>
Expand All @@ -33,7 +33,6 @@
</a>
</p>


The Nameshield AnyCA Gateway REST plugin extends the capabilities of Nameshield CA 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
* All Sync jobs are currently treated as a full sync
Expand All @@ -46,7 +45,7 @@ The Nameshield AnyCA Gateway REST plugin extends the capabilities of Nameshield
The Nameshield AnyCA Gateway REST plugin is compatible with the Keyfactor AnyCA Gateway REST 24.2.0 and later.

## Support
The Nameshield AnyCA Gateway REST plugin is open source and community supported, meaning that there is **no SLA** applicable.
The Nameshield AnyCA Gateway REST plugin is open source and community supported, meaning that there is **no SLA** applicable.

> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab.

Expand All @@ -60,16 +59,15 @@ TODO Requirements is a required section

2. On the server hosting the AnyCA Gateway REST, download and unzip the latest [Nameshield AnyCA Gateway REST plugin](https://github.com/Keyfactor/nameshield-caplugin/releases/latest) from GitHub.

3. Copy the unzipped directory (usually called `net6.0` or `net8.0`) to the Extensions directory:
3. Copy the unzipped directory (usually called `net6.0`) to the Extensions directory:


```shell
Depending on your AnyCA Gateway REST version, copy the unzipped directory to one of the following locations:
Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net6.0\Extensions
Program Files\Keyfactor\AnyCA Gateway\AnyGatewayREST\net8.0\Extensions
```

> The directory containing the Nameshield AnyCA Gateway REST plugin DLLs (`net6.0` or `net8.0`) can be named anything, as long as it is unique within the `Extensions` directory.
> The directory containing the Nameshield AnyCA Gateway REST plugin DLLs (`net6.0`) can be named anything, as long as it is unique within the `Extensions` directory.

4. Restart the AnyCA Gateway REST service.

Expand All @@ -87,25 +85,24 @@ TODO Requirements is a required section

Populate using the configuration fields collected in the [requirements](#requirements) section.

* **ApiUrl** - The base URL to send API requests to. Standard URLs are https://api.nameshield.net/ for production environment, and https://ote-api.nameshield.net/ for test and development
* **ApiToken** - The Bearer token to use to authenticate to the Nameshield API
* **Enabled** - Flag to Enable or Disable gateway functionality. Diabling is primarily used to allow creation of the CA prior to configuration information being available.
* **ApiUrl** - The base URL to send API requests to. Standard URLs are https://api.nameshield.net/ for production environment, and https://ote-api.nameshield.net/ for test and development
* **ApiToken** - The Bearer token to use to authenticate to the Nameshield API
* **Enabled** - Flag to Enable or Disable gateway functionality. Diabling is primarily used to allow creation of the CA prior to configuration information being available.

2. When defining templates, use the product names as the ProductID (e.g. Digicert Secure Site EV) as opposed to the ID (e.g. ABC012)

3. Follow the [official Keyfactor documentation](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Keyfactor.htm) to add each defined Certificate Authority to Keyfactor Command and import the newly defined Certificate Templates.

4. In Keyfactor Command (v12.3+), for each imported Certificate Template, follow the [official documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm) to define enrollment fields for each of the following parameters:

* **OrganizationId** - If you know your organization's ID in nameshield, you can supply that here and that will be used as the organization regardless of what values exist in the request or the Organization field
* **Organization** - If OrganizationId is empty, and an organization name is provided here, the Nameshield gateway will use that name to do organization lookups when enrolling. If neither OrganizationId nor Organization is supplied, the gateway will use whatever is in the O= field of the request subject.


* **OrganizationId** - If you know your organization's ID in nameshield, you can supply that here and that will be used as the organization regardless of what values exist in the request or the Organization field
* **Organization** - If OrganizationId is empty, and an organization name is provided here, the Nameshield gateway will use that name to do organization lookups when enrolling. If neither OrganizationId nor Organization is supplied, the gateway will use whatever is in the O= field of the request subject.
* **Lifetime** - REQUIRED:The lifetime of the certificate to be issued. This should be a string in the format recognized by Nameshield (e.g. 1 year, etc.)

## License

Apache License 2.0, see [LICENSE](LICENSE).

## Related Integrations

See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway).
See all [Keyfactor Any CA Gateways (REST)](https://github.com/orgs/Keyfactor/repositories?q=anycagateway).
4 changes: 4 additions & 0 deletions integration-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
{
"name": "Organization",
"description": "If OrganizationId is empty, and an organization name is provided here, the Nameshield gateway will use that name to do organization lookups when enrolling. If neither OrganizationId nor Organization is supplied, the gateway will use whatever is in the O= field of the request subject."
},
{
"name": "Lifetime",
"description": "REQUIRED:The lifetime of the certificate to be issued. This should be a string in the format recognized by Nameshield (e.g. 1 year, etc.)"
}
]
}
Expand Down
5 changes: 5 additions & 0 deletions nameshield-caplugin/API/RequestCertificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public CertificateRequest()
data.relationships.product.data = new RelationshipData();
data.relationships.organization = new CertRequestRel();
data.relationships.organization.data = new RelationshipData();
data.relationships.certificate = new CertRequestRel();
data.relationships.certificate.data = new RelationshipData();
}

public string GetOrderData()
Expand Down Expand Up @@ -81,6 +83,9 @@ public class CertificateRequestRelationships
[JsonPropertyName("organization")]
[JsonProperty("organization")]
public CertRequestRel organization { get; set; }
[JsonPropertyName("certificate")]
[JsonProperty("certificate")]
public CertRequestRel certificate { get; set; }
}

public class CertRequestRel
Expand Down
1 change: 1 addition & 0 deletions nameshield-caplugin/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class Template
{
public const string ORGANIZATION_ID = "OrganizationId";
public const string ORGANIZATION = "Organization";
public const string LIFETIME = "Lifetime";
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions nameshield-caplugin/Models/Certificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class Certificate
[JsonPropertyName("attributes")]
[JsonProperty("attributes")]
public CertificateAttributes Attributes { get; set; }
[JsonPropertyName("relationships")]
[JsonProperty("relationships")]
public CertificateRelationships Relationships { get; set; }
}

public class CertificateAttributes
Expand All @@ -60,4 +63,18 @@ public class CertificateAttributes
[JsonProperty("pem")]
public string Pem { get; set; }
}

public class CertificateRelationships
{
[JsonPropertyName("current-order")]
[JsonProperty("current-order")]
public CertOrderRelationship Order { get; set; }
}

public class CertOrderRelationship
{
[JsonPropertyName("data")]
[JsonProperty("data")]
public RelationshipData Data { get; set; }
}
}
3 changes: 3 additions & 0 deletions nameshield-caplugin/Models/Order.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class OrderAttributes
[JsonPropertyName("status")]
[JsonProperty("status")]
public string Status { get; set; }
[JsonPropertyName("can_renew_at")]
[JsonProperty("can_renew_at")]
public string CanRenewAt { get; set; }
}

public class OrderRelationships
Expand Down
102 changes: 86 additions & 16 deletions nameshield-caplugin/NameshieldCAPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Keyfactor.AnyGateway.Extensions;
using Keyfactor.Extensions.CAPlugin.Nameshield.API;
using Keyfactor.Extensions.CAPlugin.Nameshield.Client;
using Keyfactor.Extensions.CAPlugin.Nameshield.Models;
using Keyfactor.Logging;
using Keyfactor.PKI.Enums.EJBCA;

Expand Down Expand Up @@ -36,7 +37,7 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa
_config = JsonConvert.DeserializeObject<NameshieldConfig>(rawConfig);
}

public async Task<EnrollmentResult> Enroll(string csr, string subject, Dictionary<string, string[]> san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmnentType)
public async Task<EnrollmentResult> Enroll(string csr, string subject, Dictionary<string, string[]> san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType)
{
_logger.MethodEntry(LogLevel.Debug);
NameshieldClient client = NameshieldClient.InitializeClient(_config);
Expand Down Expand Up @@ -93,17 +94,51 @@ public async Task<EnrollmentResult> Enroll(string csr, string subject, Dictionar
{
throw new Exception($"No organization provided in attributes or subject that matches an organization in the Nameshield account");
}
var lifetime = productInfo.ProductParameters[Constants.Config.Template.LIFETIME];

CertificateRequest request = new CertificateRequest();
request.data.type = "order";
request.data.attributes.action = "create";

request.data.attributes.csr = csr;
request.data.attributes.duration = "1 year";
request.data.attributes.duration = lifetime;
request.data.relationships.product.data.type = "product";
request.data.relationships.product.data.id = productId;
request.data.relationships.organization.data.type = "organization";
request.data.relationships.organization.data.id = orgId;

string orderAction = "create";
// Determine if we should do a renew or a reissue
if (enrollmentType == EnrollmentType.RenewOrReissue)
{
string priorCertSnString = productInfo.ProductParameters["PriorCertSN"];
var priorCertReqID = await _certificateDataReader.GetRequestIDBySerialNumber(priorCertSnString);
if (string.IsNullOrEmpty(priorCertReqID))
{
throw new Exception($"Cannot find prior certificate with serial number {priorCertSnString}.");
}

string orderId = priorCertReqID.Split('-')[0];

var orderResponse = Task.Run(async () => await client.GetOrderDetails(orderId)).Result;
if (orderResponse.Order.Attributes.CanRenewAt != null)
{
DateTime canRenewDT = DateTime.Parse(orderResponse.Order.Attributes.CanRenewAt);
if (canRenewDT < DateTime.UtcNow)
{
orderAction = "renew";
}
else
{
orderAction = "reissue";
}
}

request.data.relationships.certificate.data.type = "certificate";
request.data.relationships.certificate.data.id = priorCertReqID.Split('-')[1];
}

request.data.attributes.action = orderAction;

var response = Task.Run(async () => await client.RequestCertificate(request)).Result;

var status = response.Order.Attributes.Status;
Expand Down Expand Up @@ -135,11 +170,12 @@ public async Task<EnrollmentResult> Enroll(string csr, string subject, Dictionar
}
_logger.LogTrace($"Certificate enrolled successfully");

var cert = GetSingleRecord(response.Order.Relationships.Certificate.Data.id).Result;

var reqId = $"{response.Order.Id}-{response.Order.Relationships.Certificate.Data.id}";
var cert = GetSingleRecord(reqId).Result;

return new EnrollmentResult
{
CARequestID = cert.CARequestID,
CARequestID = reqId,
Certificate = cert.Certificate,
Status = cert.Status,
StatusMessage = $"Successfully enrolled for certificate {subject}"
Expand Down Expand Up @@ -191,11 +227,17 @@ public List<string> GetProductIds()
public async Task<AnyCAPluginCertificate> GetSingleRecord(string caRequestID)
{
NameshieldClient client = NameshieldClient.InitializeClient(_config);
var idParts = caRequestID.Split('-').ToList();
if (idParts.Count == 1)
{
_logger.LogWarning($"Order {caRequestID} is still awaiting delivery.");
throw new Exception($"Unable to retrieve certificate for order {caRequestID} as it is still awaiting delivery. The certificate will be picked up by a future sync once it is issued.");
}
string certId = idParts[1];
var cert = Task.Run(async () => await client.GetCertificate(certId)).Result;
var certProduct = Task.Run(async () => await client.GetCertificateProduct(certId)).Result;

var cert = Task.Run(async () => await client.GetCertificate(caRequestID)).Result;
var certProduct = Task.Run(async () => await client.GetCertificateProduct(caRequestID)).Result;

int status = ConvertToKeyfactorStatus(cert.Certificate.Attributes.Status, caRequestID);
int status = ConvertToKeyfactorStatus(cert.Certificate.Attributes.Status, certId);

return new AnyCAPluginCertificate()
{
Expand Down Expand Up @@ -224,6 +266,13 @@ public Dictionary<string, PropertyConfigInfo> GetTemplateParameterAnnotations()
Hidden = false,
DefaultValue = "",
Type = "String"
},
[Constants.Config.Template.LIFETIME] = new PropertyConfigInfo()
{
Comments = "REQUIRED:The lifetime of the certificate to be issued. This should be a string in the format recognized by Nameshield (e.g. 1 year, etc.)",
Hidden = false,
DefaultValue = "1 year",
Type = "String"
}
};
}
Expand Down Expand Up @@ -271,11 +320,13 @@ public async Task Synchronize(BlockingCollection<AnyCAPluginCertificate> blockin

foreach (var cert in certs.Certificates)
{
var certObj = Task.Run(async () => await client.GetCertificate(cert.Id)).Result;
var certProduct = Task.Run(async () => await client.GetCertificateProduct(cert.Id)).Result;
int status = ConvertToKeyfactorStatus(cert.Attributes.Status, cert.Id);
var newCert = new AnyCAPluginCertificate()
{
CARequestID = cert.Id,
CARequestID = $"{certObj.Certificate.Relationships.Order.Data.id}-{cert.Id}",
OldCARequestID = certObj.Certificate.Relationships.Order.Data.id,
ProductID = certProduct.CertificateProduct.Attributes.Name,
Status = status,
Certificate = cert.Attributes.Pem,
Expand Down Expand Up @@ -310,20 +361,39 @@ public async Task ValidateProductInfo(EnrollmentProductInfo productInfo, Diction

var productList = Task.Run(async () => await localClient.ListProducts()).Result;

bool foundProduct = false;
Product foundProduct = null;
foreach (var product in productList.Products)
{
if (string.Equals(product.Attributes.Name, productInfo.ProductID, StringComparison.OrdinalIgnoreCase))
{
foundProduct = true;
foundProduct = product;
break;
}
}

if (!foundProduct)
if (foundProduct == null)
{
throw new Exception($"Product ID {productInfo.ProductID} was not found in the list of valid products.");
}
string lifetime = productInfo.ProductParameters[Constants.Config.Template.LIFETIME];
if (string.IsNullOrEmpty(lifetime))
{
throw new Exception($"Product ID {productInfo.ProductID} is missing a required lifetime value");
}

bool allowedDuration = false;
foreach (var duration in foundProduct.Attributes.Durations)
{
if (string.Equals(duration, lifetime, StringComparison.OrdinalIgnoreCase))
{
allowedDuration = true;
break;
}
}
if (!allowedDuration)
{
throw new Exception($"Product ID {productInfo.ProductID} has an invalid lifetime value {lifetime}. Valid values for this product are: {string.Join(',', foundProduct.Attributes.Durations)}");
}
}

private static string ValidateConfigurationKey(Dictionary<string, object> connectionInfo, string key)
Expand All @@ -336,7 +406,7 @@ private static string ValidateConfigurationKey(Dictionary<string, object> connec
return string.Empty;
}

private static int ConvertToKeyfactorStatus(string status, string sslId)
private static int ConvertToKeyfactorStatus(string status, string certId)
{
switch (status.ToUpper())
{
Expand All @@ -353,7 +423,7 @@ private static int ConvertToKeyfactorStatus(string status, string sslId)
return (int)EndEntityStatus.REVOKED;

default:
throw new Exception($"Request ID {sslId} has unknown status {status}");
throw new Exception($"Request ID {certId} has unknown status {status}");
}
}

Expand Down
2 changes: 1 addition & 1 deletion nameshield-caplugin/nameshield-caplugin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Keyfactor.AnyGateway.IAnyCAPlugin" Version="3.0.0" />
<PackageReference Include="Keyfactor.AnyGateway.IAnyCAPlugin" Version="3.2.0" />
<PackageReference Include="Keyfactor.Common" Version="2.5.0" />
<PackageReference Include="Keyfactor.Logging" Version="1.1.1" />
<PackageReference Include="Keyfactor.PKI" Version="5.5.0" />
Expand Down
Loading