Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate user provided certificates #834

Merged
merged 4 commits into from
Nov 27, 2024

Conversation

petrutlucian94
Copy link
Contributor

We'll validate user provided certificates, checking fields such as:

  • Common Name
  • Organization
  • NotBefore, NotAfter
  • DNS Subject Alternative Name

Also, we ensure that the certificates are signed by the specified CAs.

@petrutlucian94 petrutlucian94 requested a review from a team as a code owner November 22, 2024 14:02
@petrutlucian94 petrutlucian94 force-pushed the cert_validation branch 2 times, most recently from f514e7f to 3d28d7c Compare November 25, 2024 08:43
Copy link
Contributor

@bschimke95 bschimke95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, only minor nits and a question

src/k8s/pkg/k8sd/pki/control_plane.go Outdated Show resolved Hide resolved
src/k8s/pkg/k8sd/pki/control_plane_test.go Outdated Show resolved Hide resolved
}
}
for _, ip := range check.IPSANs {
if err := cert.VerifyHostname("[" + ip + "]"); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this IPv6-safe?
I guess ip cannot be [<ipv6>] so that we would have double brackets, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The ip address wouldn't include a port, so no need for square brackets. I haven't checked how VerifyHostname handles double brackets, we could just do something like strings.Trim(ip, "[]").

However, we aren't using this check right now so we might as well remove it.

Copy link
Contributor

@HomayoonAlimohammadi HomayoonAlimohammadi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! Thanks a lot @petrutlucian94. Left some comments mostly for discussion. Feel free to ignore them if they don't seem relevant.

src/k8s/pkg/utils/pki/validate.go Show resolved Hide resolved
}
}

now := time.Now()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to have functionalities like time, passed like this to make it more testable:

type CertValidator struct {
    nowFn func() time.Time
    ...
}

NewCertValidator(time.Now, ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this seems unnecessary and would complicate the caller code. We already have tests that check the NotBefore and NotAfter fields without having to pass a time function.

@@ -118,6 +118,11 @@ func (c *ControlPlanePKI) CompleteCertificates() error {
}
c.CACert = cert
c.CAKey = key
} else {
certCheck := pkiutil.CertCheck{CN: "kubernetes-ca", AllowSelfSigned: true}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally go with a dependency injection and interface. Something like:

type CertValidator interface {
    Validate(certPEM, certKey string, opts... certvalidator.Option) error
}

type ControlPlanePKI struct {
    certValidator CertValidator
    ...
}

func NewControlPlanePKI(certValidator CertValidator, ...) *ControlPlanePKI {
    ...
    return &ControlPlanePKI{
        certValidator: certValidator,
        ...
    }
}

func (c *ControlPlanePKI) CompleteCertificates() error {
    ...
    if err := c.certValidator.Validate(certPEM, certKey, certvalidator.WithAllowSelfSigned(), certvalidator.WithCanonicalName(cn)) {
        return fmt.Errorf("failed to validate certificate: %w")
    }
    ...
}
}

and in utils/pki/certvalidator/validator.go:

package certvalidator

type Validator struct {
    nowFn func() time.Time
    ...
}

type Option func(v *Validator)

func WithCanonicalName(name string) Option {
    return func(v *Validator) {
        v.cn = name
   }
}

func (v *Validator) Validate(certPEM, certKey string, options ...Option) error {
    for _, o in range options {
        o(v)
    }

    ...
}

I think it's more testable this way but I don't have any strong objections with the current implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion but again I think this would complicate the implementation unnecessarily. We have just one concrete implementation and we don't need to stub it for testing purposes, in which case I don't see the point of using an interface and dependency injection.

We'll validate user provided certificates, checking fields such as:

* Common Name
* Organization
* NotBefore, NotAfter
* DNS Subject Alternative Name

Also, we ensure that the certificates are signed by the specified
CAs.
@petrutlucian94 petrutlucian94 merged commit 146f53e into canonical:main Nov 27, 2024
14 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants