2020-05-08 01:22:09 +00:00
package main
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
2022-08-19 00:46:20 +00:00
"crypto/sha1" // nolint:gosec // used to create the Subject Key Identifier by RFC 5280
2020-05-08 01:22:09 +00:00
"crypto/x509"
"crypto/x509/pkix"
2020-09-17 23:07:32 +00:00
"encoding/hex"
2020-05-08 01:22:09 +00:00
"encoding/pem"
"flag"
"fmt"
"math/big"
"os"
"time"
"github.com/pkg/errors"
2020-10-29 20:10:03 +00:00
"go.step.sm/cli-utils/fileutil"
"go.step.sm/cli-utils/ui"
2022-08-09 00:58:18 +00:00
"go.step.sm/crypto/kms"
"go.step.sm/crypto/kms/apiv1"
2020-08-14 22:33:50 +00:00
"go.step.sm/crypto/pemutil"
2020-06-12 21:55:35 +00:00
// Enable yubikey.
2022-08-09 00:58:18 +00:00
_ "go.step.sm/crypto/kms/yubikey"
2020-05-08 01:22:09 +00:00
)
2020-09-17 23:07:32 +00:00
// Config is a mapping of the cli flags.
2020-05-08 01:22:09 +00:00
type Config struct {
2020-09-17 23:07:32 +00:00
RootOnly bool
RootSlot string
CrtSlot string
RootFile string
KeyFile string
Pin string
ManagementKey string
Force bool
2020-05-08 01:22:09 +00:00
}
2020-09-17 23:07:32 +00:00
// Validate checks the flags in the config.
2020-05-08 01:22:09 +00:00
func ( c * Config ) Validate ( ) error {
switch {
2020-09-17 23:07:32 +00:00
case c . ManagementKey != "" && len ( c . ManagementKey ) != 48 :
return errors . New ( "flag `--management-key` must be 48 hexadecimal characters (24 bytes)" )
2020-05-08 01:22:09 +00:00
case c . RootFile != "" && c . KeyFile == "" :
return errors . New ( "flag `--root` requires flag `--key`" )
case c . KeyFile != "" && c . RootFile == "" :
return errors . New ( "flag `--key` requires flag `--root`" )
case c . RootOnly && c . RootFile != "" :
return errors . New ( "flag `--root-only` is incompatible with flag `--root`" )
case c . RootSlot == c . CrtSlot :
2020-05-12 02:40:12 +00:00
return errors . New ( "flag `--root-slot` and flag `--crt-slot` cannot be the same" )
case c . RootFile == "" && c . RootSlot == "" :
return errors . New ( "one of flag `--root` or `--root-slot` is required" )
2020-05-08 01:22:09 +00:00
default :
2020-05-12 02:40:12 +00:00
if c . RootFile != "" {
c . RootSlot = ""
}
if c . RootOnly {
c . CrtSlot = ""
}
2020-09-17 23:07:32 +00:00
if c . ManagementKey != "" {
if _ , err := hex . DecodeString ( c . ManagementKey ) ; err != nil {
return errors . Wrap ( err , "flag `--management-key` is not valid" )
}
}
2020-05-08 01:22:09 +00:00
return nil
}
}
func main ( ) {
var c Config
2020-09-17 23:07:32 +00:00
flag . StringVar ( & c . ManagementKey , "management-key" , "" , ` Management key to use in hexadecimal format. (default "010203040506070801020304050607080102030405060708") ` )
2020-05-08 01:22:09 +00:00
flag . BoolVar ( & c . RootOnly , "root-only" , false , "Slot only the root certificate and sign and intermediate." )
flag . StringVar ( & c . RootSlot , "root-slot" , "9a" , "Slot to store the root certificate." )
flag . StringVar ( & c . CrtSlot , "crt-slot" , "9c" , "Slot to store the intermediate certificate." )
flag . StringVar ( & c . RootFile , "root" , "" , "Path to the root certificate to use." )
flag . StringVar ( & c . KeyFile , "key" , "" , "Path to the root key to use." )
2020-05-12 02:40:12 +00:00
flag . BoolVar ( & c . Force , "force" , false , "Force the delete of previous keys." )
2020-05-08 01:22:09 +00:00
flag . Usage = usage
flag . Parse ( )
if err := c . Validate ( ) ; err != nil {
fatal ( err )
}
2021-10-07 18:09:32 +00:00
// Initialize windows terminal
ui . Init ( )
2020-05-08 01:22:09 +00:00
pin , err := ui . PromptPassword ( "What is the YubiKey PIN?" )
if err != nil {
fatal ( err )
}
c . Pin = string ( pin )
2020-05-15 18:33:22 +00:00
k , err := kms . New ( context . Background ( ) , apiv1 . Options {
2022-08-09 00:58:18 +00:00
Type : apiv1 . YubiKey ,
2020-09-17 23:07:32 +00:00
Pin : c . Pin ,
ManagementKey : c . ManagementKey ,
2020-05-08 01:22:09 +00:00
} )
if err != nil {
fatal ( err )
}
2020-05-12 02:40:12 +00:00
// Check if the slots are empty, fail if they are not
if ! c . Force {
switch {
case c . RootSlot != "" :
checkSlot ( k , c . RootSlot )
case c . CrtSlot != "" :
checkSlot ( k , c . CrtSlot )
}
}
2020-05-08 01:22:09 +00:00
if err := createPKI ( k , c ) ; err != nil {
fatal ( err )
}
defer func ( ) {
_ = k . Close ( )
} ( )
2021-10-07 19:43:24 +00:00
// Reset windows terminal
ui . Reset ( )
2020-05-08 01:22:09 +00:00
}
func fatal ( err error ) {
2020-09-17 23:07:32 +00:00
if os . Getenv ( "STEPDEBUG" ) == "1" {
fmt . Fprintf ( os . Stderr , "%+v\n" , err )
} else {
fmt . Fprintln ( os . Stderr , err )
}
2021-10-07 19:43:24 +00:00
ui . Reset ( )
2020-05-08 01:22:09 +00:00
os . Exit ( 1 )
}
func usage ( ) {
fmt . Fprintln ( os . Stderr , "Usage: step-yubikey-init" )
fmt . Fprintln ( os . Stderr , `
The step - yubikey - init command initializes a public key infrastructure ( PKI )
to be used by step - ca .
This tool is experimental and in the future it will be integrated in step cli .
OPTIONS ` )
fmt . Fprintln ( os . Stderr )
flag . PrintDefaults ( )
2022-02-16 10:08:26 +00:00
fmt . Fprintf ( os . Stderr , `
2020-05-08 01:22:09 +00:00
COPYRIGHT
2022-02-16 10:08:26 +00:00
( c ) 2018 - % d Smallstep Labs , Inc .
` , time . Now ( ) . Year ( ) )
2020-05-08 01:22:09 +00:00
os . Exit ( 1 )
}
2020-05-15 18:33:22 +00:00
func checkSlot ( k kms . KeyManager , slot string ) {
2020-05-12 02:40:12 +00:00
if _ , err := k . GetPublicKey ( & apiv1 . GetPublicKeyRequest {
Name : slot ,
} ) ; err == nil {
fmt . Fprintf ( os . Stderr , "⚠️ Your YubiKey already has a key in the slot %s.\n" , slot )
fmt . Fprintln ( os . Stderr , " If you want to delete it and start fresh, use `--force`." )
os . Exit ( 1 )
}
}
2020-05-15 18:33:22 +00:00
func createPKI ( k kms . KeyManager , c Config ) error {
2020-05-08 01:22:09 +00:00
var err error
ui . Println ( "Creating PKI ..." )
now := time . Now ( )
// Root Certificate
var signer crypto . Signer
var root * x509 . Certificate
if c . RootFile != "" && c . KeyFile != "" {
root , err = pemutil . ReadCertificate ( c . RootFile )
if err != nil {
return err
}
key , err := pemutil . Read ( c . KeyFile )
if err != nil {
return err
}
var ok bool
if signer , ok = key . ( crypto . Signer ) ; ! ok {
return errors . Errorf ( "key type '%T' does not implement a signer" , key )
}
} else {
resp , err := k . CreateKey ( & apiv1 . CreateKeyRequest {
Name : c . RootSlot ,
SignatureAlgorithm : apiv1 . ECDSAWithSHA256 ,
} )
if err != nil {
return err
}
signer , err = k . CreateSigner ( & resp . CreateSignerRequest )
if err != nil {
return err
}
template := & x509 . Certificate {
IsCA : true ,
NotBefore : now ,
NotAfter : now . Add ( time . Hour * 24 * 365 * 10 ) ,
KeyUsage : x509 . KeyUsageCertSign | x509 . KeyUsageCRLSign ,
BasicConstraintsValid : true ,
MaxPathLen : 1 ,
MaxPathLenZero : false ,
Issuer : pkix . Name { CommonName : "YubiKey Smallstep Root" } ,
Subject : pkix . Name { CommonName : "YubiKey Smallstep Root" } ,
SerialNumber : mustSerialNumber ( ) ,
SubjectKeyId : mustSubjectKeyID ( resp . PublicKey ) ,
2020-05-19 20:05:55 +00:00
AuthorityKeyId : mustSubjectKeyID ( resp . PublicKey ) ,
2020-05-08 01:22:09 +00:00
}
b , err := x509 . CreateCertificate ( rand . Reader , template , template , resp . PublicKey , signer )
if err != nil {
return err
}
root , err = x509 . ParseCertificate ( b )
if err != nil {
return errors . Wrap ( err , "error parsing root certificate" )
}
2020-05-15 18:33:22 +00:00
if cm , ok := k . ( kms . CertificateManager ) ; ok {
2021-10-08 18:59:57 +00:00
if err := cm . StoreCertificate ( & apiv1 . StoreCertificateRequest {
2020-05-15 18:33:22 +00:00
Name : c . RootSlot ,
Certificate : root ,
} ) ; err != nil {
return err
}
2020-05-08 01:22:09 +00:00
}
2021-10-08 18:59:57 +00:00
if err := fileutil . WriteFile ( "root_ca.crt" , pem . EncodeToMemory ( & pem . Block {
2020-05-08 01:22:09 +00:00
Type : "CERTIFICATE" ,
Bytes : b ,
} ) , 0600 ) ; err != nil {
return err
}
2020-05-12 02:42:21 +00:00
ui . PrintSelected ( "Root Key" , resp . Name )
2020-05-08 01:22:09 +00:00
ui . PrintSelected ( "Root Certificate" , "root_ca.crt" )
}
// Intermediate Certificate
var keyName string
var publicKey crypto . PublicKey
if c . RootOnly {
priv , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
if err != nil {
2020-05-19 20:05:55 +00:00
return errors . Wrap ( err , "error creating intermediate key" )
2020-05-08 01:22:09 +00:00
}
pass , err := ui . PromptPasswordGenerate ( "What do you want your password to be? [leave empty and we'll generate one]" ,
ui . WithRichPrompt ( ) )
if err != nil {
return err
}
_ , err = pemutil . Serialize ( priv , pemutil . WithPassword ( pass ) , pemutil . ToFile ( "intermediate_ca_key" , 0600 ) )
if err != nil {
return err
}
publicKey = priv . Public ( )
} else {
resp , err := k . CreateKey ( & apiv1 . CreateKeyRequest {
Name : c . CrtSlot ,
SignatureAlgorithm : apiv1 . ECDSAWithSHA256 ,
} )
if err != nil {
return err
}
publicKey = resp . PublicKey
keyName = resp . Name
}
template := & x509 . Certificate {
IsCA : true ,
NotBefore : now ,
NotAfter : now . Add ( time . Hour * 24 * 365 * 10 ) ,
KeyUsage : x509 . KeyUsageCertSign | x509 . KeyUsageCRLSign ,
BasicConstraintsValid : true ,
MaxPathLen : 0 ,
MaxPathLenZero : true ,
Issuer : root . Subject ,
Subject : pkix . Name { CommonName : "YubiKey Smallstep Intermediate" } ,
SerialNumber : mustSerialNumber ( ) ,
SubjectKeyId : mustSubjectKeyID ( publicKey ) ,
}
b , err := x509 . CreateCertificate ( rand . Reader , template , root , publicKey , signer )
if err != nil {
return err
}
intermediate , err := x509 . ParseCertificate ( b )
if err != nil {
return errors . Wrap ( err , "error parsing intermediate certificate" )
}
2020-05-15 18:33:22 +00:00
if cm , ok := k . ( kms . CertificateManager ) ; ok {
2021-10-08 18:59:57 +00:00
if err := cm . StoreCertificate ( & apiv1 . StoreCertificateRequest {
2020-05-15 18:33:22 +00:00
Name : c . CrtSlot ,
Certificate : intermediate ,
} ) ; err != nil {
return err
}
2020-05-08 01:22:09 +00:00
}
2021-10-08 18:59:57 +00:00
if err := fileutil . WriteFile ( "intermediate_ca.crt" , pem . EncodeToMemory ( & pem . Block {
2020-05-08 01:22:09 +00:00
Type : "CERTIFICATE" ,
Bytes : b ,
} ) , 0600 ) ; err != nil {
return err
}
if c . RootOnly {
ui . PrintSelected ( "Intermediate Key" , "intermediate_ca_key" )
} else {
2020-05-12 02:42:21 +00:00
ui . PrintSelected ( "Intermediate Key" , keyName )
2020-05-08 01:22:09 +00:00
}
ui . PrintSelected ( "Intermediate Certificate" , "intermediate_ca.crt" )
return nil
}
func mustSerialNumber ( ) * big . Int {
serialNumberLimit := new ( big . Int ) . Lsh ( big . NewInt ( 1 ) , 128 )
sn , err := rand . Int ( rand . Reader , serialNumberLimit )
if err != nil {
panic ( err )
}
return sn
}
func mustSubjectKeyID ( key crypto . PublicKey ) [ ] byte {
b , err := x509 . MarshalPKIXPublicKey ( key )
if err != nil {
panic ( err )
}
2022-08-19 00:46:20 +00:00
// nolint:gosec // used to create the Subject Key Identifier by RFC 5280
2020-05-08 01:22:09 +00:00
hash := sha1 . Sum ( b )
return hash [ : ]
}