Software supply chain attacks have grown in both frequency and sophistication. Recent high-profile compromises of build systems and open source dependencies demonstrate that attackers increasingly target the trust mechanisms that verify software integrity. Those trust mechanisms rely on digital signature algorithms, and today many use traditional algorithms such as Rivest-Shamir-Adleman (RSA) and the Elliptic Curve Digital Signature Algorithm (ECDSA). If a sufficiently powerful quantum computer could forge these signatures, an attacker could potentially inject unauthorized code into signed artifacts, fabricate build attestations, or impersonate legitimate publishers. Using quantum-resistant signatures today defends against threats that might emerge in the future.

Since 2025, the Commercial National Security Algorithm Suite 2.0 (CNSA 2.0), published by the US National Security Agency (NSA), has required US National Security Systems (NSS) to “support and prefer” quantum-resistant signatures for software and firmware, with exclusive use mandated by 2030. The EU Cyber Resilience Act (CRA) takes full effect in December 2027. It requires manufacturers to maintain verifiable software integrity throughout a product’s lifecycle. Organizations that ship software to US federal agencies or sell products in the European market face converging deadlines that demand action now.

Amazon Web Services (AWS) provides the building blocks to construct a quantum-resistant software supply chain today using production-ready, generally available services. In this post, I show you how to combine Module-Lattice-Based Digital Signature Algorithm (ML-DSA) keys in AWS Key Management Service (AWS KMS), ML-DSA certificate hierarchies in AWS Private Certificate Authority, and ML-DSA trust anchors in AWS Identity and Access Management (IAM) Roles Anywhere into an end-to-end signing architecture for binaries, container images, and software bills of materials (SBOMs).

This post is Part 1 of a two-part series on adopting ML-DSA signatures, standardized in NIST FIPS 204 (ML-DSA) signatures, on AWS. As of Oct 2026, this series is the first end-to-end guide from any cloud provider for post-quantum signing across the software supply chain, from establishing an ML-DSA root of trust to enforcing algorithm policy across an organization. This part is for builders and walks through constructing and operating the signing pipeline. Part 2, Enforce your ML-DSA algorithm policy with SCPs and IAM, is for security and governance teams and shows how to turn the algorithm choices made here into preventive and detective controls across an organization.

Understanding the harvest now, forge later threat

After a cryptographically relevant quantum computer (CRQC) exists, the mathematical problems protecting today’s public-key cryptography can be solved efficiently. Attacks that are impractical today because of the computational complexity suddenly become practical using a CRQC. Most discussions of post-quantum cryptography (PQC) focus on the harvest now, decrypt later threat to data confidentiality. The asymmetric keys protecting data in transit can be efficiently derived using a CRQC, exposing the data that was meant to be protected. Digital signatures rely on the same asymmetric mathematical problems that a CRQC would break. This means that software supply chains face a parallel threat that targets integrity rather than confidentiality. An attacker who records signed artifacts today can forge those signatures in the future on a totally different artifact, using a CRQC.

Consider firmware signed with ECDSA P-256 and deployed to a fleet of embedded devices in 2026. These devices verify the firmware signature on every boot for their 15-year operational lifetime and can’t be recalled to re-sign after deployment. If a CRQC becomes operational in 2035, an attacker could produce a valid-looking signature over tampered firmware, and every device in the fleet would accept it as authentic on next restart. For organizations that rely on classical digital signatures as the primary mechanism for verifying software integrity, this weakens a critical link in the verification chain. This is why CNSA 2.0 specifies aggressive timelines for digital signatures: the artifacts you sign today must remain trustworthy for their full operational lifetime.

ML-DSA is designed to resist attacks from both classical and quantum computers. AWS KMS, AWS Private CA, and AWS IAM Roles Anywhere all support ML-DSA, making it possible to address the forge-later threat across your supply chain today without additional tooling.

Architecture overview: Five pillars of a quantum-resistant supply chain

The architecture comprises five trust pillars that together protect your software delivery pipeline:

  1. ML-DSA key management in AWS KMS – All signing keys are generated and protected in FIPS 140-3 Security Level 3 hardware security modules (HSMs). Private keys never leave the HSM boundary.
  2. ML-DSA public key infrastructure (PKI) through AWS Private CA – A certificate hierarchy (root CA, subordinate CA, end-entity certificates) binds ML-DSA public keys to identities, establishing who signed what. CA private keys are generated and stored in hardware-protected HSMs managed by the service and never leave the HSM boundary.
  3. Quantum-resistant workload identity (for external build agents) – For continuous integration and delivery (CI/CD) agents running outside AWS that authenticate through X.509 certificates, IAM Roles Anywhere with ML-DSA certificates ties the signer’s identity back to the same quantum-resistant root of trust.
  4. ML-DSA artifact signing – Build outputs (binaries, container image manifests, packages) are signed using AWS KMS ML-DSA keys. Signatures are stored alongside artifacts in Amazon Simple Storage Service (Amazon S3) or Amazon Elastic Container Registry (Amazon ECR).
  5. ML-DSA SBOM integrity attestation – The SBOM for each release is wrapped in an in-toto Statement that names the artifact it describes by digest, and the Statement is signed with the same ML-DSA code-signing key. This provides quantum-resistant proof that the dependency manifest hasn’t been tampered with and that it belongs to the artifact it accompanies.

These pillars create a chain of trust: AWS Private CA protects the root CA key in its own HSMs, issues certificates that bind identities to AWS KMS code-signing keys, IAM Roles Anywhere uses those certificates to authenticate build agents, and authenticated agents sign every artifact and SBOM they produce.

Figure 1: End-to-end quantum-resistant software supply chain on AWS

Figure 1: End-to-end quantum-resistant software supply chain on AWS

Figure 1 illustrates this architecture in four layers, numbered in the order you build them:

  1. Root of trust (Pillars 1 and 2) – Establish your AWS Private CA hierarchy and create your code-signing key in AWS KMS.
  2. Workload identity (Pillar 3) – Register the CA as an IAM Roles Anywhere trust anchor and issue ML-DSA certificates to build agents, ensuring the signer’s identity chains back to the same quantum-resistant root of trust.
  3. Artifact signing (Pillars 4 and 5) – Sign binaries, container images, and SBOMs using the authenticated build agent.
  4. Verification and enforcement – Validate signatures at deployment and enforce ML-DSA-only policies.

Prerequisites

To follow along, you need:

  • An AWS account with permissions to create resources in AWS KMS, AWS Private CA, IAM Roles Anywhere, and Amazon ECR.
  • AWS Command Line Interface (AWS CLI) v2 configured with appropriate credentials.
  • IAM Roles Anywhere credential helper (aws_signing_helper) v1.8.0 or later, the first release that supports ML-DSA keys and certificates, for workload authentication. Download from the IAM Roles Anywhere documentation.
  • OpenSSL 3.5 or later (required for ML-DSA signature verification and mu computation).
  • ORAS CLI v1.2 or later (for attaching Open Container Initiative (OCI) referrer artifacts to container images). Alternatively, you can use the Notation CLI with a custom plugin or any OCI-compliant client that supports the OCI Referrers API.
  • jq, for constructing and parsing the in-toto Statements used for SBOM attestation and for parsing ORAS output.
  • Amazon Inspector SBOM Generator (Sbomgen), or another tool that produces a CycloneDX JSON SBOM, for the SBOM example. Sbomgen runs only on Linux (x86-64 and ARM64), so run that step on a Linux build agent or container. It uses the registry credentials that oras login stores in the Docker config file, and it also accepts them through --username or the INSPECTOR_SBOMGEN_USERNAME and INSPECTOR_SBOMGEN_PASSWORD environment variables.
  • Java 11 or later and Apache Maven, to run the diy-code-signing-kms-private-ca sample that produces the code-signing CSR in Step 4 of Building the ML-DSA PKI foundation.
  • A container image pushed to Amazon ECR (for the container signing example).

Building the ML-DSA PKI foundation

Start by creating the certificate hierarchy that will anchor your supply chain trust. AWS Private CA generates and stores its own CA signing keys internally in hardware-protected HSMs when you create a certificate authority. Use ML-DSA-87 (NIST security category 5, defined as at least as hard to break as exhaustive key search on AES-256) for the root CA because it protects the longest-lived trust anchor. Use ML-DSA-65 (NIST security category 3) for the subordinate CA to balance security with smaller signature sizes for frequently issued certificates.

The parameter sets used here, ML-DSA-87 and ML-DSA-65, are also the approved set in the example algorithm policy that Part 2 of this series turns into enforceable controls. If your organization has standardized on a different set, substitute it in the commands that follow; the build steps don’t change.

Step 1: Create the root CA in AWS Private CA

When you specify KeyAlgorithm: ML_DSA_87, Private CA generates an ML-DSA-87 key pair internally. You don’t need to create a separate AWS KMS key for the CA.

aws acm-pca create-certificate-authority \ --certificate-authority-configuration '{ "KeyAlgorithm": "ML_DSA_87", "SigningAlgorithm": "ML_DSA_87", "Subject": { "CommonName": "PQC Supply Chain Root CA", "Organization": "Example Corp", "Country": "US" } }' \ --certificate-authority-type ROOT

After creation, the root CA is in PENDING_CERTIFICATE state. Activate it by self-signing its certificate. This is where you set the root CA’s validity period (10 years is typical for a root):

# Retrieve the root CA's CSR aws acm-pca get-certificate-authority-csr \ --certificate-authority-arn <root-ca-arn> \ --output text > root-ca.csr # Self-sign the root CA certificate (valid for 10 years) ROOT_CERT_ARN=$(aws acm-pca issue-certificate \ --certificate-authority-arn <root-ca-arn> \ --csr fileb://root-ca.csr \ --signing-algorithm ML_DSA_87 \ --validity Value=3650,Type=DAYS \ --template-arn arn:aws:acm-pca:::template/RootCACertificate/V1 \ --query CertificateArn --output text) # Retrieve and install the self-signed root certificate aws acm-pca get-certificate \ --certificate-authority-arn <root-ca-arn> \ --certificate-arn $ROOT_CERT_ARN \ --query Certificate --output text > root-ca-cert.pem aws acm-pca import-certificate-authority-certificate \ --certificate-authority-arn <root-ca-arn> \ --certificate fileb://root-ca-cert.pem

Step 2: Create a subordinate CA for code signing

aws acm-pca create-certificate-authority \ --certificate-authority-configuration '{ "KeyAlgorithm": "ML_DSA_65", "SigningAlgorithm": "ML_DSA_65", "Subject": { "CommonName": "PQC Code Signing CA", "Organization": "Example Corp", "Country": "US" } }' \ --certificate-authority-type SUBORDINATE

After creation, the subordinate CA enters PENDING_CERTIFICATE state with no link to the root. Activate it by signing its certificate signing request (CSR) with the root CA:

# Retrieve the subordinate CA's certificate signing request aws acm-pca get-certificate-authority-csr \ --certificate-authority-arn <sub-ca-arn> \ --output text > sub-ca.csr # Sign the CSR using the root CA (this establishes the trust hierarchy) SUB_CERT_ARN=$(aws acm-pca issue-certificate \ --certificate-authority-arn <root-ca-arn> \ --csr fileb://sub-ca.csr \ --signing-algorithm ML_DSA_87 \ --validity Value=1825,Type=DAYS \ --template-arn arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1 \ --query CertificateArn --output text) # Retrieve the signed certificate and import it to activate the subordinate CA aws acm-pca get-certificate \ --certificate-authority-arn <root-ca-arn> \ --certificate-arn $SUB_CERT_ARN \ --query Certificate --output text > sub-ca-cert.pem aws acm-pca import-certificate-authority-certificate \ --certificate-authority-arn <sub-ca-arn> \ --certificate fileb://sub-ca-cert.pem \ --certificate-chain fileb://root-ca-cert.pem

Step 3: Create the code-signing key in AWS KMS

Your build agents use this key to sign artifacts, so their IAM roles need the kms:Sign permission on it. The end-entity certificate issued by your subordinate CA binds an identity to this key’s public key.

aws kms create-key \ --key-spec ML_DSA_65 \ --key-usage SIGN_VERIFY \ --description "PQC code-signing key for supply chain" \ --tags TagKey=Purpose,TagValue=CodeSigning

Create an alias for convenient reference in your CI/CD scripts:

aws kms create-alias \ --alias-name alias/pqc-code-signing \ --target-key-id <key-id-from-above>

Step 4: Issue the end-entity code signing certificate

To issue a certificate, you need a CSR. Because the ML-DSA private key never leaves the AWS KMS HSM, you can’t generate the CSR locally with OpenSSL. Instead, you construct the CSR’s TBS (to-be-signed) structure containing your subject name and public key, sign it remotely by calling aws kms sign, and assemble the final CSR from those components.

For a working implementation that assembles an ML-DSA CSR around a KMS-managed private key and signs it with AWS KMS, see Post-quantum (ML-DSA) code signing with AWS Private CA and AWS KMS and its diy-code-signing-kms-private-ca sample on GitHub (Java, built with Maven). There is no AWS CLI-only path for this step: the CSR must be constructed and signed in code, and the sample is the reference implementation.

After you have the code-signing.csr file, issue the certificate from your subordinate CA:

# Issue a code-signing certificate valid for one year, signed by the subordinate CA using ML-DSA-65. # Replace <sub-ca-arn> with the ARN that create-certificate-authority returned for the subordinate CA in Step 2. CODE_SIGNING_CERT_ARN=$(aws acm-pca issue-certificate \ --certificate-authority-arn <sub-ca-arn> \ --csr fileb://code-signing.csr \ --signing-algorithm ML_DSA_65 \ --validity Value=365,Type=DAYS \ --template-arn arn:aws:acm-pca:::template/CodeSigningCertificate/V1 \ --query CertificateArn --output text) # Retrieve the issued certificate aws acm-pca get-certificate \ --certificate-authority-arn <sub-ca-arn> \ --certificate-arn $CODE_SIGNING_CERT_ARN \ --query Certificate --output text > code-signing-cert.pem

This certificate binds the signer identity to the AWS KMS code-signing key material. Note that the certificate validity period (365 days in this example) determines how long verifiers can validate signatures by checking the certificate alone. AWS Private CA lets you choose the validity period of an issued certificate, as long as it ends no later than the issuing CA’s own certificate; in this example the subordinate CA certificate from Step 2 is valid for 1,825 days, so the three-year option below fits inside it. For artifacts that must remain verifiable beyond the certificate’s expiry, consider these approaches:

  • Longer certificate validity – For private PKI, code-signing certificates are commonly issued with multi-year validity (for example, Value=1095,Type=DAYS for 3 years) because you control the issuance policy.
  • Trusted timestamping (RFC 3161) – Countersign each signature with an RFC 3161 timestamp from a Time Stamping Authority (TSA) at signing time. The timestamp proves the signature was created while the certificate was valid, allowing verification indefinitely. AWS doesn’t provide a managed TSA; use a third-party TSA or operate your own.
  • Re-signing – Before a certificate expires, re-sign active artifacts with a newly issued certificate.

Distribute the certificate chain (root CA certificate, subordinate CA certificate, and end-entity certificate) to systems that need to verify your signatures.

Signing artifacts with ML-DSA in your CI/CD pipeline

With the PKI hierarchy in place, integrate ML-DSA signing into your build process. The approach depends on artifact type. ML-DSA-65 signatures are 3,309 bytes (compared to 64 bytes for ECDSA P-256), and the latency of the AWS KMS Sign operation for ML-DSA is comparable to existing asymmetric operations. The larger signature size has negligible impact when stored alongside multi-megabyte container images or packages, but is worth accounting for in size-constrained environments like embedded device images transmitted over low-bandwidth links.

Binary and package signing

Most build outputs, including compiled binaries and packages, are far larger than the 4,096-byte limit that AWS KMS places on the message passed to aws kms sign. Make the EXTERNAL_MU method your pipeline’s default: compute the 64-byte message representative (mu) externally with OpenSSL 3.5, then pass only mu to AWS KMS. This handles an artifact of any size and never needs to change as your artifacts grow. AWS KMS also accepts content directly with --message-type RAW, up to 4,096 bytes. Use RAW only for inputs whose size is fixed by construction, such as the 71-character manifest digest in the container image example that follows. An artifact that fits today is likely to outgrow the limit later, and a pipeline that depends on RAW fails when it does. Both methods produce cryptographically identical signatures. The verifier doesn’t need to know which method was used; standard ML-DSA verification applies in either case.

# Retrieve the public key aws kms get-public-key --key-id alias/pqc-code-signing \ --query PublicKey --output text | base64 --decode > pk.der # Extract raw public key bytes from the DER-encoded SubjectPublicKeyInfo. # The BIT STRING starts at offset 17 for all ML-DSA key sizes. # Expected output: 1,312 bytes (ML-DSA-44), 1,952 bytes (ML-DSA-65), or 2,592 bytes (ML-DSA-87). openssl asn1parse -inform DER -in pk.der -strparse 17 -noout -out pk_raw.bin # Compute mu per FIPS 204 Section 6.2 printf '\x00\x00' | cat - artifact.bin > m_prime.bin openssl dgst -shake256 -xoflen 64 -binary pk_raw.bin > pk_hash.bin cat pk_hash.bin m_prime.bin | openssl dgst -shake256 -xoflen 64 -binary > mu.bin # Sign the mu value aws kms sign \ --key-id alias/pqc-code-signing \ --message fileb://mu.bin \ --message-type EXTERNAL_MU \ --signing-algorithm ML_DSA_SHAKE_256 \ --query Signature --output text | base64 --decode > artifact.bin.sig

Store the .sig file alongside the artifact in Amazon S3 or your package repository. Protect the integrity of both the artifact and its signature by enabling S3 Object Lock (WORM protection) or at minimum S3 Versioning with restricted write access. If an attacker can modify or delete either file, verification can’t be trusted. For comprehensive guidance, see Security best practices for Amazon S3.

Container image signing

For container image signing, use the OCI referrer pattern, defined in the OCI Image and Distribution Specifications v1.1. A referrer is an artifact, such as a signature or an SBOM, whose manifest names another image in its subject field. Registries that support the pattern, including Amazon ECR, return these artifacts when a client asks for an image’s referrers. This approach uses AWS KMS ML-DSA keys to generate the signature and stores it as a referrer of the image:

# Repository URI used by ORAS here and in the SBOM steps; replace the account ID and Region ECR_URI=111122223333.dkr.ecr.us-east-1.amazonaws.com/myapp # Authenticate ORAS to Amazon ECR aws ecr get-login-password --region us-east-1 | \ oras login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com # Get the image manifest digest IMAGE_DIGEST=$(aws ecr batch-get-image \ --repository-name myapp \ --image-ids imageTag=v1.2.0 \ --query 'images[0].imageId.imageDigest' --output text) # Sign the digest. It is a fixed 71 characters, so RAW is safe here (see Binary and package signing). # AWS CLI v2 expects blob parameters to be base64, so write the digest to a file and pass it with fileb:// echo -n "$IMAGE_DIGEST" > digest.txt aws kms sign \ --key-id alias/pqc-code-signing \ --message fileb://digest.txt \ --message-type RAW \ --signing-algorithm ML_DSA_SHAKE_256 \ --query Signature --output text | base64 --decode > image.mldsa.sig # Attach signature as OCI referrer using ORAS oras attach $ECR_URI@$IMAGE_DIGEST \ --artifact-type "application/vnd.example.mldsa-signature.v1" \ image.mldsa.sig:application/octet-stream

At deployment time, your admission controller retrieves the referrer artifact and verifies the ML-DSA signature before allowing the image to run. See Verifying a container image signature before deployment later in this post for the full verification workflow and admission control implementation.

SBOM signing

A software bill of materials (SBOM) is a structured inventory of every component, library, and dependency included in a software release. It lists package names, versions, suppliers, and dependency relationships. Organizations use SBOMs to track vulnerabilities, verify license compliance, and respond quickly when a compromised component is disclosed.

Signing the SBOM file on its own proves that the inventory hasn’t been tampered with since it was signed, but not which artifact it describes. If the image signature and the SBOM signature are independent of each other, an attacker who holds a legitimately signed SBOM for a clean image can present it alongside a different signed image and claim that the two correspond. Attaching the SBOM to the image as an OCI referrer doesn’t close this gap by itself, because the subject field of a referrer manifest is set by whoever pushes the referrer; it’s a discovery mechanism, not a cryptographic binding. The binding must be inside the bytes that the signature covers.

To create that binding, wrap the SBOM in an in-toto Statement before signing it. A Statement names the artifacts it describes in its subject field, by digest, and carries the SBOM as its predicate. Because the image manifest digest is part of the signed content, a verifier can confirm that the SBOM in hand was issued for exactly the image it is about to deploy, and a swapped SBOM or a swapped digest fails verification. Embedding the SBOM in the image filesystem is another way to bind the two, because the image digest then covers the SBOM bytes. This post uses the Statement approach because it also works for SBOMs generated by scanning a finished image, for artifacts that have no filesystem such as binaries and firmware, and for images that are already in your registry, and because the SBOM stays readable from the registry without pulling the image.

Generate the SBOM with your preferred tool (such as Amazon Inspector SBOM Generator (Sbomgen) for container images), wrap it in a Statement, and sign the Statement with the same EXTERNAL_MU step you use for binaries:

# Scan the pushed image by digest so the inventory describes exactly the manifest you signed. # Sbomgen reuses the registry login from the oras login step in the previous section. inspector-sbomgen container --image $ECR_URI@$IMAGE_DIGEST -o sbom.json # Wrap the SBOM in an in-toto Statement whose subject is the image manifest digest. # This puts the digest inside the signed bytes, which binds this SBOM to this image. jq -n \ --arg name "$ECR_URI" \ --arg digest "${IMAGE_DIGEST#sha256:}" \ --slurpfile sbom sbom.json \ '{ "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": $name, "digest": {"sha256": $digest}}], "predicateType": "https://cyclonedx.org/bom", "predicate": $sbom[0] }' > sbom.statement.json # Sign using EXTERNAL_MU (same mu computation as binary signing above, # with sbom.statement.json as the input) # ... compute mu from sbom.statement.json ... aws kms sign \ --key-id alias/pqc-code-signing \ --message fileb://mu.bin \ --message-type EXTERNAL_MU \ --signing-algorithm ML_DSA_SHAKE_256 \ --query Signature --output text | base64 --decode > sbom.statement.json.sig # Attach the Statement and its signature to the image as an OCI referrer so # verifiers discover them alongside the image signature oras attach $ECR_URI@$IMAGE_DIGEST \ --artifact-type "application/vnd.example.mldsa-sbom-attestation.v1" \ sbom.statement.json:application/vnd.in-toto+json \ sbom.statement.json.sig:application/octet-stream

Ship the Statement and its .sig file together; the SBOM itself is the Statement’s predicate field, and verifiers extract it after verification. The same pattern binds an SBOM to a binary or package: set the subject digest to the SHA-256 of the file instead of an image manifest digest. The Verifying a binary or SBOM signature section later in this post walks through the full verification workflow, including the subject check that completes the binding.

Quantum-resistant workload identity for build agents

Signing artifacts with ML-DSA keys addresses the integrity of the artifacts themselves. Separately, build agents running outside AWS (on-premises Jenkins, self-hosted GitHub Actions runners, or third-party CI/CD systems) authenticate using X.509 certificates or OIDC tokens typically signed with RSA or ECDSA. These authentication credentials represent a classical cryptographic dependency in the pipeline. IAM Roles Anywhere with ML-DSA certificates replaces that dependency: the credential that the external build agent presents, and the CA that vouches for it, no longer rely on algorithms that are vulnerable to quantum attack. (The TLS connection that carries the request still authenticates the AWS endpoint with a traditional certificate, as public TLS does today; that is a transport concern separate from the signing identity this post is about.) Build agents running on AWS with an attached IAM role don’t have this dependency: their API requests are authenticated with AWS Signature Version 4 (SigV4), which uses a symmetric hash-based message authentication code (HMAC-SHA256). Symmetric schemes like this are already considered quantum-resistant, so no additional action is needed.

Step 1: Register a workload identity CA as a trust anchor

For this walkthrough, reuse the subordinate CA that you created earlier; that avoids the monthly charge for a second CA, and the commands that follow assume it. In production, use a dedicated subordinate CA for workload identity certificates, separate from your code-signing CA. This limits the blast radius if either CA is compromised and has to be revoked, and it lets you apply different issuance policies to the two certificate types.

# Use the ARN that create-certificate-authority returned when you created the # subordinate CA earlier. If you no longer have it, list your CAs and copy the # ARN of the SUBORDINATE one. aws acm-pca list-certificate-authorities \ --query "CertificateAuthorities[?Type=='SUBORDINATE'].[Arn,Status,CertificateAuthorityConfiguration.Subject.CommonName]" \ --output table SUB_CA_ARN=arn:aws:acm-pca:us-east-1:111122223333:certificate-authority/abc-123 # replace with yours # Register the subordinate CA as an IAM Roles Anywhere trust anchor aws rolesanywhere create-trust-anchor \ --name "PQC-CI-CD-Trust-Anchor" \ --source "sourceType=AWS_ACM_PCA,sourceData={acmPcaArn=$SUB_CA_ARN}" \ --enabled

Step 2: Create the build agent role and a profile that references it

An IAM Roles Anywhere profile specifies which IAM roles a workload can assume after it authenticates with a certificate from your trust anchor. Create the role first. Its trust policy lets IAM Roles Anywhere assume it only through your trust anchor and only for a certificate whose subject common name identifies the build agent; without those conditions, any certificate that the trust anchor CA issues could assume the role. Its permissions policy grants only what signing requires, on only the code-signing key. Save the trust policy as build-agent-trust.json, replacing the trust anchor ARN with the one that create-trust-anchor returned in Step 1:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"Service": "rolesanywhere.amazonaws.com"}, "Action": ["sts:AssumeRole", "sts:TagSession", "sts:SetSourceIdentity"], "Condition": { "ArnEquals": { "aws:SourceArn": "arn:aws:rolesanywhere:us-east-1:111122223333:trust-anchor/ta-id" }, "StringEquals": { "aws:PrincipalTag/x509Subject/CN": "pqc-build-agent-01" } } } ] }

Save the permissions policy as build-agent-permissions.json, replacing the resource with your key’s ARN (aws kms describe-key --key-id alias/pqc-code-signing --query KeyMetadata.Arn --output text):

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["kms:Sign", "kms:GetPublicKey", "kms:DescribeKey"], "Resource": "<code-signing-key-arn>" } ] }

Create the role, attach the policy, and then create a profile that references the role:

aws iam create-role \ --role-name PQCBuildAgentRole \ --assume-role-policy-document file://build-agent-trust.json aws iam put-role-policy \ --role-name PQCBuildAgentRole \ --policy-name pqc-code-signing \ --policy-document file://build-agent-permissions.json aws rolesanywhere create-profile \ --name "PQC-Build-Agent-Profile" \ --role-arns "arn:aws:iam::111122223333:role/PQCBuildAgentRole" \ --enabled

Step 3: Issue the build agent certificate and configure the credential helper

Generate the build agent’s ML-DSA-65 key pair and CSR locally with OpenSSL, have the subordinate CA issue a client authentication certificate for it, and then configure the IAM Roles Anywhere credential helper to present that certificate:

# Generate the build agent's key pair and CSR. The CN must match the # aws:PrincipalTag/x509Subject/CN condition in the role's trust policy. openssl genpkey -algorithm ML-DSA-65 -out build-agent-mldsa-key.pem chmod 400 build-agent-mldsa-key.pem openssl req -new -key build-agent-mldsa-key.pem \ -subj "/C=US/O=Example Corp/CN=pqc-build-agent-01" \ -out build-agent.csr # Issue a 30-day client authentication certificate from the subordinate CA # registered as the trust anchor in Step 1. BUILD_AGENT_CERT_ARN=$(aws acm-pca issue-certificate \ --certificate-authority-arn $SUB_CA_ARN \ --csr fileb://build-agent.csr \ --signing-algorithm ML_DSA_65 \ --validity Value=30,Type=DAYS \ --template-arn arn:aws:acm-pca:::template/EndEntityClientAuthCertificate/V1 \ --query CertificateArn --output text) aws acm-pca get-certificate \ --certificate-authority-arn $SUB_CA_ARN \ --certificate-arn $BUILD_AGENT_CERT_ARN \ --query Certificate --output text > build-agent-mldsa.pem # In production, store the private key in a TPM or HSM rather than as a plaintext file. aws_signing_helper credential-process \ --certificate build-agent-mldsa.pem \ --private-key build-agent-mldsa-key.pem \ --trust-anchor-arn arn:aws:rolesanywhere:us-east-1:111122223333:trust-anchor/ta-id \ --profile-arn arn:aws:rolesanywhere:us-east-1:111122223333:profile/profile-id \ --role-arn arn:aws:iam::111122223333:role/PQCBuildAgentRole

The helper produces short-lived AWS Security Token Service (AWS STS) credentials scoped to the build role, replacing long-lived AWS access keys. In production, protect the ML-DSA private key with hardware-backed storage such as a Trusted Platform Module (TPM), an HSM, or an encrypted credential store with restricted file permissions. The plaintext file path shown here is for demonstration purposes. The private key authorizes access to your code-signing infrastructure and should be treated with the same protection level as any signing key. On Linux build agents, at minimum restrict the file to chmod 400 owned by the service account and consider using a PKCS#11 provider to avoid storing the raw key on disk entirely.

Verification and policy enforcement

A signing architecture is only as strong as its verification. Producing signatures is half the story. The other half is giving the systems and people that deploy your artifacts, such as deployment pipelines, Kubernetes admission controllers, and engineers installing a package, a concrete way to verify those signatures and make trust decisions before an artifact is deployed or run.

Distributing your trust chain

Before any verifier can check a signature, it needs to trust the CA that issued the code-signing certificate. This trust is established through a trust store: a local collection of root CA certificates that a system treats as authoritative. Trust stores exist in operating systems (such as /etc/pki/ca-trust/ on Linux, Keychain on macOS), language run-times (Java cacerts), container base images, and custom application configurations.

For private PKI hierarchies like the one in this post, your root CA certificate isn’t in any default public trust store. You must explicitly add it to the trust stores of every system that will verify your signatures. Common approaches include:

  • Baking the root CA certificate into your container base images during the build process
  • Distributing it through configuration management (AWS Systems Manager Parameter Store, AWS Secrets Manager, or your existing configuration tooling)
  • Publishing it to an internal S3 bucket with versioning and access logging
  • Storing it as an OCI artifact in Amazon ECR alongside the images it protects

Treat trust store management with the same rigor as credential management. If an attacker can inject a rogue root certificate into your trust stores, they can make forged signatures appear valid. Restrict write access to trust stores and audit changes through AWS CloudTrail or your system-level audit logs.

Exporting your certificate chain requires exporting the root CA, and then any subordinate CAs that issue certificates that will be involved in verifying signatures:

# Export the root CA certificate aws acm-pca get-certificate-authority-certificate \ --certificate-authority-arn <root-ca-arn> \ --query Certificate --output text > root-ca.pem # Export the subordinate CA certificate. Select only the Certificate field: for a # subordinate CA this call also returns CertificateChain, and --output text would # print both on one tab-separated line, producing a file that OpenSSL can't parse. aws acm-pca get-certificate-authority-certificate \ --certificate-authority-arn <sub-ca-arn> \ --query Certificate --output text > sub-ca.pem # Bundle the full chain for verifiers cat root-ca.pem sub-ca.pem > trust-chain.pem

Verifiers use this chain to confirm that the code-signing public key was issued by a CA they trust.

The CAs in this post are created without a revocation configuration, and the verification commands that follow don’t check revocation status, which keeps the walkthrough short. In production, configure a certificate revocation list (CRL) or an Online Certificate Status Protocol (OCSP) responder on the subordinate CA (see Plan your AWS Private CA certificate revocation method in the AWS Private CA User Guide; both work with ML-DSA CAs). If you use a CRL, distribute it alongside the trust chain and add -crl_check -CRLfile <crl-file> to the openssl verify command below, where <crl-file> is the CRL that AWS Private CA publishes to your S3 bucket, so that a revoked code-signing certificate fails verification.

Verifying a binary or SBOM signature

Verify the code-signing certificate against the trust chain, confirm that it is a code-signing certificate issued to the signer you expect, extract its public key, and then verify the signature with OpenSSL 3.5. The second check matters: a chain check alone accepts every certificate that the CA hierarchy has issued, including the build agent client certificates from the previous section, so pin the expected subject in your verifier configuration:

# Step 1: Verify that the code-signing certificate chains to the trusted root CA and carries the # Code Signing extended key usage. -purpose codesign (OpenSSL 3.2 and later) rejects a certificate # that chains correctly but was issued for another purpose, such as a build agent's client # authentication certificate from the same subordinate CA. # trust-chain.pem contains the root CA and subordinate CA certificates distributed in the previous section. openssl verify -CAfile trust-ch...
Source:https://aws.amazon.com/blogs/security/quantum-resistant-signing-on-aws-part-1-build-a-software-supply-chain-with-ml-dsa/

Related posts