EZCA is the first Cloud Certificate Authority build by developers for developers. We understand the importance of automation and have built our certificate authority API to be as simple as possible. To help you get started, other than supporting some usual PKI standards such as SCEP (SCEP Sample Code), ACME, and EST (EST Sample code) we have also created a swagger documentation that you can use to test our API, full documentation for IoT Certificate Issuance, and even a NuGet Package with sample code to help you issue certificates as quickly as possible. In this page we will focus on our own API and how you can use it to issue certificates.
The easiest way to issue certificates through the API is using our NuGet Package and sample code but let’s understand how it works.
To authenticate with EZCA, we use Entra ID if you are automating it, you can use either a service principal or an MSI one of the cool things of the Azure.Identity nuget package is that in testing (or if your computer is logged in to the Azure CLI, you can use your own credentials). Since it is Entra ID, you can use the same token you would use for any other Azure API.
// Option 1: Use the Azure.Identity NuGet package With DefaultAzureCredential
var azureTokenCredential = new DefaultAzureCredential(); // for MSI and User Testing
// Option 2: Use the Azure.Identity NuGet package With ClientCertificateCredential
var azureTokenCredential = new ClientCertificateCredential(tenantID, appID, certificate); // for Service Principal with certificate authentication where:
// tenantID is the Azure Tenant ID
// appID is the Azure Application ID
// the certificate is a X509Certificate2
TokenRequestContext authContext =
new(new[] { "https://management.core.windows.net/.default" });
string token = (await azureTokenCredential.GetTokenAsync(authContext, default)).Token;
if (string.IsNullOrWhiteSpace(_token.Token))
{
throw new AuthenticationFailedException("Error getting token");
}
But if you are just using the NuGet Packet, the authentication will be taken care of for you. All you have to do is instantiate the client, and you can pass your own token provider if you are using certificate authentication:
IEZCAClient ezcaClient = new EZCAClientClass(
new HttpClient(),
logger, // (optional) ILogger?
baseUri, // (optional) string baseUri by default it will use "https://portal.ezca.io/" but if you are using one of our other instances, you can pass the baseUri here
TokenCredential tokenProvider // (optional) TokenCredential you can pass your own token provider if you are using certificate or password based authentication, if left empty it will use the DefaultAzureCredential
);
The fist step is to get the CA ID and the Template ID, you will need this for all the steps below. You can get them by calling the GetAvailableCAsAsync()
method from the NuGet Package which calls the /api/CA/GetAvailableSSLCAs
endpoint and returns an array of AvailableCAModel.
AvailableCAModel[]? availableCAs = await ezcaClient.GetAvailableCAsAsync();
if (availableCAs == null || availableCAs.Length == 0)
{
throw new Exception("No CAs available");
}
[
{
"TenantID": "string",
"CAID": "string",
"CAFriendlyName": "string",
"CATemplateType": "string",
"TemplateID": "string",
"TemplateName": "string",
"DualKeyRequired": true,
"AllowWildCardDomains": true,
"AllowAllOrgAsRequesters": true,
"KeyUsage": "string",
"MaxCertLifeDays": 0,
"DomainRestrictions": "string",
"ApprovedAADRequesters": [
{
"ObjectId": "string",
"FriendlyName": "string",
"ObjectType": "string",
"isValid": true
}
],
"Approvers": [
{
"ObjectId": "string",
"FriendlyName": "string",
"ObjectType": "string",
"isValid": true
}
],
"AllowedDomains": [
{
"Success": true,
"Message": "string"
}
],
"CAKeyType": "string",
"CAHashing": "string",
"EKUs": [
"string"
],
"CustomEKUs": [
{
"Oid": "string",
"FriendlyName": "string",
"Selected": true
}
],
"ScepUserName": "string",
"SCEPDynamicChallenge": true,
"SCEPStaticChallenge": true,
"SCEPSelfService": true,
"SCEPChallengeLength": 0,
"SCEPSecret": "string",
"SCEPUserPassword": "string",
"scepHashingAllowed": [
{
"Success": true,
"Message": "string"
}
],
"SCEPEncryptionAllowed": [
{
"Success": true,
"Message": "string"
}
],
"SCEPSelfServiceProfiles": [
{
"SelfServiceProfile": {
"TenantID": "string",
"CaID": "string",
"TemplateID": "string",
"ProfileID": "string",
"allUsers": true,
"MultipleDevices": true,
"otherTenants": true,
"otherTenantsIds": "string",
"subjectName": "string",
"subjectAltNames": "string",
"durationInDays": 0,
"Ekus": "string",
"KeyUsages": "string",
"PolicyName": "string",
"EncryptionKeyLocation": "string",
"BehalfOfAgents": "string"
},
"GuestProfiles": [
{
"GuestProfile": {
"TenantID": "string",
"CaTenantID": "string",
"CaID": "string",
"TemplateID": "string",
"ProfileID": "string",
"allUsers": true,
"PolicyName": "string"
},
"Acls": [
{
"TenantID": "string",
"CaID": "string",
"TemplateID": "string",
"ProfileID": "string",
"ObjectID": "string",
"objectType": "string",
"objectName": "string",
"ObjectTenantID": "string"
}
]
}
],
"Acls": [
{
"TenantID": "string",
"CaID": "string",
"TemplateID": "string",
"ProfileID": "string",
"ObjectID": "string",
"objectType": "string",
"objectName": "string",
"ObjectTenantID": "string"
}
],
"Sans": [
{
"SanType": "string",
"SanValue": "string"
}
],
"EKUs": [
{
"Oid": "string",
"FriendlyName": "string",
"Selected": true
}
],
"KeyUsages": [
{
"Success": true,
"Message": "string"
}
]
}
]
}
]
Now that we have our token and the CAID and Tenant ID (If it is always the same CA, those values can be hardcoded no need to get the CAs on each time we are getting a new certificate), we can issue a certificate. Each of the domains in the certificate will be registered to the identity that issued the certificate. If you want to instead register the domain with more identities and detailed RBAC, you Can use the RegisterDomainAsync
method from the NuGet Package which calls the /api/CA/RegisterNewDomain
endpoint.
//This is optional if you want to register the domain (or domains) with more identities and detailed RBAC
AADObjectModel graphUser1 =
new("600b1201-8a0d-4a82-8904-351f00f796df", // the object id of the user
"eapttls@codingflamingogmail.onmicrosoft.com", // the email of the user
"User"); // the type of the object
AADObjectModel graphGroup =
new("200b1201-8a0d-4a82-8904-351f00f796df", "requesters-group", "Group");
List<string> extraEmails = ["security@keytos.io"]; // notification emails where alerts about the certificate will be sent
string domain = "myiotdevice";
APIResultModel registrationResult = await ezcaClient.RegisterDomainAsync(
availableCAs[0],// the CA we are going to use
domain,
[graphUser1], //owners (these manage who can issue certificates for this domain), leaving null it uses current user.
[graphUser1], //certificate administrators (who can request and revoke certificates) leaving null it uses current user.
[graphGroup],//requesters only, these accounts can only request certificates
extraEmails
);
if (!registrationResult.Success)
{
Console.WriteLine($"Could not register new device in EZCA {registrationResult.Message}");
return;
}
{
"Success": true,
"Message": "string"
}
Now to Issue the certificate, we use RequestCertificateAsync
method which calls the /api/CA/RequestSSLCertificate
and returns the certificate that was issued. Or if you want the full chain (the certificate, and the certificates of the Certificate Authorities that Issued the certificate) You can call RequestCertificateWithChainV2Async
method which calls the /api/CA/RequestSSLCertificateV2
endpoint.
//This first certificate request example we will let the EZClient library create the CSR (Certificate Signing Request)
//for us.
Console.WriteLine($"Requesting Certificate");
X509Certificate2? firstCert = await ezcaClient.RequestCertificateAsync(availableCAs[0], domain, 20);
//Now I will create a CSR using the Windows Store and use that csr to request a certificate
//(NOTE: comment this code if running on Mac or Linux)
List<string> subjectAltNames = new List<string> { domain };
CX509CertificateRequestPkcs10 certRequest = WindowsCertStoreService.CreateCSR(
"CN=" + domain,
subjectAltNames,
4096 // the key size
);
string csr = certRequest.RawData[EncodingType.XCN_CRYPT_STRING_BASE64REQUESTHEADER];
Console.WriteLine($"Getting Windows Certificate");
X509Certificate2? windowsCert = await ezcaClient.RequestCertificateAsync(
availableCAs[0],
csr,
domain,
20 // the number of days the certificate will be valid
);
if (windowsCert != null)
{
Console.WriteLine($"Installing Windows Certificate");
WindowsCertStoreService.InstallCertificate(
CryptoStaticService.ExportToPEM(windowsCert),
certRequest
);
}
{
"Success": true,
"Message": "string"
}
//create a 4096 RSA key
RSA key = RSA.Create(4096);
//create Certificate Signing Request
X500DistinguishedName x500DistinguishedName = new("CN=" + domain);
CertificateRequest certificateRequest =
new(x500DistinguishedName, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
string csr = CryptoStaticService.PemEncodeSigningRequest(certificateRequest);
List<SubjectAltValue> subjectAlternateNames = [
new(domain), // a string will default to 2 which is a DNS name Subject Alternate Name
new("1.1.1.1", 7) // passing a 7 will make it an IP address Subject Alternate Name
];
//request the certificate
CertificateCreatedResponse? certificateWithChain = await ezcaClient.RequestCertificateWithChainV2Async(
availableCAs[0],
csr,
domain,
20, // the number of days the certificate will be valid
subjectAlternateNames
);
string pemCert = Regex.Replace(certificateWithChain.CertificatePEM, @"-----[a-z A-Z]+-----", "").Trim();
X509Certificate2 fullCertificate = (new X509Certificate2(Convert.FromBase64String(pemCert))).CopyWithPrivateKey(key);
{
"CertificatePEM ": "string",
"IssuingCACertificate ": "string",
"RootCertificate": "string"
}
One of the cool things for our API, is that once you create your certificate you can use your own certificate to renew itself, meaning that if you are using this for IoT devices, you can have the device renew its own certificate without entra ID. Our Nuget package automatically will create a token using the certificate that works for renewals but if you are writing your own, you can see how we do it in the sample code below:
private static TokenModel CreateRSAJWTToken(X509Certificate2 clientCertificate)
{
if (clientCertificate.GetRSAPublicKey() == null)
{
throw new ArgumentException(
"Only RSA certificates are supported for certificate based authentication"
);
}
var headers = new Dictionary<string, object>
{
{ "typ", "JWT" },
{ "x5t", clientCertificate.Thumbprint }
};
TokenModel token = new();
var payload = new Dictionary<string, object>()
{
{ "aud", $"https://ezca.io" },
{ "jti", Guid.NewGuid().ToString() },
{ "nbf", (ulong)token.NotBefore.ToUnixTimeSeconds() },
{ "exp", (ulong)token.ExpiresOn.ToUnixTimeSeconds() }
};
token.AccessToken = JWT.Encode(
payload,
clientCertificate.GetRSAPrivateKey(),
JwsAlgorithm.RS256,
extraHeaders: headers
);
return token;
}
As mentioned before, the NuGet package will take care of this for you so we only have to pass the old certificate and a csr to the RenewCertificateAsync
method which calls the /api/Certificates/RenewCertificate
endpoint.
RSA key = RSA.Create(4096);
//create Certificate Signing Request
X500DistinguishedName x500DistinguishedName = new("CN=" + domain);
CertificateRequest certificateRequest =
new(x500DistinguishedName, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
csr = CryptoStaticService.PemEncodeSigningRequest(certificateRequest);
Console.WriteLine($"Renewing certificate");
string newCert = await ezcaClient.RenewCertificateAsync(firstCert, csr);
X509Certificate2 certificate = CryptoStaticService.ImportCertFromPEMString(newCert);
X509Certificate2 certificateWithPrivateKey = certificate.CopyWithPrivateKey(key);
Console.WriteLine("Finished renewing certificate");
And as simple as that, you can issue certificates through the API. If you want to learn more about how to use the API, you can check the Swagger Documentation or the NuGet Package and sample code. If you have any questions, you can always reach out to us, and with our professional services we can help you integrate it with your existing systems.