← Week 2: ACM Private CA — architecture + API

Day 11: IssueCertificate — The Core Issuance Flow

Phase 5 · September 5, 2026

← Week 2: ACM Private CA — architecture + API

Agenda (2–3 hours)

  • Read (30 min): ACM PCA IssueCertificate API; GetCertificate polling pattern
  • Build (90 min): Rust code for the full issuance flow in your provisioning service
  • Test (30 min): Issue a real cert and parse it with x509-parser
← Week 2: ACM Private CA — architecture + API

The Issuance Flow

Caller (device / service)           Provisioning Service         ACM PCA
    │                                       │                       │
    │── POST /provision {csr, device_id} ──►│                       │
    │                                       │── IssueCertificate ──►│
    │                                       │                       │ (async: HSM signs)
    │                                       │◄── {certificateArn} ──│
    │                                       │                       │
    │                                       │── GetCertificate ────►│
    │                                       │◄── {cert PEM} ────────│
    │                                       │                       │
    │◄── 200 {cert PEM} ────────────────────│                       │

IssueCertificate is asynchronous — it returns a certificateArn immediately,
and the actual certificate is available after polling GetCertificate.

← Week 2: ACM Private CA — architecture + API

IssueCertificate API Call

pub async fn issue_cert(
    client: &aws_sdk_acmpca::Client,
    ca_arn: &str,
    csr_pem: &[u8],
    validity_days: i64,
) -> anyhow::Result<String> {
    let resp = client
        .issue_certificate()
        .certificate_authority_arn(ca_arn)
        .csr(aws_smithy_types::Blob::new(csr_pem))
        .signing_algorithm(SigningAlgorithm::Sha256Withecdsa)
        .template_arn(
            "arn:aws:acm-pca:::template/EndEntityCertificate/V1"
        )
        .validity(
            Validity::builder()
                .r#type(ValidityPeriodType::Days)
                .value(validity_days)
                .build()?
        )
        .send()
        .await?;

    let cert_arn = resp.certificate_arn()
        .ok_or_else(|| anyhow::anyhow!("no certificate ARN returned"))?;

    Ok(cert_arn.to_string())
}
← Week 2: ACM Private CA — architecture + API

Polling GetCertificate

ACM PCA issues certificates asynchronously. Poll until the cert is ready:

pub async fn get_cert_with_retry(
    client: &aws_sdk_acmpca::Client,
    ca_arn: &str,
    cert_arn: &str,
) -> anyhow::Result<String> {
    for attempt in 0..10 {
        match client
            .get_certificate()
            .certificate_authority_arn(ca_arn)
            .certificate_arn(cert_arn)
            .send()
            .await
        {
            Ok(resp) => {
                let pem = resp.certificate()
                    .ok_or_else(|| anyhow::anyhow!("no cert in response"))?;
                return Ok(pem.to_string());
            }
            Err(e) if is_request_in_progress(&e) => {
                tokio::time::sleep(Duration::from_millis(250 * 2u64.pow(attempt))).await;
            }
            Err(e) => return Err(e.into()),
        }
    }
    Err(anyhow::anyhow!("timed out waiting for certificate"))
}

fn is_request_in_progress(e: &aws_sdk_acmpca::Error) -> bool {
    matches!(e, aws_sdk_acmpca::Error::RequestInProgressException(_))
}
← Week 2: ACM Private CA — architecture + API

Generating a CSR in Rust

Before calling IssueCertificate, the caller must provide a CSR.
Your provisioning service might generate the key + CSR on behalf of the device:

use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType};

pub fn generate_csr(
    common_name: &str,
    dns_sans: &[&str],
) -> anyhow::Result<(String, rcgen::KeyPair)> {
    let mut params = CertificateParams::new(dns_sans.to_vec())?;
    params.distinguished_name = DistinguishedName::new();
    params.distinguished_name.push(DnType::CommonName, common_name);

    let key_pair = rcgen::KeyPair::generate()?;
    let cert = params.serialize_request(&key_pair)?;

    Ok((cert.pem(), key_pair))
}

Security note: if the provisioning service generates the private key,
it must securely transmit it to the device. The device should ideally
generate its own key and send only the CSR (which is the standard pattern).

← Week 2: ACM Private CA — architecture + API

Parsing the Issued Certificate

Verify what ACM PCA actually issued using x509-parser:

use x509_parser::prelude::*;
use pem::parse;

pub fn inspect_cert(cert_pem: &str) -> anyhow::Result<()> {
    let pem = parse(cert_pem)?;
    let (_, cert) = X509Certificate::from_der(&pem.contents())?;

    println!("Subject:    {}", cert.subject());
    println!("Issuer:     {}", cert.issuer());
    println!("Not before: {}", cert.validity().not_before);
    println!("Not after:  {}", cert.validity().not_after);

    if let Ok(Some(san)) = cert.subject_alternative_names() {
        for name in &san.value.general_names {
            println!("SAN: {:?}", name);
        }
    }

    if let Some(eku) = cert.extended_key_usage()? {
        println!("EKU: serverAuth={} clientAuth={}",
            eku.value.server_auth, eku.value.client_auth);
    }

    Ok(())
}
← Week 2: ACM Private CA — architecture + API

Idempotency and the idempotencyToken

IssueCertificate accepts an optional idempotencyToken:

client
    .issue_certificate()
    // ...
    .idempotency_token(format!("device-{device_id}-{timestamp}"))
    .send()
    .await?;

If the same token is submitted twice within 5 minutes, ACM PCA returns
the same certificate ARN without issuing a new cert. Use this to:

  • Handle retry on network failure (same token → same cert)
  • Prevent double-issuance when your Lambda retries on failure

For your provisioning service: derive the idempotency token from the
device ID + a request timestamp (rounded to the minute).

← Week 2: ACM Private CA — architecture + API

Challenge Assignment

Implement the full issuance flow in Rust:

  1. generate_csr(common_name, dns_sans) → CSR PEM + KeyPair
  2. issue_cert(ca_arn, csr_pem, validity_days) → certificateArn
  3. get_cert_with_retry(ca_arn, cert_arn) → certificate PEM
  4. inspect_cert(cert_pem) → prints subject, validity, SANs, EKU

Test against a real ACM PCA CA (use the AWS test/sandbox environment or
a development CA in your AWS account, if available).

If no ACM PCA access: mock the API with a local rcgen CA and simulate
the IssueCertificate / GetCertificate round-trip in memory.

Add the issuance flow description to acm-pca-design.md §3.

← Week 2: ACM Private CA — architecture + API

Resources

  • IssueCertificate API: docs.aws.amazon.com/privateca/latest/APIReference/API_IssueCertificate.html
  • GetCertificate API: docs.aws.amazon.com/privateca/latest/APIReference/API_GetCertificate.html
  • aws-sdk-acmpca Rust: docs.rs/aws-sdk-acmpca
  • ACM PCA SDK example: github.com/awslabs/aws-sdk-rust/tree/main/examples/acm-pca
  • Idempotency tokens: docs.aws.amazon.com/privateca/latest/userguide/PcaIssueCert.html