How To Issue X.509 Certificates Through API

Introduction How Can I Issue Certificates Through API?

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.

How To Issue X509 Certificates in C# Using EZCA’s API

The easiest way to issue certificates through the API is using our NuGet Package and sample code but let’s understand how it works.

How To Authenticate

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
);

Getting the CA ID and Template ID

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");
}

Sample JSON Response:

[
  {
"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"
  }
]
  }
]
  }
]

How To Issue A Certificate Using EZCA’s API and Entra ID

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;
}

Sample JSON Response:

{
  "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
    );
}

Sample JSON Response (The Message is the base64 encoded certificate):

{
  "Success": true,
  "Message": "string"
}

How To Create a Certificate using the Full Chain API

//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);

Sample JSON Response:

{
  "CertificatePEM ": "string",
  "IssuingCACertificate ": "string",
  "RootCertificate": "string"
}

How to Renew a Certificate

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:

How to generate a JWT token for EZCA PKI using a certificate

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;
}

How to renew a certificate using itself to authenticate

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.