From 86bad8161733f9d99d209ff1aa2193044cc389c6 Mon Sep 17 00:00:00 2001 From: Aditya Potdar Date: Sun, 18 Oct 2026 05:28:30 +0000 Subject: [PATCH] Send a placeholder subject in the certificate signing request STS does not parse the CSR, so the subject passes through to the signer, and the signer rejects a blank one. Every X.509-SVID issuance therefore fails against the current stack, because buildCSR sent an empty pkix.Name. Set a fixed common name instead. The value names the agent rather than the workload on purpose: the agent does not choose the identity, so a common name that looked like one would claim it does. The SPIFFE ID still comes only from the attested session, and the request continues to carry no SAN and no extensions. This is temporary. STS intends to overwrite the subject with an empty value and mark the SAN extension critical itself, which is what makes a certificate with no subject SPIFFE compliant; that change is blocked on the signer's own validation. Any subject works until then and is ignored afterwards, so this is forwards compatible and the constant should be deleted once STS overwrites it. buildCSR keeps its signature, since the value is fixed and needs nothing from the workload. Its doc comment claimed STS ignores the subject and SAN in the request, which is not true of either; it now states the invariant that holds, that the request names no identity. TestBuildCSRCarriesNothingButTheKey is renamed to TestBuildCSRNamesNoIdentity, as the request is no longer nothing but the key, and now pins the exact subject so an extra attribute fails. Its other assertions are unchanged. --- internal/material/x509/keypair.go | 14 +++++++++++--- internal/material/x509/keypair_test.go | 16 +++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/internal/material/x509/keypair.go b/internal/material/x509/keypair.go index 699624e..7697756 100644 --- a/internal/material/x509/keypair.go +++ b/internal/material/x509/keypair.go @@ -23,6 +23,13 @@ import ( // certificate request. The name is fixed by RFC 7468. const pemTypeCertificateRequest = "CERTIFICATE REQUEST" +// csrSubjectCommonName is a placeholder subject. STS does not parse the CSR, so an empty +// one reaches the signer, which rejects it. It names the agent, not the workload, because +// the agent does not choose the identity. +// +// TEMPORARY: delete once STS overwrites the subject and marks the SAN critical itself. +const csrSubjectCommonName = "eks-workload-identity" + // generateKey creates a new key pair of the configured kind for one certificate. // // It returns a crypto.Signer rather than the key, because a Signer does not expose the @@ -64,11 +71,12 @@ func rsaKey(bits int, kind workloadidentity.KeyType) (crypto.Signer, error) { // buildCSR creates the certificate request we send to STS. It carries the public half of // the key pair and a signature made with the private half, which proves possession. // -// It carries no name. STS takes the subject from the attested workload identity in the -// session, and ignores any subject or SAN in the request. +// It names no identity. The SPIFFE ID comes from the attested workload identity in the +// session, never from the request, so the request carries no SAN and no extensions. Its +// subject is the fixed csrSubjectCommonName placeholder, for the reason given there. func buildCSR(key crypto.Signer) (string, error) { der, err := cryptox509.CreateCertificateRequest(rand.Reader, &cryptox509.CertificateRequest{ - Subject: pkix.Name{}, + Subject: pkix.Name{CommonName: csrSubjectCommonName}, }, key) if err != nil { return "", fmt.Errorf("building the certificate signing request: %w", err) diff --git a/internal/material/x509/keypair_test.go b/internal/material/x509/keypair_test.go index 177230c..30372b8 100644 --- a/internal/material/x509/keypair_test.go +++ b/internal/material/x509/keypair_test.go @@ -93,10 +93,10 @@ func TestGenerateKeyRejectsAnUnknownType(t *testing.T) { "the error has to name the types an operator can choose instead") } -// TestBuildCSRCarriesNothingButTheKey is the definition-of-done assertion on the -// request: it carries a public key and a proof of possession, and nothing that -// could be read as the agent naming an identity. -func TestBuildCSRCarriesNothingButTheKey(t *testing.T) { +// TestBuildCSRNamesNoIdentity is the definition-of-done assertion on the request: it +// carries a public key, a proof of possession, and a fixed placeholder subject, and +// nothing that could be read as the agent naming an identity. +func TestBuildCSRNamesNoIdentity(t *testing.T) { key, err := generateKey(workloadidentity.DefaultKeyType) require.NoError(t, err) @@ -114,7 +114,13 @@ func TestBuildCSRCarriesNothingButTheKey(t *testing.T) { assert.NoError(t, csr.CheckSignature(), "the request has to prove possession of the key") assert.Equal(t, key.Public(), csr.PublicKey, "the request has to carry this issuance's public key") - assert.Empty(t, csr.Subject.String(), "a populated subject would be discarded by STS and would mislead a reader into thinking the agent picks the identity") + // The subject is the fixed placeholder and nothing else: a blank one is currently + // rejected by the signer, and a workload-specific one would say the agent picks the + // identity. Asserting the exact value keeps this pinned to the one string that has + // to be deleted when STS starts overwriting the subject itself. + assert.Equal(t, csrSubjectCommonName, csr.Subject.CommonName) + assert.Equal(t, "CN="+csrSubjectCommonName, csr.Subject.String(), + "the subject carries the placeholder common name and no other attribute") assert.Empty(t, csr.Extensions, "the request carries no extensions") assert.Empty(t, csr.ExtraExtensions, "the request carries no extensions") assert.Empty(t, csr.DNSNames)