Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions cmd/nv2/gencert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package main

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"log"
"math/big"
"net"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/urfave/cli/v2"
)

const (
// This needs to be configurable. Once the location of the
// configuration is finalized this parameter and file locations
// should be exposed as options on the cli with mock tests.
KEYS_BASE_DIR = ".notation"
)

var certificatesCommand = &cli.Command{
Name: "certificates",
Usage: "Commands to manage certificates",
Subcommands: []*cli.Command{
generateCertCommand,
},
}

var generateCertCommand = &cli.Command{
Name: "generate",
Usage: "Generates a test crt and key file.",
ArgsUsage: "[host]",
Flags: []cli.Flag{
&cli.IntFlag{
Name: "rsaBits",
Usage: "--rsaBits 3072",
Required: false,
Value: 3072,
},
&cli.StringFlag{
Name: "not-after",
Copy link
Contributor

Choose a reason for hiding this comment

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

Will it be a better customer experience to take validity period validForDays instead of actual date?
Pros:

  • easy to associate default value of 1 year
  • No need to worry about not-after being in past.
certificates generate --not-after 2006-01-02T15:04:05-07:00
certificates generate --valid-for-days 365

Copy link
Contributor

Choose a reason for hiding this comment

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

validForDays might not be a good one. For testing purposes, people may want short-lived certs like serveral hours or minutes.

Usage: "--not-after 2006-01-02T15:04:05-07:00 (default is 1 year)",
Required: false,
},
},

Action: runGenerateCert,
}

func ensureKeysDir() (string, error) {

// Expected to ensure ~/.notation/keys
dirname, err := os.UserHomeDir()
Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed in #76 we should support XDG_Base_Directory.

Copy link
Contributor

Choose a reason for hiding this comment

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

Notation CLI Alpha honors XDG directories.

if err != nil {
log.Fatal(err)
}

keysDir := filepath.Join(dirname, KEYS_BASE_DIR, "keys")
fsStat, err := os.Stat(keysDir)
if os.IsNotExist(err) {
err = os.MkdirAll(keysDir, 0700) // Only user has permissions on this directory
if err != nil {
return "", err
}
} else if fsStat.IsDir() == false {
return "", fmt.Errorf("%s should be a directory", keysDir)
}

return keysDir, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to ensure that keysDir is really a directory. It could be a regular file or symbolic link.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use IsDir. Is that good enough or do we need to ensure more checks. I believe you handled this in ORAS a well.

}

//ref: https://golang.org/src/crypto/tls/generate_cert.go

func runGenerateCert(ctx *cli.Context) error {

host := ctx.Args().First()
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not accept an argument list instead of a signle argument, making host a []string slice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it ok to come back to this once we have sorted out the UX for this CLI?

if len(host) == 0 {
return errors.New("Missing required [host] parameter")
}

// Set certificate validity
notBefore := time.Now()
notAfter := notBefore.Add(time.Duration(365 * 24 * time.Hour))
Copy link
Contributor

Choose a reason for hiding this comment

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

dead code?


expiry := ctx.String("not-after")
var err error
if len(expiry) != 0 {
notAfter, err = time.Parse(time.RFC3339, expiry)
if err != nil {
return fmt.Errorf("Invalid --not-after %s value specified %v", expiry, err)
}
}

if notAfter.Before(notBefore) {
return fmt.Errorf("Invalid --not-after that is earlier than not-before [%s] specified", notBefore.Format(time.RFC3339))
}

// Generate RSA Bits
rsaBits := ctx.Int("rsaBits")
var priv crypto.Signer

fmt.Printf("Generating RSA Key with %d bits\n", rsaBits)
priv, err = rsa.GenerateKey(rand.Reader, rsaBits)

if err != nil {
return fmt.Errorf("Failed to generate private key: %v", err)
}

// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature
// KeyUsage bits set in the x509.Certificate template
keyUsage := x509.KeyUsageDigitalSignature
extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("Failed to generate serial number: %v", err)
}

template := x509.Certificate{
SerialNumber: serialNumber,
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: keyUsage,
ExtKeyUsage: extKeyUsage,
BasicConstraintsValid: true,
}

hosts := strings.Split(host, ",")
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}
Comment on lines +139 to +146
Copy link
Contributor

Choose a reason for hiding this comment

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

as this is codesigning not ssl certificate, we don't need to support DNSName or ip-address.

Copy link
Contributor

Choose a reason for hiding this comment

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

We still need them as the x509 package checks SAN instead of CN starting from golang 1.15. See https://golang.org/doc/go1.15#commonname


template.Subject = pkix.Name{
Organization: []string{hosts[0]},
CommonName: hosts[0],
Comment on lines +149 to +150
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we take Organization Name from user instead of host? host is confusing as certificate is not used for tls.

Copy link
Contributor

Choose a reason for hiding this comment

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

Organization is removed in the Notation CLI Alpha PR.

}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
if err != nil {
return fmt.Errorf("Failed to create certificate: %v", err)
}

// Write the crt public key file
keysDir, err := ensureKeysDir()
if err != nil {
return fmt.Errorf("Could not access keys directory: %v", err)
}
Comment on lines +159 to +162
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be done as first thing after validating arguments.


crtFileName := path.Join(keysDir, hosts[0]+".crt")
crtFilePath, err := filepath.Abs(crtFileName)
if err != nil {
return fmt.Errorf("Unable to get full path of the file: %v", err)
}

certOut, err := os.OpenFile(crtFileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return fmt.Errorf("Failed to open %s for writing: %v", crtFilePath, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

s/open/create file?

}

if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return fmt.Errorf("Failed to write data to %s: %v", crtFilePath, err)
}
if err := certOut.Close(); err != nil {
return fmt.Errorf("Error closing %s: %v", crtFilePath, err)
}

fmt.Printf("Generated certificates expiring on %s\n", notAfter.Format(time.RFC3339))
fmt.Printf("Wrote self-signed certificate file: %s\n", crtFilePath)

// Write the private key file
keyFileName := path.Join(keysDir, hosts[0]+".key")
keyFilePath, err := filepath.Abs(keyFileName)
if err != nil {
return fmt.Errorf("Unable to get full path of the file: %v", err)
}

keyOut, err := os.OpenFile(keyFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return fmt.Errorf("Failed to open key.pem for writing: %v", err)
}

privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return fmt.Errorf("Unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
return fmt.Errorf("Failed to write data to key.pem: %v", err)
}
if err := keyOut.Close(); err != nil {
return fmt.Errorf("Error closing %s: %v", keyFilePath, err)
}
fmt.Printf("Wrote private key file: %s\n", keyFilePath)
return nil
}
1 change: 1 addition & 0 deletions cmd/nv2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func main() {
verifyCommand,
pushCommand,
pullCommand,
certificatesCommand,
},
}
if err := app.Run(os.Args); err != nil {
Expand Down