From a86f377950107d715c1cfbcf55c87dbd90af4fa1 Mon Sep 17 00:00:00 2001 From: Taahir Ahmed Date: Tue, 27 Jan 2026 18:28:48 -0800 Subject: [PATCH] Pod Certificates: Add StubPKCS10Request; migrate in-tree usages to it --- pkg/apis/certificates/types.go | 51 +++++++- .../certificates/validation/validation.go | 122 +++++++++++++++++- .../validation/validation_test.go | 94 ++++++++++++-- .../podcertificate/podcertificatemanager.go | 90 +++++-------- .../podcertificaterequest/strategy.go | 2 +- .../k8s.io/api/certificates/v1beta1/types.go | 50 +++++-- .../hermeticpodcertificatesigner.go | 6 +- 7 files changed, 327 insertions(+), 88 deletions(-) diff --git a/pkg/apis/certificates/types.go b/pkg/apis/certificates/types.go index 1f7e345406b..df269b28f7d 100644 --- a/pkg/apis/certificates/types.go +++ b/pkg/apis/certificates/types.go @@ -340,8 +340,7 @@ type PodCertificateRequestSpec struct { // seconds (1 hour). This constraint is enforced by kube-apiserver. MaxExpirationSeconds *int32 - // pkixPublicKey is the PKIX-serialized public key the signer will issue the - // certificate to. + // The PKIX-serialized public key the signer will issue the certificate to. // // The key must be one of RSA3072, RSA4096, ECDSAP256, ECDSAP384, ECDSAP521, // or ED25519. Note that this list may be expanded in the future. @@ -352,13 +351,20 @@ type PodCertificateRequestSpec struct { // setting a status.conditions entry with a type of "Denied" and a reason of // "UnsupportedKeyType". It may also suggest a key type that it does support // in the message field. + // + // DEPRECATED: This field is replaced by StubPKCS10Request. If + // StubPKCS10Request is set, this field must be empty. Signer + // implementations should extract the public key from the StubPKCS10Request + // field. + // + // +optional PKIXPublicKey []byte - // proofOfPossession proves that the requesting kubelet holds the private - // key corresponding to pkixPublicKey. + // A proof that the requesting kubelet holds the private key corresponding + // to pkixPublicKey. // // It is contructed by signing the ASCII bytes of the pod's UID using - // `PKIXPublicKey`. + // `pkixPublicKey`. // // kube-apiserver validates the proof of possession during creation of the // PodCertificateRequest. @@ -374,8 +380,40 @@ type PodCertificateRequestSpec struct { // If the key is an ED25519 key, the the signature is as described by the // [ED25519 Specification](https://ed25519.cr.yp.to/) (as implemented by the // golang library crypto/ed25519.Sign). + // + // DEPRECATED: This field is replaced by StubPKCS10Request. If + // StubPKCS10Request is set, this field must be empty. + // + // +optional ProofOfPossession []byte + // A PKCS#10 certificate signing request (DER-serialized) generated by + // Kubelet using the subject private key. + // + // Most signer implementations will ignore the contents of the CSR except to + // extract the subject public key. The API server automatically verifies the + // CSR signature during admission, so the signer does not need to repeat the + // verification. + // + // The subject public key must be one of RSA3072, RSA4096, ECDSAP256, + // ECDSAP384, ECDSAP521, or ED25519. Note that this list may be expanded in + // the future. + // + // Signer implementations do not need to support all key types supported by + // kube-apiserver and kubelet. If a signer does not support the key type + // used for a given PodCertificateRequest, it must deny the request by + // setting a status.conditions entry with a type of "Denied" and a reason of + // "UnsupportedKeyType". It may also suggest a key type that it does support + // in the message field. + // + // Some CA implementations require that the client (the signer + // implementation, in this case) provide a PKCS#10 certificate signing + // request, even if the CA only extracts the subject public key from the + // request. To enable compatibility with these CAs, Kubelet will generate a + // stub PKCS#10 request that the signer implementation can then pass on to + // the CA. + StubPKCS10Request []byte + // unverifiedUserAnnotations allow pod authors to pass additional information to // the signer implementation. Kubernetes does not restrict or validate this // metadata in any way. @@ -485,6 +523,9 @@ const ( // MaxProofOfPossessionSize is the maximum size permitted for the // ProofOfPossession field. MaxProofOfPossessionSize = 10 * 1024 + // MaxStubPKCS10RequestSize is the maximum size permitted for the + // UnverifiedPKCS10Request field. + MaxStubPKCS10RequestSize = 10 * 1024 // MaxCertificateChainSize is the maximum size permitted for the // CertificateChain field. // diff --git a/pkg/apis/certificates/validation/validation.go b/pkg/apis/certificates/validation/validation.go index 0b2b6046bbd..82948c0af94 100644 --- a/pkg/apis/certificates/validation/validation.go +++ b/pkg/apis/certificates/validation/validation.go @@ -646,6 +646,26 @@ func ValidatePodCertificateRequestCreate(req *certificates.PodCertificateRequest } } + // Either (PKIXPublicKey, ProofOfPossession) xor (StubPKCS10Request) + // must be set. + if len(req.Spec.StubPKCS10Request) != 0 && len(req.Spec.PKIXPublicKey) == 0 && len(req.Spec.ProofOfPossession) == 0 { + // Valid, using StubPKCS10Request + allErrors = append(allErrors, validateStubPKCS10Request(req)...) + return allErrors + } else if len(req.Spec.StubPKCS10Request) == 0 && len(req.Spec.PKIXPublicKey) != 0 && len(req.Spec.ProofOfPossession) != 0 { + // Valid, using PKIXPublicKey and ProofOfPossession + allErrors = append(allErrors, validateDeprecatedPKIXPublicKey(req)...) + return allErrors + } else { + // Invalid, any other combination. + allErrors = append(allErrors, field.Invalid(field.NewPath("spec"), field.OmitValueType{}, "exactly one of (stubPKCS10Request) or (pkixPublicKey, proofOfPossession) must be set")) + return allErrors + } +} + +func validateDeprecatedPKIXPublicKey(req *certificates.PodCertificateRequest) field.ErrorList { + var allErrors field.ErrorList + if len(req.Spec.PKIXPublicKey) > certificates.MaxPKIXPublicKeySize { allErrors = append(allErrors, field.TooLong(field.NewPath("spec", "pkixPublicKey"), req.Spec.PKIXPublicKey, certificates.MaxPKIXPublicKeySize)) return allErrors @@ -699,6 +719,85 @@ func ValidatePodCertificateRequestCreate(req *certificates.PodCertificateRequest return allErrors } +var ( + oidExtensionSubjectAltName = []int{2, 5, 29, 17} +) + +func validateStubPKCS10Request(req *certificates.PodCertificateRequest) field.ErrorList { + var allErrors field.ErrorList + + if len(req.Spec.StubPKCS10Request) > certificates.MaxStubPKCS10RequestSize { + allErrors = append(allErrors, field.TooLong(pkcs10ReqPath, req.Spec.StubPKCS10Request, certificates.MaxStubPKCS10RequestSize)) + return allErrors + } + + pkcs10Req, err := x509.ParseCertificateRequest(req.Spec.StubPKCS10Request) + if err != nil { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "must be a valid PKCS#10 CSR")) + return allErrors + } + + // Check key type and parameters + switch pkcs10Pub := pkcs10Req.PublicKey.(type) { + case ed25519.PublicKey: + // ed25519 has no key configuration to check + case *ecdsa.PublicKey: + if pkcs10Pub.Curve != elliptic.P256() && pkcs10Pub.Curve != elliptic.P384() && pkcs10Pub.Curve != elliptic.P521() { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, "curve "+pkcs10Pub.Curve.Params().Name, "elliptic public keys must use curve P256, P384, or P521")) + return allErrors + } + case *rsa.PublicKey: + if pkcs10Pub.Size()*8 != 3072 && pkcs10Pub.Size()*8 != 4096 { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, fmt.Sprintf("%d-bit modulus", pkcs10Pub.Size()*8), "RSA keys must have modulus size 3072 or 4096")) + return allErrors + } + default: + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "unknown public key type; supported types are Ed25519, ECDSA, and RSA")) + return allErrors + } + + // Check that the request is empty, except for DNS or IP SANs. + if len(pkcs10Req.Subject.Country) != 0 || + len(pkcs10Req.Subject.Organization) != 0 || + len(pkcs10Req.Subject.OrganizationalUnit) != 0 || + len(pkcs10Req.Subject.Locality) != 0 || + len(pkcs10Req.Subject.Province) != 0 || + len(pkcs10Req.Subject.StreetAddress) != 0 || + len(pkcs10Req.Subject.PostalCode) != 0 || + len(pkcs10Req.Subject.SerialNumber) != 0 || + len(pkcs10Req.Subject.CommonName) != 0 || + len(pkcs10Req.Subject.Names) != 0 { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "PKCS#10 request must have empty subject")) + return allErrors + } + if len(pkcs10Req.EmailAddresses) != 0 { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "PKCS#10 request must not contain EmailAddress subject alternate names")) + return allErrors + } + if len(pkcs10Req.URIs) != 0 { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "PKCS#10 request must not contain URI subject alternate names")) + return allErrors + } + + for i := range pkcs10Req.Extensions { + if pkcs10Req.Extensions[i].Id.Equal(oidExtensionSubjectAltName) { + // If the SubjectAlternateName contained non-IP or non-DNS content, + // then EmailAddresses or URIs would have been non-empty above. + } else { + // All other extensions are forbidden. + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "PKCS#10 request may not contain arbitrary extensions")) + return allErrors + } + } + + if err := pkcs10Req.CheckSignature(); err != nil { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "invalid signature")) + return allErrors + } + + return allErrors +} + func hashBytes(in []byte) []byte { out := sha256.Sum256(in) return out[:] @@ -707,6 +806,7 @@ func hashBytes(in []byte) []byte { var ( pkixPath = field.NewPath("spec", "pkixPublicKey") popPath = field.NewPath("spec", "proofOfPossession") + pkcs10ReqPath = field.NewPath("spec", "stubPKCS10Request") certChainPath = field.NewPath("status", "certificateChain") notBeforePath = field.NewPath("status", "notBefore") notAfterPath = field.NewPath("status", "notAfter") @@ -826,12 +926,24 @@ func ValidatePodCertificateRequestStatusUpdate(newReq, oldReq *certificates.PodC } } - // Was the certificate issued to the public key in the spec? - wantPKAny, err := x509.ParsePKIXPublicKey(oldReq.Spec.PKIXPublicKey) - if err != nil { - allErrors = append(allErrors, field.Invalid(pkixPath, oldReq.Spec.PKIXPublicKey, "must be a valid PKIX-serialized public key")) - return allErrors + // Get the public key from either StubPKCS10Request or PKIXPublicKey. + var wantPKAny crypto.PublicKey + if len(oldReq.Spec.StubPKCS10Request) != 0 { + pkcs10Req, err := x509.ParseCertificateRequest(oldReq.Spec.StubPKCS10Request) + if err != nil { + allErrors = append(allErrors, field.Invalid(pkcs10ReqPath, field.OmitValueType{}, "must be a valid PKCS#10 CSR")) + return allErrors + } + wantPKAny = pkcs10Req.PublicKey + } else { + wantPKAny, err = x509.ParsePKIXPublicKey(oldReq.Spec.PKIXPublicKey) + if err != nil { + allErrors = append(allErrors, field.Invalid(pkixPath, field.OmitValueType{}, "must be a valid PKIX-serialized public key")) + return allErrors + } } + + // Was the certificate issued to the public key in the spec? switch wantPK := wantPKAny.(type) { case ed25519.PublicKey: if !wantPK.Equal(leafCert.PublicKey) { diff --git a/pkg/apis/certificates/validation/validation_test.go b/pkg/apis/certificates/validation/validation_test.go index 717251d188f..7be4c383333 100644 --- a/pkg/apis/certificates/validation/validation_test.go +++ b/pkg/apis/certificates/validation/validation_test.go @@ -1610,9 +1610,10 @@ func TestValidateClusterTrustBundleUpdate(t *testing.T) { func TestValidatePodCertificateRequestCreate(t *testing.T) { podUID1 := "pod-uid-1" - _, _, ed25519PubPKIX1, ed25519Proof1 := mustMakeEd25519KeyAndProof(t, []byte(podUID1)) - _, _, ed25519PubPKIX2, ed25519Proof2 := mustMakeEd25519KeyAndProof(t, []byte("other-value")) - _, _, _, ed25519Proof3 := mustMakeEd25519KeyAndProof(t, []byte(podUID1)) + _, _, ed25519PubPKIX1, ed25519Proof1, ed25519CSR1 := mustMakeEd25519KeyAndProof(t, []byte(podUID1), []string{}) + _, _, ed25519PubPKIX2, ed25519Proof2, _ := mustMakeEd25519KeyAndProof(t, []byte("other-value"), []string{}) + _, _, _, ed25519Proof3, _ := mustMakeEd25519KeyAndProof(t, []byte(podUID1), []string{}) + _, _, _, _, ed25519CSR4 := mustMakeEd25519KeyAndProof(t, []byte(podUID1), []string{"example.com", "foo.example.example"}) _, _, ecdsaP224PubPKIX1, ecdsaP224Proof1 := mustMakeECDSAKeyAndProof(t, elliptic.P224(), []byte(podUID1)) _, _, ecdsaP256PubPKIX1, ecdsaP256Proof1 := mustMakeECDSAKeyAndProof(t, elliptic.P256(), []byte(podUID1)) _, _, ecdsaP384PubPKIX1, ecdsaP384Proof1 := mustMakeECDSAKeyAndProof(t, elliptic.P384(), []byte(podUID1)) @@ -1624,7 +1625,7 @@ func TestValidatePodCertificateRequestCreate(t *testing.T) { _, _, rsaWrongProofPKIX, rsaWrongProof := mustMakeRSAKeyAndProof(t, 3072, []byte("other-value")) podUIDEmpty := "" - _, _, pubPKIXEmpty, proofEmpty := mustMakeEd25519KeyAndProof(t, []byte(podUIDEmpty)) + _, _, pubPKIXEmpty, proofEmpty, _ := mustMakeEd25519KeyAndProof(t, []byte(podUIDEmpty), []string{}) testCases := []struct { description string @@ -1653,6 +1654,73 @@ func TestValidatePodCertificateRequestCreate(t *testing.T) { }, wantErrors: nil, }, + { + description: "valid Ed25519 PCR (using PKCS#10)", + pcr: &capi.PodCertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "bar", + }, + Spec: capi.PodCertificateRequestSpec{ + SignerName: "foo.com/abc", + PodName: "pod-1", + PodUID: types.UID(podUID1), + ServiceAccountName: "sa-1", + ServiceAccountUID: "sa-uid-1", + NodeName: "node-1", + NodeUID: "node-uid-1", + MaxExpirationSeconds: ptr.To[int32](86400), + StubPKCS10Request: ed25519CSR1, + }, + }, + wantErrors: nil, + }, + { + description: "valid Ed25519 PCR (using PKCS#10, with DNS SANs)", + pcr: &capi.PodCertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "bar", + }, + Spec: capi.PodCertificateRequestSpec{ + SignerName: "foo.com/abc", + PodName: "pod-1", + PodUID: types.UID(podUID1), + ServiceAccountName: "sa-1", + ServiceAccountUID: "sa-uid-1", + NodeName: "node-1", + NodeUID: "node-uid-1", + MaxExpirationSeconds: ptr.To[int32](86400), + StubPKCS10Request: ed25519CSR4, + }, + }, + wantErrors: nil, + }, + { + description: "invalid Ed25519 PCR (both StubPKCS10Request and PKIXPublicKey set)", + pcr: &capi.PodCertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "bar", + }, + Spec: capi.PodCertificateRequestSpec{ + SignerName: "foo.com/abc", + PodName: "pod-1", + PodUID: types.UID(podUID1), + ServiceAccountName: "sa-1", + ServiceAccountUID: "sa-uid-1", + NodeName: "node-1", + NodeUID: "node-uid-1", + MaxExpirationSeconds: ptr.To[int32](86400), + PKIXPublicKey: ed25519PubPKIX1, + ProofOfPossession: ed25519Proof1, + StubPKCS10Request: ed25519CSR1, + }, + }, + wantErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec"), field.OmitValueType{}, "exactly one of (stubPKCS10Request) or (pkixPublicKey, proofOfPossession) must be set"), + }, + }, { description: "invalid Ed25519 proof of possession (correct key signed wrong message)", pcr: &capi.PodCertificateRequest{ @@ -2187,7 +2255,7 @@ func TestValidatePodCertificateRequestCreate(t *testing.T) { NodeUID: "node-uid-1", MaxExpirationSeconds: ptr.To[int32](86400), PKIXPublicKey: make([]byte, capi.MaxPKIXPublicKeySize+1), - ProofOfPossession: []byte{}, + ProofOfPossession: []byte("abc"), }, }, wantErrors: field.ErrorList{ @@ -2210,7 +2278,7 @@ func TestValidatePodCertificateRequestCreate(t *testing.T) { NodeName: "node-1", NodeUID: "node-uid-1", MaxExpirationSeconds: ptr.To[int32](86400), - PKIXPublicKey: []byte{}, + PKIXPublicKey: ed25519PubPKIX1, ProofOfPossession: make([]byte, capi.MaxProofOfPossessionSize+1), }, }, @@ -2308,7 +2376,7 @@ func TestValidatePodCertificateRequestCreate(t *testing.T) { func TestValidatePodCertificateRequestUpdate(t *testing.T) { podUID1 := "pod-uid-1" - _, _, pubPKIX1, proof1 := mustMakeEd25519KeyAndProof(t, []byte(podUID1)) + _, _, pubPKIX1, proof1, _ := mustMakeEd25519KeyAndProof(t, []byte(podUID1), []string{}) testCases := []struct { description string @@ -2396,7 +2464,7 @@ func TestValidatePodCertificateRequestStatusUpdate(t *testing.T) { intermediateCACertDER, intermediateCAPrivKey := mustMakeIntermediateCA(t, caCertDER, caPrivKey) podUID1 := "pod-uid-1" - _, pub1, pubPKIX1, proof1 := mustMakeEd25519KeyAndProof(t, []byte(podUID1)) + _, pub1, pubPKIX1, proof1, _ := mustMakeEd25519KeyAndProof(t, []byte(podUID1), []string{}) pod1Cert1 := mustSignCertForPublicKey(t, 24*time.Hour, pub1, caCertDER, caPrivKey, false, "", "") pod1Cert2 := mustSignCertForPublicKey(t, 18*time.Hour, pub1, caCertDER, caPrivKey, false, "", "") @@ -4438,7 +4506,7 @@ func mustParseTime(t *testing.T, stamp string) time.Time { return got } -func mustMakeEd25519KeyAndProof(t *testing.T, toBeSigned []byte) (ed25519.PrivateKey, ed25519.PublicKey, []byte, []byte) { +func mustMakeEd25519KeyAndProof(t *testing.T, toBeSigned []byte, pkcs10DNSSANS []string) (ed25519.PrivateKey, ed25519.PublicKey, []byte, []byte, []byte) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("Error while generating ed25519 key: %v", err) @@ -4448,7 +4516,13 @@ func mustMakeEd25519KeyAndProof(t *testing.T, toBeSigned []byte) (ed25519.Privat t.Fatalf("Error while marshaling PKIX public key: %v", err) } sig := ed25519.Sign(priv, toBeSigned) - return priv, pub, pubPKIX, sig + + pkcs10DER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: pkcs10DNSSANS}, priv) + if err != nil { + t.Fatalf("Error while creating PKCS#10 certificate signing request: %v", err) + } + + return priv, pub, pubPKIX, sig, pkcs10DER } func mustMakeECDSAKeyAndProof(t *testing.T, curve elliptic.Curve, toBeSigned []byte) (*ecdsa.PrivateKey, *ecdsa.PublicKey, []byte, []byte) { diff --git a/pkg/kubelet/podcertificate/podcertificatemanager.go b/pkg/kubelet/podcertificate/podcertificatemanager.go index f06cef94497..7765b0fb426 100644 --- a/pkg/kubelet/podcertificate/podcertificatemanager.go +++ b/pkg/kubelet/podcertificate/podcertificatemanager.go @@ -25,7 +25,6 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" - "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" @@ -742,18 +741,14 @@ func (m *IssuingManager) createPodCertificateRequest( podName string, podUID types.UID, serviceAccountName string, serviceAccountUID types.UID, nodeName types.NodeName, nodeUID types.UID, - signerName, keyType string, maxExpirationSeconds *int32, userAnnotations map[string]string) ([]byte, *certificatesv1beta1.PodCertificateRequest, error) { - - privateKey, publicKey, proof, err := generateKeyAndProof(keyType, []byte(podUID)) + signerName, keyType string, maxExpirationSeconds *int32, + userAnnotations map[string]string, +) ([]byte, *certificatesv1beta1.PodCertificateRequest, error) { + privateKey, pkcs10Req, err := generateKeyAndProof(keyType) if err != nil { return nil, nil, fmt.Errorf("while generating keypair: %w", err) } - pkixPublicKey, err := x509.MarshalPKIXPublicKey(publicKey) - if err != nil { - return nil, nil, fmt.Errorf("while marshaling public key: %w", err) - } - keyPEM, err := pemEncodeKey(privateKey) if err != nil { return nil, nil, fmt.Errorf("while PEM-encoding private key: %w", err) @@ -781,8 +776,7 @@ func (m *IssuingManager) createPodCertificateRequest( NodeName: nodeName, NodeUID: nodeUID, MaxExpirationSeconds: maxExpirationSeconds, - PKIXPublicKey: pkixPublicKey, - ProofOfPossession: proof, + StubPKCS10Request: pkcs10Req, UnverifiedUserAnnotations: userAnnotations, }, } @@ -877,73 +871,57 @@ func (m *IssuingManager) MetricReport() *MetricReport { return report } -func hashBytes(in []byte) []byte { - out := sha256.Sum256(in) - return out[:] -} +func generateKeyAndProof(keyType string) (crypto.PrivateKey, []byte, error) { + var privKey crypto.PrivateKey -func generateKeyAndProof(keyType string, toBeSigned []byte) (privKey crypto.PrivateKey, pubKey crypto.PublicKey, sig []byte, err error) { switch keyType { case "RSA3072": - key, err := rsa.GenerateKey(rand.Reader, 3072) + priv, err := rsa.GenerateKey(rand.Reader, 3072) if err != nil { - return nil, nil, nil, fmt.Errorf("while generating RSA 3072 key: %w", err) + return nil, nil, fmt.Errorf("while generating RSA 3072 key: %w", err) } - sig, err := rsa.SignPSS(rand.Reader, key, crypto.SHA256, hashBytes(toBeSigned), nil) - if err != nil { - return nil, nil, nil, fmt.Errorf("while signing proof: %w", err) - } - return key, &key.PublicKey, sig, nil + privKey = priv case "RSA4096": - key, err := rsa.GenerateKey(rand.Reader, 4096) + priv, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { - return nil, nil, nil, fmt.Errorf("while generating RSA 4096 key: %w", err) + return nil, nil, fmt.Errorf("while generating RSA 4096 key: %w", err) } - sig, err := rsa.SignPSS(rand.Reader, key, crypto.SHA256, hashBytes(toBeSigned), nil) - if err != nil { - return nil, nil, nil, fmt.Errorf("while signing proof: %w", err) - } - return key, &key.PublicKey, sig, nil + privKey = priv case "ECDSAP256": - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, nil, nil, fmt.Errorf("while generating ECDSA P256 key: %w", err) + return nil, nil, fmt.Errorf("while generating ECDSA P256 key: %w", err) } - sig, err := ecdsa.SignASN1(rand.Reader, key, hashBytes(toBeSigned)) - if err != nil { - return nil, nil, nil, fmt.Errorf("while signing proof: %w", err) - } - return key, &key.PublicKey, sig, nil + privKey = priv case "ECDSAP384": - key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { - return nil, nil, nil, fmt.Errorf("while generating ECDSA P384 key: %w", err) + return nil, nil, fmt.Errorf("while generating ECDSA P384 key: %w", err) } - sig, err := ecdsa.SignASN1(rand.Reader, key, hashBytes(toBeSigned)) - if err != nil { - return nil, nil, nil, fmt.Errorf("while signing proof: %w", err) - } - return key, &key.PublicKey, sig, nil + privKey = priv case "ECDSAP521": - key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) if err != nil { - return nil, nil, nil, fmt.Errorf("while generating ECDSA P521 key: %w", err) + return nil, nil, fmt.Errorf("while generating ECDSA P521 key: %w", err) } - sig, err := ecdsa.SignASN1(rand.Reader, key, hashBytes(toBeSigned)) - if err != nil { - return nil, nil, nil, fmt.Errorf("while signing proof: %w", err) - } - return key, &key.PublicKey, sig, nil + privKey = priv case "ED25519": - pub, priv, err := ed25519.GenerateKey(rand.Reader) + _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { - return nil, nil, nil, fmt.Errorf("while generating Ed25519 key: %w", err) + return nil, nil, fmt.Errorf("while generating Ed25519 key: %w", err) } - sig := ed25519.Sign(priv, toBeSigned) - return priv, pub, sig, nil + privKey = priv default: - return nil, nil, nil, fmt.Errorf("unknown key type %q", keyType) + return nil, nil, fmt.Errorf("unknown key type %q", keyType) } + + tmpl := &x509.CertificateRequest{} + pkcs10Req, err := x509.CreateCertificateRequest(rand.Reader, tmpl, privKey) + if err != nil { + return nil, nil, fmt.Errorf("while generating stub PKCS#10 request: %w", err) + } + + return privKey, pkcs10Req, nil } func pemEncodeKey(key crypto.PrivateKey) ([]byte, error) { diff --git a/pkg/registry/certificates/podcertificaterequest/strategy.go b/pkg/registry/certificates/podcertificaterequest/strategy.go index 4ab196f6bab..0fec66de62f 100644 --- a/pkg/registry/certificates/podcertificaterequest/strategy.go +++ b/pkg/registry/certificates/podcertificaterequest/strategy.go @@ -119,7 +119,7 @@ func NewStatusStrategy(strategy *Strategy, authorizer authorizer.Authorizer, clo // and should not be modified by the user. func (s *StatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { fields := map[fieldpath.APIVersion]*fieldpath.Set{ - "certificates.k8s.io/v1alpha1": fieldpath.NewSet( + "certificates.k8s.io/v1beta1": fieldpath.NewSet( fieldpath.MakePathOrDie("spec"), ), } diff --git a/staging/src/k8s.io/api/certificates/v1beta1/types.go b/staging/src/k8s.io/api/certificates/v1beta1/types.go index acfabbfe9fd..6e8c8a01954 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/types.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/types.go @@ -438,8 +438,7 @@ type PodCertificateRequestSpec struct { // +default=86400 MaxExpirationSeconds *int32 `json:"maxExpirationSeconds,omitempty" protobuf:"varint,8,opt,name=maxExpirationSeconds"` - // pkixPublicKey is the PKIX-serialized public key the signer will issue the - // certificate to. + // The PKIX-serialized public key the signer will issue the certificate to. // // The key must be one of RSA3072, RSA4096, ECDSAP256, ECDSAP384, ECDSAP521, // or ED25519. Note that this list may be expanded in the future. @@ -451,11 +450,16 @@ type PodCertificateRequestSpec struct { // "UnsupportedKeyType". It may also suggest a key type that it does support // in the message field. // - // +required + // DEPRECATED: This field is replaced by StubPKCS10Request. If + // StubPKCS10Request is set, this field must be empty. Signer + // implementations should extract the public key from the StubPKCS10Request + // field. + // + // +optional PKIXPublicKey []byte `json:"pkixPublicKey" protobuf:"bytes,9,opt,name=pkixPublicKey"` - // proofOfPossession proves that the requesting kubelet holds the private - // key corresponding to pkixPublicKey. + // A proof that the requesting kubelet holds the private key corresponding + // to pkixPublicKey. // // It is contructed by signing the ASCII bytes of the pod's UID using // `pkixPublicKey`. @@ -472,12 +476,42 @@ type PodCertificateRequestSpec struct { // golang library function crypto/ecdsa.SignASN1) // // If the key is an ED25519 key, the the signature is as described by the - // [ED25519 Specification](https://ed25519.cr.yp.to/) (as implemented by - // the golang library crypto/ed25519.Sign). + // [ED25519 Specification](https://ed25519.cr.yp.to/) (as implemented by the + // golang library crypto/ed25519.Sign). // - // +required + // DEPRECATED: This field is replaced by StubPKCS10Request. If + // StubPKCS10Request is set, this field must be empty. + // + // +optional ProofOfPossession []byte `json:"proofOfPossession" protobuf:"bytes,10,opt,name=proofOfPossession"` + // A PKCS#10 certificate signing request (DER-serialized) generated by + // Kubelet using the subject private key. + // + // Most signer implementations will ignore the contents of the CSR except to + // extract the subject public key. The API server automatically verifies the + // CSR signature during admission, so the signer does not need to repeat the + // verification. + // + // The subject public key must be one of RSA3072, RSA4096, ECDSAP256, + // ECDSAP384, ECDSAP521, or ED25519. Note that this list may be expanded in + // the future. + // + // Signer implementations do not need to support all key types supported by + // kube-apiserver and kubelet. If a signer does not support the key type + // used for a given PodCertificateRequest, it must deny the request by + // setting a status.conditions entry with a type of "Denied" and a reason of + // "UnsupportedKeyType". It may also suggest a key type that it does support + // in the message field. + // + // Some CA implementations require that the client (the signer + // implementation, in this case) provide a PKCS#10 certificate signing + // request, even if the CA only extracts the subject public key from the + // request. To enable compatibility with these CAs, Kubelet will generate a + // stub PKCS#10 request that the signer implementation can then pass on to + // the CA. + StubPKCS10Request []byte `json:"stubPKCS10Request" protobuf:"bytes,12,opt,name=stubPKCS10Request"` + // unverifiedUserAnnotations allow pod authors to pass additional information to // the signer implementation. Kubernetes does not restrict or validate this // metadata in any way. diff --git a/test/utils/hermeticpodcertificatesigner/hermeticpodcertificatesigner.go b/test/utils/hermeticpodcertificatesigner/hermeticpodcertificatesigner.go index 0d46c34ab11..34fa4e6bad8 100644 --- a/test/utils/hermeticpodcertificatesigner/hermeticpodcertificatesigner.go +++ b/test/utils/hermeticpodcertificatesigner/hermeticpodcertificatesigner.go @@ -229,9 +229,9 @@ func (c *Controller) handlePCR(ctx context.Context, pcr *certsv1beta1.PodCertifi return nil } - subjectPublicKey, err := x509.ParsePKIXPublicKey(pcr.Spec.PKIXPublicKey) + req, err := x509.ParseCertificateRequest(pcr.Spec.StubPKCS10Request) if err != nil { - return fmt.Errorf("while parsing subject public key: %w", err) + return fmt.Errorf("while parsing PKCS#10 request: %w", err) } // If our signer had an opinion on which key types were allowable, it would @@ -276,7 +276,7 @@ func (c *Controller) handlePCR(ctx context.Context, pcr *certsv1beta1.PodCertifi return fmt.Errorf("while parsing signing certificate: %w", err) } - subjectCertDER, err := x509.CreateCertificate(rand.Reader, template, signingCert, subjectPublicKey, c.caKeys[len(c.caKeys)-1]) + subjectCertDER, err := x509.CreateCertificate(rand.Reader, template, signingCert, req.PublicKey, c.caKeys[len(c.caKeys)-1]) if err != nil { return fmt.Errorf("while signing subject cert: %w", err) }