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

Megan/expiration #495

Merged
merged 20 commits into from Jul 8, 2021
Merged

Megan/expiration #495

merged 20 commits into from Jul 8, 2021

Conversation

meganm53
Copy link
Contributor

Fixes issue #398 , added flag that allows you to check how much longer until a certificate is about to expire, along with the color coding the outputs based on if it is about to expire/expired (red), close (yellow), or still has under 66% of its life used (green). added to description how to use this function.

@meganm53 meganm53 requested a review from dopey June 21, 2021 22:28
@CLAassistant
Copy link

CLAassistant commented Jun 21, 2021

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions bot added the needs triage Waiting for discussion / prioritization by team label Jun 21, 2021
@@ -14,7 +14,7 @@ func init() {
Description: `**step certificate** command group provides facilities for creating
certificate signing requests (CSRs), creating self-signed certificates
(e.g., for use as a root certificate authority), generating leaf or
intermediate CA certificate by signing a CSR, validating certificates,
intermediate CA certificate by signing a CSR, validating certificates, check expiration of certificate,
Copy link
Contributor

Choose a reason for hiding this comment

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

checking certificate expiration

The tense on the other verbs all seem to end with ing, so we should be consistent.

@codecov
Copy link

codecov bot commented Jun 21, 2021

Codecov Report

Merging #495 (1187e48) into master (0da3b28) will decrease coverage by 0.16%.
The diff coverage is 26.66%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #495      +/-   ##
==========================================
- Coverage   63.29%   63.12%   -0.17%     
==========================================
  Files          57       57              
  Lines        6865     6894      +29     
==========================================
+ Hits         4345     4352       +7     
- Misses       2281     2303      +22     
  Partials      239      239              
Impacted Files Coverage Δ
command/certificate/verify.go 44.60% <24.13%> (-5.40%) ⬇️
command/certificate/certificate.go 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0da3b28...1187e48. Read the comment docs.

} else if percentIntoLifeTime < 1 {
fmt.Println("\033[32m", "Leaf is less than 1% through its lifetime.", "\033[0m")
} else {
fmt.Println("Error")
Copy link
Contributor

Choose a reason for hiding this comment

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

return an errors.Errorf error here that described the issue. See errors returned in above code for examples.

percentIntoLifeTime := ((totalLifeTimeOfCert.Hours() - NowTillEndOfCert.Hours()) / totalLifeTimeOfCert.Hours()) * 100

if percentIntoLifeTime >= 100 {
fmt.Println("\033[31m", "This certificate has already expired.", "\033[0m") //"\033[__m" are color codes
Copy link
Contributor

Choose a reason for hiding this comment

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

Just gonna list some bullet points here for improvements to this section:

if percent >= 100 {
}
else if percent >= 90 {
}
else if percent >= 66 {
}
else if percent >= 1 {
}
else if precent >= 0 {
} else {
}
  • Use fmt.Printf instead of fmt.Println if possible.
  • parameterize commonly used strings and colors so you can reuse them. e.g var colorExpired = "31m".

@@ -164,6 +177,30 @@ func verifyAction(ctx *cli.Context) error {
}
}

if expire {

NowTillEndOfCert := time.Until(cert.NotAfter)
Copy link
Contributor

Choose a reason for hiding this comment

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

variable names with capital first letters are exported. Use a lower case first letter unless you need to export the variable beyond the current package.

Copy link
Contributor

Choose a reason for hiding this comment

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

use remainingValidity and totalValidity. The term validity window is already associated with certificates and we already frequently use the work validity in our code when doing similar calculations.

NowTillEndOfCert := time.Until(cert.NotAfter)
totalLifeTimeOfCert := cert.NotAfter.Sub(cert.NotBefore)

percentIntoLifeTime := ((totalLifeTimeOfCert.Hours() - NowTillEndOfCert.Hours()) / totalLifeTimeOfCert.Hours()) * 100
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we specifying .Hours() here rather than using the complete duration?

Copy link
Contributor

Choose a reason for hiding this comment

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

You can just call this variable percent.

Generally speaking, the rule for variable names is that the length of the name should be proportionate to how far away from the instantiation of the variable it is being used. So if it's super far away, e.g. different functions, files or packages, then you want a really descriptive name like you've chosen here. But in this case, the variable and it's use are super close. So we can opt for shorter, less descriptive, more context dependent names. Lmk if that does/doesn't make sense.

if expire {

NowTillEndOfCert := time.Until(cert.NotAfter)
totalLifeTimeOfCert := cert.NotAfter.Sub(cert.NotBefore)
Copy link
Contributor

@dopey dopey Jun 21, 2021

Choose a reason for hiding this comment

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

I would check first if time.Now().Before(cert.NotBefore) because if so, then the cert is not valid yet, and that's a different message than any of the ones you have below. At the same time I would check if time.Now().After(cert.NotAfter) just to rule out both of those special cases. Then we know that the results should be within our bounds.

I would also do (1 - remainingValidity/totalValidity) * 100 because it's a bit more natural.

@@ -65,12 +68,22 @@ Verify a certificate using a custom directory of root certificates for path vali
'''
$ step certificate verify ./certificate.crt --roots ./root-certificates/
'''

Verify the expiration time left of a certificate using a custom root certificate and host for path validation:
Copy link
Contributor

Choose a reason for hiding this comment

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

Verify the remaining validity of a certificate ...

`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Usage: `Check whether the certificate is for the specified host.`,
},
cli.BoolFlag{
Name: "expire",
Usage: `Checks the certificate time till expiration`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Checks the remaining certificate validity till expiration


return nil
}

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 we should verify the certificate first cert.VerifyOpts below, and then if it's valid we can analyze how much of the lifespan has been used. The benefit of this is that we won't have to check if now < cert.NotBefore or if the cert is already expired. The VerifyOpts method already does that for us. So only if the certificate is valid do we print expiry information.

if percentUsed >= 100 {
fmt.Println(red, "This certificate has already expired.", reset)
} else if percentUsed > 90 {
fmt.Printf(red,"Leaf is", int(percentUsed), "% through its lifetime.", reset)
Copy link
Contributor

Choose a reason for hiding this comment

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

When you use fmt.Printf you can do things like: fmt.Printf("%sLeaf is %d% through it's lifetime.", red, percentUsed). The variables at the end replace the %s and %d.

reset := "\033[0m"

if percentUsed >= 100 {
fmt.Println(red, "This certificate has already expired.", reset)
Copy link
Contributor

Choose a reason for hiding this comment

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

We're using the word certificate in this output, but the word leaf in future outputs to refer to the same thing. I think we should consistently use the term certificate.

reset := "\033[0m"

if percentUsed >= 100 {
fmt.Println(red, "This certificate has already expired.", reset)
Copy link
Contributor

Choose a reason for hiding this comment

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

@tashian How would you like this output to look? Your plan was to use this as input from a pipe. I recall you saying that you wanted these to be errorCodes rather than printed statements.

Copy link
Contributor

@tashian tashian Jun 22, 2021

Choose a reason for hiding this comment

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

Yes, the exit codes are described in #398

Verify the remaining validity of a certificate using a custom root certificate and host for path validation:

'''
$ step certificate verify ./certificate.crt --host smallstep.com --expire
Copy link
Contributor

@dopey dopey Jun 22, 2021

Choose a reason for hiding this comment

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

@mmalone @tashian what did we want this flag to be called? expire, validity, verdancy?

Copy link
Contributor

Choose a reason for hiding this comment

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

verdancy

@tashian
Copy link
Contributor

tashian commented Jun 22, 2021

hi @cookie-annihilator @tommy-56 I'm excited to see this PR!
I want to explain why --verdancy is the word we landed on for this flag during design discussions.
The idea of verdancy is, it's a matter of degree: What the command is asking is "Just how green is this certificate?"
And, like a leaf on a tree, as the certificate ages it changes color.
Whereas, --expire implies a yes/no answer: Is it expired or not?
Verdancy provides that answer and more.

The use cases are:

  • Verdancy aids automated renewal, and once this feature is released I will immediately put it into use in our docs. We recommend people renew certificates when they are around 2/3 through their validity period, so we want an easy way to confirm that.
  • Expired (yes/no) could also be used in a script, if there is a chance a certificate may have expired. Because with an unexpired cert you can run step ca renew to get a new certificate, but with an expired cert you'd need to run step ca certificate.
  • We also discussed an expires-in flag that would allow you to pass a % or duration, and you'd get back a yes/no.

Happy to answer any other questions y'all have about this feature...

Action: cli.ActionFunc(needsRenewalAction),
Usage: `Check if a certificate needs to be renewed`,
UsageText: `**step certificate needs-renewal** <crt_file or host_name> [**--expires-in <duration>]`,
Description: `**step certificate needs-renewal** Checks certificate expiration from file or from a host if
Copy link
Contributor

Choose a reason for hiding this comment

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

step certificate needs-renewal returns '0' if the certificate needs to be renewed based on it's remaining lifetime. Returns '1' if the certificate is within it's validity lifetime bounds and does not need to be renewed. Returns '255' for any other error. By default, if a certificate "needs renewal" when it has passed 66% of it's allotted lifetime. This threshold can be adjusted using the '--expires-in' flag.

this command will produce a non-zero return value.
## POSITIONAL ARGUMENTS

<crt_file>
Copy link
Contributor

Choose a reason for hiding this comment

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

Combine the positional arguments into one, as we do with the UsageText above.

<cert_file or hostname> The path to a certificate to validate OR a hostname with protocol prefix.


## EXIT CODES

This command returns 0 if needing renewal, or returns 1 if the certificate does not need renewal. It will return 255 if any errors occurred
Copy link
Contributor

@dopey dopey Jul 2, 2021

Choose a reason for hiding this comment

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

This command returns '0' if the certificate needs renewal, '1' if the certificate does not need renewal, and '255' for any other error.

'''
$ step certificate needs-renewal ./certificate.crt
'''
Check certificate for renewal using a host
Copy link
Contributor

Choose a reason for hiding this comment

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

using a hostname:


This command returns 0 if needing renewal, or returns 1 if the certificate does not need renewal. It will return 255 if any errors occurred

## EXAMPLES
Copy link
Contributor

Choose a reason for hiding this comment

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

Put a colon : after each sentence prefacing the example command, like so: https://github.com/smallstep/cli/blob/master/command/certificate/verify.go#L38

Name: "expires-in",
Usage: `Check if the certificate expires in given time duration
using <percent|duration>. For <percent>, must be followed by "%".
With <duration>, it is a sequence of decimal numbers, each with optional
Copy link
Contributor

Choose a reason for hiding this comment

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

With -> For

)

if addr, isURL, err := trimURL(crtFile); err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

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

this should return 255 as well

if addr, isURL, err := trimURL(crtFile); err != nil {
return err
} else if isURL {
peerCertificates, err := getPeerCertificates(addr, serverName, roots, false)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're changing the operation of this command to now apply to all certificates in the bundle, you can probably just use the full list of peer certificates rather than grabbing the first index.

// The first certificate PEM in the file is our leaf Certificate.
// Any certificate after the first is added to the list of Intermediate
// certificates used for path validation.
for len(crtBytes) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're changing the behavior of this command to apply to all certs in the file, what we want to do is loop over each pem block in the file (just like you're doing) and then convert it to a certificate and append it to an array of x509 certifcates.

}

}
var remainingValidity = time.Until(cert.NotAfter)
Copy link
Contributor

Choose a reason for hiding this comment

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

This will happen in a for loop over all the certificates in the list


if expiresIn != "" {
if strings.Contains(expiresIn, "%") {
percentageInput, err := strconv.Atoi(strings.ReplaceAll(expiresIn, "%", ""))
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 say just trimSuffix instead of replace all. We want people entering 50% instead of %3%5

os.Exit(1)
}
} else {
if percentUsed >= 66 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's parameterize this value, meaning let's make it a constant defined at the top of the file.

const defaultPercentUsedThreshold = 66

}

if percentageInput > int(percentUsed) {
return nil
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 this is the case where we want to return 1.

}
} else {
if percentUsed >= 66 {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to combine this with the other percentage case so we don't copy the same conditional code.

Something like:

for _, cert := range certs {
if expiredIn == "" || strings.Contains(expiresIn, "%") {
    var threshold int
    if expiresIn == "" {
        threshold = defaultPercentThreshold
    } else {
        threshold, err = strconv.Atoi(strings.TrimSuffix(expiresIn, "%"))
        if err ....
    }
    
    if threshold > 100 || threshold < 0 ...

    if int(percentUsed) >= threshold ...
        return nil
} else {
    			duration, err := time.ParseDuration(expiresIn)

			if err != nil {
				return errs.NewExitError(err, 255)
			} else if duration > remainingValidity {
				return nil
			}
}
}
// We only get here if the certs are not expiring, so return an error to that effect.
return errors.New("certificate is verdant")

Copy link
Contributor

@dopey dopey left a comment

Choose a reason for hiding this comment

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

left a bunch of comments.

All commented changes requested from Max have been Implemented here
the certificate is over 66% of its lifetime. If the certificate needs renewal this command will return '0'.
If the certificate is not far enough into its life, it will return '1'. If validation fails, or if an error occurs,
this command will produce a non-zero return value.
UsageText: `**step certificate needs-renewal** <crt_file or host_name> [**--expires-in**=<duration>]`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Use cert_file here and in other places where you refer to this input. Over time we came to the custom of using cert if it's in documentation that the user sees because that's the standard other popular tools use (e.g. openssl, curl), and crt if we're just naming variables internally in our code.

UsageText: `**step certificate needs-renewal** <crt_file or host_name> [**--expires-in**=<duration>]`,
Description: `**step certificate needs-renewal** returns '0' if the certificate needs to be renewed based on it's remaining lifetime.
Returns '1' if the certificate is within it's validity lifetime bounds and does not need to be renewed.
Returns '255' for any other error. By default, if a certificate "needs renewal" when it has passed 66% of it's allotted lifetime.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove the if in this line.


<host_name>
: Address of remote host
<cert_file or hostname> The path to a certificate to validate OR a hostname with protocol prefix.
Copy link
Contributor

Choose a reason for hiding this comment

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

remove to validate

dopey and others added 4 commits July 7, 2021 15:33
--roots and --servername are in with their descriptions from inspect.go. Examples for --roots were copied and adjusted for needs-renewal purposes
- change a bit of grammar in docs/examples
@tommy-56 tommy-56 marked this pull request as ready for review July 8, 2021 20:15
@tommy-56 tommy-56 merged commit 74aef56 into master Jul 8, 2021
@tommy-56 tommy-56 deleted the megan/expiration branch July 8, 2021 20:17
@tommy-56 tommy-56 linked an issue Jul 8, 2021 that may be closed by this pull request
@tashian
Copy link
Contributor

tashian commented Jul 19, 2021

Thank you @tommy-56 @cookie-annihilator for this!

I've updated our systemd stuff to use needs-renewal (see smallstep/certificates#653 and smallstep/docs#105). It's so much cleaner, and it removes the dependency on jq.

@dopey dopey removed the needs triage Waiting for discussion / prioritization by team label Aug 31, 2021
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.

Testing certificates for verdancy
7 participants