Repository: flightctl/flightctl
Pull request #986: EDM-1214: Perform better validation of CSR and enrollment requests
Author: login: kot-begemot-uk
Created: 2025-03-11T13:58:21Z | Merged: 2025-03-20T02:09:18Z

1. Add comparisons between ASN.1 and JSON CSR metadata
2. Add checks between mTLS identity and requested CSR identity to allow the use of the CSR interface for renewals
3. Make adding prefixes one-off and allow for prefixes in CN comparisons
4. This should allow the use of CSR interface for renewals

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
  - Enhanced validation checks during certificate signing requests to ensure proper handling of Common Names (CN), preventing identity mismatches and unauthorized certificate renewals.
  - Improved error handling to provide clearer feedback on CN discrepancies.
  - Strengthened validation in enrollment requests to ensure only valid and expected CNs are processed, reducing the risk of identity theft.
  - Added checks for specific prefixes in names and fingerprints to streamline processing.
  - Introduced a new method to validate Certificate Signing Request (CSR) parameters, enhancing error handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/internal/cli/certificate.go b/internal/cli/certificate.go
index 523e2030dd..594dfa3091 100644
--- a/internal/cli/certificate.go
+++ b/internal/cli/certificate.go
@@ -6,6 +6,7 @@ import (
 	"crypto"
 	"crypto/rand"
 	"crypto/x509"
+	"crypto/x509/pkix"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/pem"
@@ -281,9 +282,14 @@ func createCsr(o *CertificateOptions, name string, priv crypto.PrivateKey) ([]by
 		return nil, err
 	}
 
-	// the CN is going to be a FC-generated UUID, populated at signing time
+	if name == "" {
+		name = uuid.NewString()
+	}
 	template := &x509.CertificateRequest{
 		SignatureAlgorithm: x509.ECDSAWithSHA256,
+		Subject: pkix.Name{
+			CommonName: name,
+		},
 	}
 
 	csrInner, err := x509.CreateCertificateRequest(rand.Reader, template, priv)
diff --git a/internal/crypto/cert.go b/internal/crypto/cert.go
index c35a5ec7ef..7034afd553 100644
--- a/internal/crypto/cert.go
+++ b/internal/crypto/cert.go
@@ -10,6 +10,7 @@ import (
 	"math/big"
 	"os"
 	"path/filepath"
+	"strings"
 	"time"
 
 	oscrypto "github.com/openshift/library-go/pkg/crypto"
@@ -22,7 +23,18 @@ const ClientBootstrapCommonNamePrefix = "client-enrollment-"
 const AdminCommonName = "flightctl-admin"
 const DeviceCommonNamePrefix = "device:"
 
+var allowedPrefixes = [...]string{
+	DeviceCommonNamePrefix,
+	ClientBootstrapCommonNamePrefix,
+}
+
 func BootstrapCNFromName(name string) string {
+
+	for _, prefix := range allowedPrefixes {
+		if strings.HasPrefix(name, prefix) {
+			return name
+		}
+	}
 	return ClientBootstrapCommonNamePrefix + name
 }
 
@@ -30,6 +42,9 @@ func CNFromDeviceFingerprint(fingerprint string) (string, error) {
 	if len(fingerprint) < 16 {
 		return "", errors.New("device fingerprint must have 16 characters at least")
 	}
+	if strings.HasPrefix(fingerprint, DeviceCommonNamePrefix) {
+		return fingerprint, nil
+	}
 	return DeviceCommonNamePrefix + fingerprint, nil
 }
 
diff --git a/internal/service/certificatesigningrequest.go b/internal/service/certificatesigningrequest.go
index 084d9aab02..bd9853b02f 100644
--- a/internal/service/certificatesigningrequest.go
+++ b/internal/service/certificatesigningrequest.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	api "github.com/flightctl/flightctl/api/v1alpha1"
+	"github.com/flightctl/flightctl/internal/api_server/middleware"
 	"github.com/flightctl/flightctl/internal/crypto"
 	"github.com/flightctl/flightctl/internal/flterrors"
 	"github.com/flightctl/flightctl/internal/store"
@@ -77,11 +78,18 @@ func signApprovedCertificateSigningRequest(ca *crypto.CA, request api.Certificat
 	}
 
 	// the CN will need the enrollment prefix applied;
-	// if a CN is not specified in the CSR, generate a UUID to represent the future device
+	// if the certificate is being renewed, the name will have an existing prefix.
+	// we do not touch in this case.
+
 	u := csr.Subject.CommonName
-	if u == "" {
-		u = uuid.NewString()
+
+	// Once we move all prefixes/name formation to the client this can become a simple
+	// comparison of u and *request.Metadata.Name
+
+	if crypto.BootstrapCNFromName(u) != crypto.BootstrapCNFromName(*request.Metadata.Name) {
+		return nil, fmt.Errorf("%w - CN %s Metadata %s mismatch", flterrors.ErrSignCert, u, *request.Metadata.Name)
 	}
+
 	csr.Subject.CommonName = crypto.BootstrapCNFromName(u)
 
 	expiry := DefaultEnrollmentCertExpirySeconds
@@ -155,6 +163,27 @@ func (h *ServiceHandler) ListCertificateSigningRequests(ctx context.Context, par
 	}
 }
 
+func (h *ServiceHandler) verifyCSRParameters(ctx context.Context, csr api.CertificateSigningRequest) error {
+
+	// Crypto validation
+	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)
+
+	// Note - if auth is disabled and there is no mTLS handshake we get ok == False.
+	// We cannot check anything in that case.
+
+	if ok {
+		if csr.Spec.SignerName != "enrollment" {
+			if csr.Metadata.Name == nil {
+				return errors.New("invalid csr record - no name in metadata")
+			}
+			if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
+				return errors.New("denied attempt to renew other entity certificate")
+			}
+		}
+	}
+	return nil
+}
+
 func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, csr api.CertificateSigningRequest) (*api.CertificateSigningRequest, api.Status) {
 	orgId := store.NullOrgId
 
@@ -166,6 +195,11 @@ func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, cs
 		return nil, api.StatusBadRequest(errors.Join(errs...).Error())
 	}
 
+	err := h.verifyCSRParameters(ctx, csr)
+	if err != nil {
+		return nil, api.StatusUnauthorized(err.Error())
+	}
+
 	result, err := h.store.CertificateSigningRequest().Create(ctx, orgId, &csr)
 	if err != nil {
 		return nil, StoreErrorToApiStatus(err, true, api.CertificateSigningRequestKind, csr.Metadata.Name)
@@ -174,6 +208,7 @@ func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, cs
 	if result.Spec.SignerName == "enrollment" {
 		h.autoApprove(ctx, orgId, result)
 	}
+
 	if api.IsStatusConditionTrue(result.Status.Conditions, api.CertificateSigningRequestApproved) {
 		h.signApprovedCertificateSigningRequest(ctx, orgId, result)
 	}
@@ -254,6 +289,11 @@ func (h *ServiceHandler) ReplaceCertificateSigningRequest(ctx context.Context, n
 		return nil, api.StatusBadRequest("resource name specified in metadata does not match name in path")
 	}
 
+	err := h.verifyCSRParameters(ctx, csr)
+	if err != nil {
+		return nil, api.StatusUnauthorized(err.Error())
+	}
+
 	result, created, err := h.store.CertificateSigningRequest().CreateOrUpdate(ctx, orgId, &csr)
 	if err != nil {
 		return nil, StoreErrorToApiStatus(err, created, api.CertificateSigningRequestKind, &name)
diff --git a/internal/service/enrollmentrequest.go b/internal/service/enrollmentrequest.go
index dc5a084ca5..398f7d42c3 100644
--- a/internal/service/enrollmentrequest.go
+++ b/internal/service/enrollmentrequest.go
@@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity theft, csr: %s, metadata %s", supplied, desired)
+	}
+	csr.Subject.CommonName = desired
+
 	if err := csr.CheckSignature(); err != nil {
 		return fmt.Errorf("failed to verify signature of CSR: %w", err)
 	}


Reviews:
user: login: lkatalin
body: A few small comments / questions
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T12:26:20Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T13:16:31Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T13:16:52Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T13:17:22Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4571bcbacfd1bb26897617e9dceb765b7c67ec3b and 65e49f1f423c94b95afb41937d05bd7c70c7c4bb.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `internal/cli/certificate.go` (2 hunks)
* `internal/crypto/cert.go` (2 hunks)
* `internal/service/certificatesigningrequest.go` (4 hunks)
* `internal/service/enrollmentrequest.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* internal/crypto/cert.go
* internal/cli/certificate.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: smoke
* GitHub Check: integration-tests
* GitHub Check: unit-tests
* GitHub Check: lint
* GitHub Check: e2e

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (5)</summary>

`11-11`: **Appropriate addition of middleware import**

The added import correctly brings in the middleware package that provides access to the TLS Common Name from the context, which is essential for the enhanced validation logic.

---

`80-83`: **Clearer comment explaining CN handling logic**

The improved comment clarifies the behavior when handling certificate renewal, explaining that existing prefixes will be preserved. This is an important distinction that helps developers understand the certificate naming approach.

---

`86-88`: **Good explanation of future improvements**

This comment properly documents the reasoning behind the current implementation and hints at future simplification once all prefix/name formation is moved to the client side.

---

`89-91`: **Robust CN validation implementation**

The change correctly implements a more robust validation by using `BootstrapCNFromName()` on both values before comparison, which handles prefixes consistently regardless of whether they were added client-side or server-side.

---

`176-188`: **Enhanced security through mTLS identity validation**

This implementation properly checks the TLS Common Name against the requested CSR identity, providing an essential security measure for certificate renewal. The code correctly:
1. Extracts the CN from the context
2. Handles the case when auth is disabled
3. Performs the validation only for non-enrollment signers
4. Returns a clear unauthorized error message when identity mismatch is detected

</details>
<details>
<summary>internal/service/enrollmentrequest.go (2)</summary>

`36-40`: **Good extraction and validation of supplied CN**

This change correctly extracts the Common Name from the CSR and performs proper validation, providing clear error messaging if the extraction fails. This prevents malformed CSRs from being processed.

---

`41-44`: **Proper extraction of desired CN from metadata**

The code properly retrieves the expected CN value from the enrollment request's metadata name, maintaining the existing error handling if this process fails.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:20:38Z
user: login: lkatalin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T16:22:10Z
user: login: lkatalin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T16:29:19Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T02:52:17Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T08:21:48Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T08:22:33Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>internal/service/enrollmentrequest.go (1)</summary><blockquote>

`41-44`: **Consider improving error message clarity**

The error message states "error setting CN in CSR", but the operation is actually extracting the CN from the enrollment request's metadata name. Consider updating the message to better reflect the operation being performed.

```diff
-		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
+		return fmt.Errorf("approveAndSignEnrollmentRequest: error extracting CN from enrollment request's metadata name: %w", err)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 402e63afc809dad57a2df1ca9471d50cabfca46b and a383271d6e819d8bff6a4824d24b8fd5554cf70e.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `internal/cli/certificate.go` (2 hunks)
* `internal/crypto/cert.go` (2 hunks)
* `internal/service/certificatesigningrequest.go` (4 hunks)
* `internal/service/enrollmentrequest.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* internal/service/certificatesigningrequest.go
* internal/crypto/cert.go
* internal/cli/certificate.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: smoke
* GitHub Check: e2e
* GitHub Check: integration-tests

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>internal/service/enrollmentrequest.go (3)</summary>

`36-39`: **Good security practice: validating the CN from CSR**

This change correctly extracts and validates the CN from the CSR's CommonName using `crypto.CNFromDeviceFingerprint()`. This is a security enhancement that ensures the CSR contains a valid CN before proceeding with the approval process.

---

`46-49`: **Fix typo in error message: "thef" should be "theft"**

The error message contains a typo: "identity thef" should be "identity theft".

```diff
-		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity theft, csr: %s, metadata %s", supplied, desired)
```

---

`46-49`: **Strong identity validation enhancement**

This new validation check is an excellent security enhancement that compares the CN from the CSR with the CN derived from the enrollment request's metadata name. This effectively prevents identity spoofing attempts where an attacker might try to submit a CSR with a different identity than the one in the enrollment request.

The identity comparison is straightforward and provides both values in the error message for easier debugging.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T11:27:01Z
user: login: lkatalin
body: IMO this basically looks good. However, one thing to consider that I should have thought of earlier: the validations in `CreateCertificateSigningRequest` and `ReplaceCertificateSigningRequest` should be roughly the same. A `POST` will get routed to the former and a `PUT` (which we are using in the CLI) will be routed to the latter. So some of these validations and checks should be consolidated across the two. It looks like `Replace...` already has some checks like `if name != *csr.Metadata.Name`.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T14:57:36Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T18:25:05Z
user: login: lkatalin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T18:26:07Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T18:26:23Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T18:28:02Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T18:31:08Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T18:32:42Z
user: login: lkatalin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T20:25:47Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T08:40:54Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (2)</summary><blockquote>

`174-182`: **Consider handling the auth-disabled case more explicitly.**

While the current implementation correctly skips validation when auth is disabled, it might be clearer to add a comment or log entry in the `!ok` case to explicitly document that validation is being skipped due to disabled authentication.

```diff
  if ok {
    if csr.Spec.SignerName != "enrollment" {
      if csr.Metadata.Name == nil {
        return errors.New("Invalid csr record - no name in metadata")
      }
      if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
        return errors.New("Denied attempt to renew other entity certificate")
      }
    }
+  } else {
+    // Auth is disabled, skipping CSR identity validation
  }
  return nil
```

---

`179-181`: **Consider using an error constant or StatusUnauthorized directly per previous feedback.**

Previous PR comments from hexfusion noted that using `api.StatusUnauthorized` directly would be preferable to creating an error string.

```diff
  if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
-    return errors.New("Denied attempt to renew other entity certificate")
+    return api.StatusUnauthorized("Denied attempt to renew other entity certificate")
  }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9ccc2eec0c16f82170fc81a68744ddefb09ecff0 and 45c86afb1fc9980b7a37a275df6e975c4f5673df.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `internal/cli/certificate.go` (2 hunks)
* `internal/crypto/cert.go` (2 hunks)
* `internal/service/certificatesigningrequest.go` (6 hunks)
* `internal/service/enrollmentrequest.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* internal/cli/certificate.go
* internal/crypto/cert.go
* internal/service/enrollmentrequest.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (13)</summary>

* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: lint
* GitHub Check: integration-tests
* GitHub Check: e2e
* GitHub Check: unit-tests
* GitHub Check: smoke

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (6)</summary>

`11-11`: **Import added for TLS context verification.**

This new import is needed to access the TLS CommonName from the context in the new validation logic.

---

`81-83`: **Enhanced comment explains certificate renewal prefix handling.**

This comment clarifies how prefixes are handled during renewal, which is helpful for future maintenance.

---

`86-91`: **Improved CSR validation logic with more robust CN comparison.**

The code now uses `crypto.BootstrapCNFromName` for both the CSR's CommonName and the metadata name, ensuring consistent comparison regardless of prefixes. This addresses the PR objective to compare ASN.1 and JSON metadata by ensuring their CommonNames match.

---

`198-201`: **CSR creation now performs proper validation.**

This integrates the new verification function into the create path, ensuring CSRs cannot be created with mismatched identities. The HTTP 401 Unauthorized is an appropriate status code for this scenario.

---

`292-296`: **CSR replacement now performs proper validation.**

The same verification is now applied consistently to the replace method as well, maintaining identity validation across both API endpoints.

---

`166-185`: <details>
<summary>❓ Verification inconclusive</summary>

**New verification function consolidates CSR parameter validation.**

This function implements the key PR objective of comparing mTLS identity with requested CSR identity. The validation now:
1. Extracts the TLS CommonName from the context (when auth is enabled)
2. Performs appropriate checks based on signer type
3. Ensures the entity can't create/modify certificates for other entities



Let's verify that the error message is clear and concise:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the error message clarity and verify the implementation

# Look for occurrences of the error messages in tests or other code
rg "Invalid csr record - no name in metadata" --type go
rg "Denied attempt to renew other entity certificate" --type go

# Check if there are any test cases verifying this functionality
rg -A 10 "TestVerifyCSRParameters|test.*CSR.*Parameters" --type go
```

Length of output: 424

---



</details>

**Verification Complete for CSR Parameter Validation**

The updated function in `internal/service/certificatesigningrequest.go` now performs the intended checks:
- It extracts the TLS CommonName from the context when authentication is enabled.
- It validates the CSR based on the signer type.
- It correctly returns clear error messages when the CSR record is missing a metadata name or when an entity attempts to renew another's certificate.

However, note that our search for test cases (e.g., those matching "TestVerifyCSRParameters") did not return any results. Please verify manually that this functionality is covered by tests or consider adding tests to ensure robust validation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T11:19:09Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T18:29:12Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T18:31:03Z
user: login: fzdarsky
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:31:45Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (2)</summary><blockquote>

`166-185`: **New validation method checks TLS identity against requested CSR identity**

The new `verifyCSRParameters` method effectively implements the comparison between mTLS identity and requested CSR identity, which aligns with the PR objective of enabling the CSR interface for renewal processes.

A few suggestions:

1. The error messages could be more descriptive to help with troubleshooting
2. Consider following Go error message style conventions (lowercase first letter)


```diff
-				return errors.New("invalid csr record - no name in metadata")
+				return errors.New("invalid csr record: no name in metadata")
-				return errors.New("denied attempt to renew other entity certificate")
+				return errors.New("denied attempt to renew certificate: TLS CN does not match metadata name")
```

---

`166-166`: **Consider adding documentation for the new method**

The new `verifyCSRParameters` method would benefit from a comment describing its purpose, expected inputs, and return values following Go documentation standards.

```diff
+// verifyCSRParameters validates that the CSR parameters match expected values,
+// particularly ensuring that for non-enrollment signers, the requesting entity
+// can only renew its own certificate by comparing the TLS Common Name with
+// the CSR metadata name.
 func (h *ServiceHandler) verifyCSRParameters(ctx context.Context, csr api.CertificateSigningRequest) error {
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6a6578ae73338e22f7a8b05a8662d8cbfeb91011 and f06add45a6bc07c1fd330816250c8e8b8562d9a0.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `internal/cli/certificate.go` (2 hunks)
* `internal/crypto/cert.go` (2 hunks)
* `internal/service/certificatesigningrequest.go` (6 hunks)
* `internal/service/enrollmentrequest.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* internal/crypto/cert.go
* internal/cli/certificate.go
* internal/service/enrollmentrequest.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (9)</summary>

* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: e2e

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (5)</summary>

`80-83`: **Updated comments to clarify certificate renewal handling**

The added comments nicely explain how prefixes are handled differently for certificate renewals, which supports the implementation of using the CSR interface for renewal processes as described in the PR objectives.

---

`89-91`: **Improved CN validation using consistent transformation**

Good improvement applying `BootstrapCNFromName` to both sides of the comparison, ensuring consistent handling of prefixes regardless of whether they were added on client or server side. This better supports renewal scenarios where a prefix may already be present.

---

`174-183`: **Thorough validation of CSR identity for non-enrollment signers**

The conditional logic correctly implements different validation rules for enrollment vs. non-enrollment signers, which is important for supporting different certificate issuance scenarios.

The check at line 179 that ensures the TLS Common Name matches the expected Common Name from metadata is particularly important for security when handling certificate renewals.

---

`198-201`: **Integration of CSR parameter validation in create operation**

Good integration of the new validation logic in the create operation with appropriate error handling. This ensures that CSR requests are properly validated before processing.

---

`292-295`: **Integration of CSR parameter validation in replace operation**

Excellent reuse of the same validation logic in the replace operation, maintaining consistency across different entry points. This implementation follows good DRY principles by centralizing the validation logic.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:40:32Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T13:12:45Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T13:18:08Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T13:18:59Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T13:24:00Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T13:26:51Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T13:41:51Z
user: login: hexfusion
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T13:48:55Z
user: login: kot-begemot-uk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T14:45:42Z
user: login: hexfusion
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-18T16:25:45Z
user: login: lkatalin
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T17:29:51Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (2)</summary><blockquote>

`166-185`: **Well-structured CSR parameter verification function**

The new `verifyCSRParameters` function implements security checks to verify that the client isn't attempting to create or replace a CSR for an entity other than itself. This prevents potential identity spoofing.

A few suggestions:
1. Consider using `api.StatusUnauthorized` directly in this function instead of returning a generic error
2. Consider adding a debug log when auth is disabled and verification is skipped


```diff
func (h *ServiceHandler) verifyCSRParameters(ctx context.Context, csr api.CertificateSigningRequest) error {

	// Crypto validation
	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)

	// Note - if auth is disabled and there is no mTLS handshake we get ok == False.
	// We cannot check anything in that case.

	if ok {
		if csr.Spec.SignerName != "enrollment" {
			if csr.Metadata.Name == nil {
-				return errors.New("invalid csr record - no name in metadata")
+				return fmt.Errorf("invalid csr record - no name in metadata")
			}
			if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
-				return errors.New("denied attempt to renew other entity certificate")
+				return fmt.Errorf("denied attempt to renew other entity certificate")
			}
		}
+	} else {
+		h.log.Debug("Auth disabled or no mTLS handshake, skipping CSR parameter verification")
	}
	return nil
}
```

---

`174-183`: **Consider consolidating validation logic**

As mentioned in PR comments, it might be beneficial to consolidate all validation checks into a single location, such as `api/v1alpha/validation.go` or `internal/util/validation/validation.go`. This would promote code reuse across both Create and Replace functions.


Consider moving the CSR validation logic to a dedicated validation package to follow the DRY principle. This would make the code more maintainable and allow for easier testing.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dc304a816f2c3bb325ae1e111d5d3183f019a147 and 889f3f9bc834076628687a4b26be4791d4ae08a0.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `internal/cli/certificate.go` (2 hunks)
* `internal/crypto/cert.go` (2 hunks)
* `internal/service/certificatesigningrequest.go` (6 hunks)
* `internal/service/enrollmentrequest.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* internal/service/enrollmentrequest.go
* internal/cli/certificate.go
* internal/crypto/cert.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>internal/service/certificatesigningrequest.go (4)</summary><blockquote>

<details>
<summary>internal/crypto/cert.go (1) (1)</summary>

* `BootstrapCNFromName` (31-39)

</details>
<details>
<summary>internal/flterrors/flterrors.go (1) (1)</summary>

* `ErrSignCert` (37-37)

</details>
<details>
<summary>api/v1alpha1/types.gen.go (1) (1)</summary>

* `CertificateSigningRequest` (228-243)

</details>
<details>
<summary>api/v1alpha1/error.go (1) (1)</summary>

* `StatusUnauthorized` (46-48)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (10)</summary>

* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: smoke
* GitHub Check: e2e

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (6)</summary>

`11-11`: **Good addition of the middleware import**

Adding the middleware import is necessary to access the TLS Common Name context key, which enables the new identity verification functionality.

---

`81-83`: **LGTM! Improved comment clarity**

The updated comments clearly explain the certificate renewal scenario and prefix handling logic, making the code more maintainable.

---

`86-91`: **Robust CN comparison logic for certificate validation**

This change adds important validation by comparing the CN from the CSR with the CN derived from the metadata name. The comment correctly explains that once prefix/name formatting is moved to the client side, this comparison can be simplified.

---

`198-201`: **Good integration of verification into Create endpoint**

The integration of the verification logic into the `CreateCertificateSigningRequest` function ensures that CSR creation is secure, using the caller's identity to validate permissions.

---

`292-295`: **Good integration of verification into Replace endpoint**

Similar to the Create endpoint, adding verification to the `ReplaceCertificateSigningRequest` function ensures consistent security checks across operations.

---

`89-91`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify this CN comparison handles all renewal scenarios**

The comparison logic using `crypto.BootstrapCNFromName` on both sides ensures that prefixes are consistently applied or preserved. However, it's worth validating whether this handles all renewal scenarios correctly.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check if `crypto.BootstrapCNFromName` is used consistently across the codebase to handle CN prefixes

# Find all uses of BootstrapCNFromName function
echo "Finding all uses of BootstrapCNFromName:"
rg "BootstrapCNFromName" --type go

# Check if there are any direct string comparisons with CNs that might not be using BootstrapCNFromName
echo -e "\nChecking for potential CN comparisons that might not use BootstrapCNFromName:"
rg "CommonName.*==" --type go -A 2 -B 2
rg "CommonName.*!=" --type go -A 2 -B 2
```

Length of output: 1112

---



</details>

**CN Normalization Verification**

The comparison in `internal/service/certificatesigningrequest.go` consistently applies `crypto.BootstrapCNFromName` to both sides, which should correctly normalize the CN for renewal scenarios. However, note that in `internal/crypto/cert.go` there’s a direct comparison of `CommonName` without this normalization. Please verify that this direct check is intentional and that it doesn’t introduce discrepancies during certificate renewals.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:20:25Z

Review comments:
diff_hunk: @@ -23,13 +24,22 @@ const AdminCommonName = "flightctl-admin"
 const DeviceCommonNamePrefix = "device:"
 
 func BootstrapCNFromName(name string) string {
+	if strings.HasPrefix(name, ClientBootstrapCommonNamePrefix) {
path: internal/crypto/cert.go
user: login: lkatalin
body: It would be good to have some list or array of prefixes so that we can just do the check once and have it include whatever prefixes may be added in the future.
created_at: 2025-03-11T15:24:51Z
author_association: COLLABORATOR
diff_hunk: @@ -79,9 +80,11 @@ func signApprovedCertificateSigningRequest(ca *crypto.CA, request api.Certificat
 	// the CN will need the enrollment prefix applied;
 	// if a CN is not specified in the CSR, generate a UUID to represent the future device
 	u := csr.Subject.CommonName
-	if u == "" {
-		u = uuid.NewString()
+
+	if crypto.BootstrapCNFromName(u) != crypto.BootstrapCNFromName(*request.Metadata.Name) {
path: internal/service/certificatesigningrequest.go
user: login: lkatalin
body: Why not `if u != *request.Metadata.Name`?
created_at: 2025-03-11T17:31:02Z
author_association: COLLABORATOR
diff_hunk: @@ -165,6 +168,15 @@ func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, cs
 	if errs := csr.Validate(); len(errs) > 0 {
 		return nil, api.StatusBadRequest(errors.Join(errs...).Error())
 	}
+	// Crypto validation
+	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)
+	if ok {
path: internal/service/certificatesigningrequest.go
user: login: lkatalin
body: What do we do if `!ok`?
created_at: 2025-03-12T12:18:43Z
author_association: COLLABORATOR
diff_hunk: @@ -79,9 +80,11 @@ func signApprovedCertificateSigningRequest(ca *crypto.CA, request api.Certificat
 	// the CN will need the enrollment prefix applied;
 	// if a CN is not specified in the CSR, generate a UUID to represent the future device
 	u := csr.Subject.CommonName
-	if u == "" {
-		u = uuid.NewString()
+
+	if crypto.BootstrapCNFromName(u) != crypto.BootstrapCNFromName(*request.Metadata.Name) {
path: internal/service/certificatesigningrequest.go
user: login: kot-begemot-uk
body: If we allow both client and server side CN formation, one can be transformed the other one not.
Example - client supplies a "name". At present it is the server which adds enrollment- to it. So a comparison needs to check for both cases because it may or may not contain it.
If we move all CN modifications to the client the CNF transform as a part of the check is no longer necessary.
created_at: 2025-03-12T13:16:31Z
author_association: COLLABORATOR
diff_hunk: @@ -23,13 +24,22 @@ const AdminCommonName = "flightctl-admin"
 const DeviceCommonNamePrefix = "device:"
 
 func BootstrapCNFromName(name string) string {
+	if strings.HasPrefix(name, ClientBootstrapCommonNamePrefix) {
path: internal/crypto/cert.go
user: login: kot-begemot-uk
body: I had it in an earlier version. Sure. We can do that.
created_at: 2025-03-12T13:16:52Z
author_association: COLLABORATOR
diff_hunk: @@ -165,6 +168,15 @@ func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, cs
 	if errs := csr.Validate(); len(errs) > 0 {
 		return nil, api.StatusBadRequest(errors.Join(errs...).Error())
 	}
+	// Crypto validation
+	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)
+	if ok {
path: internal/service/certificatesigningrequest.go
user: login: kot-begemot-uk
body: Not OK means we are running with auth disabled. We simply skip all of this as it does not apply.
created_at: 2025-03-12T13:17:22Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
+	}
+	csr.Subject.CommonName = desired
path: internal/service/enrollmentrequest.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Critical identity validation check**

The implementation correctly compares the supplied and desired CN values, providing an effective security measure against identity theft attempts. The error message includes both values for easier debugging.

However, there's a typo in the error message: "identity thef" should be "identity theft".



Fix the typo in the error message:

```diff
-	return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
+	return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity theft, csr: %s, metadata %s", supplied, desired)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
	if desired != supplied {
		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity theft, csr: %s, metadata %s", supplied, desired)
	}
	csr.Subject.CommonName = desired
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dc304a8
created_at: 2025-03-12T16:20:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,9 +80,11 @@ func signApprovedCertificateSigningRequest(ca *crypto.CA, request api.Certificat
 	// the CN will need the enrollment prefix applied;
 	// if a CN is not specified in the CSR, generate a UUID to represent the future device
 	u := csr.Subject.CommonName
-	if u == "" {
-		u = uuid.NewString()
+
+	if crypto.BootstrapCNFromName(u) != crypto.BootstrapCNFromName(*request.Metadata.Name) {
path: internal/service/certificatesigningrequest.go
user: login: lkatalin
body: Okay, that makes sense. It's fine this way
created_at: 2025-03-12T16:22:10Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: lkatalin
body: nit: typo on "theft"
created_at: 2025-03-12T16:29:19Z
author_association: COLLABORATOR
diff_hunk: @@ -165,6 +173,19 @@ func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, cs
 	if errs := csr.Validate(); len(errs) > 0 {
 		return nil, api.StatusBadRequest(errors.Join(errs...).Error())
 	}
+	// Crypto validation
+	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)
+
+	// Note - if auth is disabled and there is no mTLS handshake we get ok == False.
+	// We cannot check anything in that case.
+
+	if ok {
+		if csr.Spec.SignerName != "enrollment" {
+			if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
+				return nil, api.StatusUnauthorized(errors.New("Denied attempt to renew other entity certificate").Error())
path: internal/service/certificatesigningrequest.go
user: login: hexfusion
body: no need to create an error if you need a string?
```suggestion
				return nil, api.StatusUnauthorized("Denied attempt to renew other entity certificate")
```
created_at: 2025-03-13T02:49:50Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
path: internal/service/enrollmentrequest.go
user: login: hexfusion
body: can name ever be nil?
created_at: 2025-03-13T02:51:13Z
author_association: COLLABORATOR
diff_hunk: @@ -165,6 +173,19 @@ func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, cs
 	if errs := csr.Validate(); len(errs) > 0 {
 		return nil, api.StatusBadRequest(errors.Join(errs...).Error())
 	}
+	// Crypto validation
+	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)
+
+	// Note - if auth is disabled and there is no mTLS handshake we get ok == False.
+	// We cannot check anything in that case.
+
+	if ok {
+		if csr.Spec.SignerName != "enrollment" {
+			if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
+				return nil, api.StatusUnauthorized(errors.New("Denied attempt to renew other entity certificate").Error())
path: internal/service/certificatesigningrequest.go
user: login: kot-begemot-uk
body: Fair point. Will do.
created_at: 2025-03-13T08:21:48Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
path: internal/service/enrollmentrequest.go
user: login: kot-begemot-uk
body: No with a legit client - it is always initialized. This is not an excuse not to check. 
created_at: 2025-03-13T08:22:33Z
author_association: COLLABORATOR
diff_hunk: @@ -155,6 +163,27 @@ func (h *ServiceHandler) ListCertificateSigningRequests(ctx context.Context, par
 	}
 }
 
+func (h *ServiceHandler) verifyCSRParameters(ctx context.Context, csr api.CertificateSigningRequest) (bool, error) {
+
+	// Crypto validation
+	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)
+
+	// Note - if auth is disabled and there is no mTLS handshake we get ok == False.
+	// We cannot check anything in that case.
+
+	if ok {
+		if csr.Spec.SignerName != "enrollment" {
+			if csr.Metadata.Name == nil {
+				return false, errors.New("Invalid csr record - no name in metadata")
+			}
+			if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
+				return false, errors.New("Denied attempt to renew other entity certificate")
+			}
+		}
+	}
+	return true, nil
path: internal/service/certificatesigningrequest.go
user: login: hexfusion
body: logically returning true here is a little bit of a future liability. in general a function which returns a bool should return false as the default to be defensive.
created_at: 2025-03-13T18:25:05Z
author_association: COLLABORATOR
diff_hunk: @@ -278,9 +279,14 @@ func createCsr(o *CertificateOptions, name string, priv crypto.PrivateKey) ([]by
 		return nil, err
 	}
 
-	// the CN is going to be a FC-generated UUID, populated at signing time
+	if name == "" {
+		name = uuid.NewString()
+	}
path: internal/cli/certificate.go
user: login: lkatalin
body: Also, we should do this earlier - maybe as the first step in `Run()` [here](https://github.com/flightctl/flightctl/blob/main/internal/cli/certificate.go#L142) - and populate this into `o.Name` to make sure that the files written out for the user correspond to this CN as well.
created_at: 2025-03-13T18:26:07Z
author_association: COLLABORATOR
diff_hunk: @@ -166,6 +195,12 @@ func (h *ServiceHandler) CreateCertificateSigningRequest(ctx context.Context, cs
 		return nil, api.StatusBadRequest(errors.Join(errs...).Error())
 	}
 
+	ok, err := h.verifyCSRParameters(ctx, csr)
+
path: internal/service/certificatesigningrequest.go
user: login: hexfusion
body: style; drop newline
created_at: 2025-03-13T18:26:22Z
author_association: COLLABORATOR
diff_hunk: @@ -254,6 +290,12 @@ func (h *ServiceHandler) ReplaceCertificateSigningRequest(ctx context.Context, n
 		return nil, api.StatusBadRequest("resource name specified in metadata does not match name in path")
 	}
 
+	ok, err := h.verifyCSRParameters(ctx, csr)
path: internal/service/certificatesigningrequest.go
user: login: hexfusion
body: just something to consider what value does the bool provide here that a non nil error could not solve
created_at: 2025-03-13T18:28:02Z
author_association: COLLABORATOR
diff_hunk: @@ -254,6 +290,12 @@ func (h *ServiceHandler) ReplaceCertificateSigningRequest(ctx context.Context, n
 		return nil, api.StatusBadRequest("resource name specified in metadata does not match name in path")
 	}
 
+	ok, err := h.verifyCSRParameters(ctx, csr)
path: internal/service/certificatesigningrequest.go
user: login: kot-begemot-uk
body: Fair point. 
created_at: 2025-03-13T18:31:08Z
author_association: COLLABORATOR
diff_hunk: @@ -278,9 +279,14 @@ func createCsr(o *CertificateOptions, name string, priv crypto.PrivateKey) ([]by
 		return nil, err
 	}
 
-	// the CN is going to be a FC-generated UUID, populated at signing time
+	if name == "" {
+		name = uuid.NewString()
+	}
path: internal/cli/certificate.go
user: login: kot-begemot-uk
body: I will be honest - I did not look that deep into the client side. I limited myself to the portion which was initializing the request.
created_at: 2025-03-13T18:32:42Z
author_association: COLLABORATOR
diff_hunk: @@ -278,9 +279,14 @@ func createCsr(o *CertificateOptions, name string, priv crypto.PrivateKey) ([]by
 		return nil, err
 	}
 
-	// the CN is going to be a FC-generated UUID, populated at signing time
+	if name == "" {
+		name = uuid.NewString()
+	}
path: internal/cli/certificate.go
user: login: lkatalin
body: That's okay, I can also do it as a follow-on PR if you want.
created_at: 2025-03-13T20:25:47Z
author_association: COLLABORATOR
diff_hunk: @@ -278,9 +279,14 @@ func createCsr(o *CertificateOptions, name string, priv crypto.PrivateKey) ([]by
 		return nil, err
 	}
 
-	// the CN is going to be a FC-generated UUID, populated at signing time
+	if name == "" {
+		name = uuid.NewString()
+	}
path: internal/cli/certificate.go
user: login: kot-begemot-uk
body: Let's do it this way.
created_at: 2025-03-14T08:40:54Z
author_association: COLLABORATOR
diff_hunk: @@ -254,6 +289,12 @@ func (h *ServiceHandler) ReplaceCertificateSigningRequest(ctx context.Context, n
 		return nil, api.StatusBadRequest("resource name specified in metadata does not match name in path")
 	}
 
+	err := h.verifyCSRParameters(ctx, csr)
+
+	if err != nil {
path: internal/service/certificatesigningrequest.go
user: login: hexfusion
body: style: drop newline
created_at: 2025-03-17T18:28:55Z
author_association: COLLABORATOR
diff_hunk: @@ -155,6 +163,27 @@ func (h *ServiceHandler) ListCertificateSigningRequests(ctx context.Context, par
 	}
 }
 
+func (h *ServiceHandler) verifyCSRParameters(ctx context.Context, csr api.CertificateSigningRequest) error {
+
+	// Crypto validation
+	cn, ok := ctx.Value(middleware.TLSCommonNameContextKey).(string)
+
+	// Note - if auth is disabled and there is no mTLS handshake we get ok == False.
+	// We cannot check anything in that case.
+
+	if ok {
+		if csr.Spec.SignerName != "enrollment" {
+			if csr.Metadata.Name == nil {
+				return errors.New("Invalid csr record - no name in metadata")
path: internal/service/certificatesigningrequest.go
user: login: hexfusion
body: style: errors should not start with a capital letter
created_at: 2025-03-17T18:31:03Z
author_association: COLLABORATOR
diff_hunk: @@ -22,14 +23,28 @@ const ClientBootstrapCommonNamePrefix = "client-enrollment-"
 const AdminCommonName = "flightctl-admin"
 const DeviceCommonNamePrefix = "device:"
 
+var allowedPrefixes = [...]string{
+	DeviceCommonNamePrefix,
+	ClientBootstrapCommonNamePrefix,
+}
+
path: internal/crypto/cert.go
user: login: fzdarsky
body: I'm aware this isn't what the PR tries to address, but could we make the use of prefixes a bit more consistent?
Maybe agree on a format of `[prefix:]name` (using `:` as separator, unlike in `client-enrollment-`)? Shorten `client-enrollment` to `enrollment`? Are we even using the `AdminCommonName` and do we need prefixes for users/service acconts?
created_at: 2025-03-18T09:31:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: hexfusion
body: @kot-begemot-uk why not clean this up?
created_at: 2025-03-18T13:12:45Z
author_association: COLLABORATOR
diff_hunk: @@ -22,14 +23,28 @@ const ClientBootstrapCommonNamePrefix = "client-enrollment-"
 const AdminCommonName = "flightctl-admin"
 const DeviceCommonNamePrefix = "device:"
 
+var allowedPrefixes = [...]string{
+	DeviceCommonNamePrefix,
+	ClientBootstrapCommonNamePrefix,
+}
+
path: internal/crypto/cert.go
user: login: kot-begemot-uk
body: If we are going to address this and make it consistent, can I suggest we obsolete them. Fully. 

Not in this PR of course.

This is what OU field is usually used for. 

Then the question of separator does not stand as it is a separate ASN.1 field to start off with which is added to the subject by default.
created_at: 2025-03-18T13:18:07Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: kot-begemot-uk
body: Missed it :)
Will fix it right away.
created_at: 2025-03-18T13:18:59Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: kot-begemot-uk
body: It has been fixed. Just the message is different because the code for all cases was merged into a separate validate function. 

```
		if csr.Spec.SignerName != "enrollment" {
			if csr.Metadata.Name == nil {
				return errors.New("invalid csr record - no name in metadata")
			}
			if cn != crypto.BootstrapCNFromName(*csr.Metadata.Name) {
				return errors.New("denied attempt to renew other entity certificate")
			}
		}
```
created_at: 2025-03-18T13:24:00Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: hexfusion
body: https://github.com/flightctl/flightctl/blob/f06add45a6bc07c1fd330816250c8e8b8562d9a0/internal/service/enrollmentrequest.go#L47
created_at: 2025-03-18T13:26:51Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity thef, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: kot-begemot-uk
body: Sorry, looking at wrong file. Fixed and pushed.
created_at: 2025-03-18T13:41:51Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity theft, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: hexfusion
body: is this error leaking important information? is that ok?
created_at: 2025-03-18T13:48:55Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +33,21 @@ func approveAndSignEnrollmentRequest(ca *crypto.CA, enrollmentRequest *api.Enrol
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error parsing CSR: %w", err)
 	}
 
-	csr.Subject.CommonName, err = crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
+	supplied, err := crypto.CNFromDeviceFingerprint(csr.Subject.CommonName)
+	if err != nil {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: invalid CN supplied in CSR: %w", err)
+	}
+
+	desired, err := crypto.CNFromDeviceFingerprint(*enrollmentRequest.Metadata.Name)
 	if err != nil {
 		return fmt.Errorf("approveAndSignEnrollmentRequest: error setting CN in CSR: %w", err)
 	}
 
+	if desired != supplied {
+		return fmt.Errorf("approveAndSignEnrollmentRequest: attempt to supply a fake CN, possible identity theft, csr: %s, metadata %s", supplied, desired)
path: internal/service/enrollmentrequest.go
user: login: kot-begemot-uk
body: The CN itself is not particularly private. Neither is the metadata name. 

My assessment is that it is OK and it needs to be logged.
created_at: 2025-03-18T14:45:42Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-11T13:58:32Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This pull request updates various certificate handling functions. In the CLI, the `createCsr` function now assigns a UUID as the Common Name (CN) when no value is provided and uses an explicit `pkix.Name` structure. In the crypto package, the functions handling CN extraction now check for known prefixes. Additionally, the services processing certificate signing requests have added validation logic to ensure the CN from the request matches the CN derived from metadata, with appropriate error handling introduced. Additional import statements have been added where needed.

## Changes

| File(s)                                                                                     | Change Summary                                                                                                                                                                                                                                          |
|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `internal/cli/certificate.go`                                                               | Updated `createCsr` to assign a UUID when `name` is empty and explicitly sets the Common Name using `pkix.Name`; added import for `"crypto/x509/pkix"`.                                                                                         |
| `internal/crypto/cert.go`                                                                   | Introduced prefix checks in `BootstrapCNFromName` and `CNFromDeviceFingerprint` to return or prepend designated prefixes; added import for `"strings"`.                                                                                           |
| `internal/service/certificatesigningrequest.go`<br>`internal/service/enrollmentrequest.go`    | Enhanced validation in CSR signing functions: added checks to compare the extracted CN with the metadata-derived CN and return errors on mismatches; added new method `verifyCSRParameters` for CSR validation.                                     |

## Possibly related PRs

- **flightctl/flightctl#914:** The changes in the main PR, which modify the `createCsr` function to enhance the handling of the Common Name in certificate signing requests, are related to the changes in the retrieved PR that refactor CA handling and involve the creation and management of CSRs, specifically through the introduction of new functions that streamline the process of obtaining signed certificates.
- **flightctl/flightctl#749:** The changes in the main PR, which modify the `createCsr` function to enhance the handling of the Common Name in certificate signing requests, are related to the changes in the retrieved PR that also focus on the approval process for certificate signing requests, specifically by merging the approval and denial processes.

## Suggested labels

`WIP Do Not Merge`

## Suggested reviewers

- mangelajo
- lkatalin
- avishayt

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8e454b61ff2de01268f91fffb3d0a374d9562e5b and affe5cdaa409e2a922fd8e1bf7980a040ef766a0.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `internal/cli/certificate.go` (2 hunks)
* `internal/crypto/cert.go` (2 hunks)
* `internal/service/certificatesigningrequest.go` (6 hunks)
* `internal/service/enrollmentrequest.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* internal/crypto/cert.go
* internal/service/enrollmentrequest.go
* internal/cli/certificate.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>internal/service/certificatesigningrequest.go (3)</summary><blockquote>

<details>
<summary>internal/crypto/cert.go (1) (1)</summary>

* `BootstrapCNFromName` (31-39)

</details>
<details>
<summary>internal/flterrors/flterrors.go (1) (1)</summary>

* `ErrSignCert` (37-37)

</details>
<details>
<summary>api/v1alpha1/types.gen.go (1) (1)</summary>

* `CertificateSigningRequest` (228-243)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (11)</summary>

* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:centos-stream-9-x86_64
* GitHub Check: rpm-build:fedora-eln-aarch64
* GitHub Check: e2e
* GitHub Check: lint
* GitHub Check: smoke

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>internal/service/certificatesigningrequest.go (9)</summary>

`11-11`: **Good addition of the middleware import.**

The import of the middleware package is necessary for accessing the TLS common name from the request context, which is vital for the new validation functionality.

---

`81-83`: **Clear explanation of certificate renewal handling.**

This comment clarifies the policy for handling certificates during renewal - preserving existing prefixes rather than reapplying them. This helps developers understand when the CN is modified and when it's left untouched.

---

`86-88`: **Helpful comment on future simplification.**

The comment effectively documents the temporary nature of the current implementation and outlines the simplification that will be possible once the client consistently handles prefixes/name formation.

---

`89-91`: **Improved CN validation.**

The updated comparison logic correctly handles cases where the CN may or may not already have a prefix, making the verification more robust. This allows for both newly created and renewed certificates to be properly validated.

---

`166-185`: **Well-structured validation function.**

The new `verifyCSRParameters` function properly implements the validation of certificate signing requests by checking the TLS identity against the requested identity, helping prevent unauthorized certificate renewals.

A few observations:
1. The function handles the case where authentication is disabled appropriately
2. The error messages are clear and descriptive
3. The conditional logic is well-structured with proper nesting

---

`174-182`: **Security enhancement for certificate renewals.**

This logic properly prevents one entity from requesting certificates for another entity by comparing the TLS common name (identity of the requester) with the name in the certificate request.

The explicit check for the "enrollment" signer is appropriate, as enrollment requests wouldn't have a pre-existing identity to validate against.

---

`180-180`: **Consider using status creation helper.**

For consistency with the calling code that uses `api.StatusUnauthorized()`, consider using the helper function here as well.

```diff
-                return errors.New("denied attempt to renew other entity certificate")
+                return errors.New("denied attempt to renew other entity certificate")
```

---

`198-201`: **Good integration of validation in creation flow.**

The validation function is correctly integrated into the certificate creation flow, with proper error handling and status code conversion.

---

`292-295`: **Consistent validation across creation and replacement.**

The validation function is appropriately reused in both creation and replacement workflows, ensuring consistent validation regardless of how the certificate is being processed.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=flightctl/flightctl&utm_content=986):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: lkatalin
created_at: 2025-03-12T16:31:24Z
body: I think this looks good once the tests pass. @mangelajo any thoughts from you?
author_association: COLLABORATOR
user: login: kot-begemot-uk
created_at: 2025-03-13T15:20:51Z
body: Good points, I will have a look Replace, though it somewhat returns us to our perennial discussion - do we keep CSRs or not :)
author_association: COLLABORATOR
user: login: kot-begemot-uk
created_at: 2025-03-13T15:41:22Z
body: I had a look. That is YET ANOTHER name - the name from the query string parameters. 
All other checks should still be performed in this case so we should abstract them into a func. 
author_association: COLLABORATOR
user: login: lkatalin
created_at: 2025-03-13T15:43:37Z
body: > I had a look. That is YET ANOTHER name - the name from the query string parameters. All other checks should still be performed in this case so we should abstract them into a func.

Yeah, it may be good to put all the validations in one place (such as `api/v1alpha/validation.go` and the corresponding `internal/util/validation/validation.go`) and use them in both `Create...` and `Replace`.
author_association: COLLABORATOR