diff --git a/caddy/https/client.go b/caddy/https/client.go index 40e7a709..762e58aa 100644 --- a/caddy/https/client.go +++ b/caddy/https/client.go @@ -34,16 +34,7 @@ var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) { } // The client facilitates our communication with the CA server. - var kt acme.KeyType - if rsaKeySizeToUse == Rsa2048 { - kt = acme.RSA2048 - } else if rsaKeySizeToUse == Rsa4096 { - kt = acme.RSA4096 - } else { - // TODO(hkjn): Support more types? Current changes are quick fix for #640. - return nil, fmt.Errorf("https: unsupported keysize") - } - client, err := acme.NewClient(CAUrl, &leUser, kt) + client, err := acme.NewClient(CAUrl, &leUser, KeyType) if err != nil { return nil, err } diff --git a/caddy/https/crypto.go b/caddy/https/crypto.go index efc40d43..bc0ff637 100644 --- a/caddy/https/crypto.go +++ b/caddy/https/crypto.go @@ -1,26 +1,52 @@ package https import ( + "crypto" + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" "io/ioutil" "os" ) -// loadRSAPrivateKey loads a PEM-encoded RSA private key from file. -func loadRSAPrivateKey(file string) (*rsa.PrivateKey, error) { +// loadPrivateKey loads a PEM-encoded ECC/RSA private key from file. +func loadPrivateKey(file string) (crypto.PrivateKey, error) { keyBytes, err := ioutil.ReadFile(file) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyBytes) - return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + } + + return nil, errors.New("unknown private key type") } -// saveRSAPrivateKey saves a PEM-encoded RSA private key to file. -func saveRSAPrivateKey(key *rsa.PrivateKey, file string) error { - pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} +// savePrivateKey saves a PEM-encoded ECC/RSA private key to file. +func savePrivateKey(key crypto.PrivateKey, file string) error { + var pemType string + var keyBytes []byte + switch key := key.(type) { + case *ecdsa.PrivateKey: + var err error + pemType = "EC" + keyBytes, err = x509.MarshalECPrivateKey(key) + if err != nil { + return err + } + case *rsa.PrivateKey: + pemType = "RSA" + keyBytes = x509.MarshalPKCS1PrivateKey(key) + } + + pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} keyOut, err := os.Create(file) if err != nil { return err diff --git a/caddy/https/crypto_test.go b/caddy/https/crypto_test.go index 39cd27b5..c1f32b27 100644 --- a/caddy/https/crypto_test.go +++ b/caddy/https/crypto_test.go @@ -2,6 +2,9 @@ package https import ( "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -10,23 +13,17 @@ import ( "testing" ) -func init() { - rsaKeySizeToUse = 2048 // TODO(hkjn): Bring back support for small - // keys to speed up tests? Current changes - // are quick fix for #640. -} - func TestSaveAndLoadRSAPrivateKey(t *testing.T) { keyFile := "test.key" defer os.Remove(keyFile) - privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } // test save - err = saveRSAPrivateKey(privateKey, keyFile) + err = savePrivateKey(privateKey, keyFile) if err != nil { t.Fatal("error saving private key:", err) } @@ -45,23 +42,70 @@ func TestSaveAndLoadRSAPrivateKey(t *testing.T) { } // test load - loadedKey, err := loadRSAPrivateKey(keyFile) + loadedKey, err := loadPrivateKey(keyFile) if err != nil { t.Error("error loading private key:", err) } // verify loaded key is correct - if !rsaPrivateKeysSame(privateKey, loadedKey) { + if !PrivateKeysSame(privateKey, loadedKey) { t.Error("Expected key bytes to be the same, but they weren't") } } -// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same. -func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool { - return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b)) +func TestSaveAndLoadECCPrivateKey(t *testing.T) { + keyFile := "test.key" + defer os.Remove(keyFile) + + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + // test save + err = savePrivateKey(privateKey, keyFile) + if err != nil { + t.Fatal("error saving private key:", err) + } + + // it doesn't make sense to test file permission on windows + if runtime.GOOS != "windows" { + // get info of the key file + info, err := os.Stat(keyFile) + if err != nil { + t.Fatal("error stating private key:", err) + } + // verify permission of key file is correct + if info.Mode().Perm() != 0600 { + t.Error("Expected key file to have permission 0600, but it wasn't") + } + } + + // test load + loadedKey, err := loadPrivateKey(keyFile) + if err != nil { + t.Error("error loading private key:", err) + } + + // verify loaded key is correct + if !PrivateKeysSame(privateKey, loadedKey) { + t.Error("Expected key bytes to be the same, but they weren't") + } } -// rsaPrivateKeyBytes returns the bytes of DER-encoded key. -func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte { - return x509.MarshalPKCS1PrivateKey(key) +// PrivateKeysSame compares the bytes of a and b and returns true if they are the same. +func PrivateKeysSame(a, b crypto.PrivateKey) bool { + return bytes.Equal(PrivateKeyBytes(a), PrivateKeyBytes(b)) +} + +// PrivateKeyBytes returns the bytes of DER-encoded key. +func PrivateKeyBytes(key crypto.PrivateKey) []byte { + var keyBytes []byte + switch key := key.(type) { + case *rsa.PrivateKey: + keyBytes = x509.MarshalPKCS1PrivateKey(key) + case *ecdsa.PrivateKey: + keyBytes, _ = x509.MarshalECPrivateKey(key) + } + return keyBytes } diff --git a/caddy/https/https.go b/caddy/https/https.go index 90022ed5..76e5e312 100644 --- a/caddy/https/https.go +++ b/caddy/https/https.go @@ -401,21 +401,10 @@ var ( // default port for the challenge must be forwarded to this one. const AlternatePort = "5033" -// KeySize represents the length of a key in bits. -type KeySize int - -// Key sizes are used to determine the strength of a key. -const ( - Ecc224 KeySize = 224 - Ecc256 = 256 - Rsa2048 = 2048 - Rsa4096 = 4096 -) - -// rsaKeySizeToUse is the size to use for new RSA keys. +// KeyType is the type to use for new keys. // This shouldn't need to change except for in tests; // the size can be drastically reduced for speed. -var rsaKeySizeToUse = Rsa2048 +var KeyType = acme.EC384 // stopChan is used to signal the maintenance goroutine // to terminate. diff --git a/caddy/https/user.go b/caddy/https/user.go index 203e07a2..a7e6e5f6 100644 --- a/caddy/https/user.go +++ b/caddy/https/user.go @@ -3,8 +3,9 @@ package https import ( "bufio" "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" "encoding/json" "errors" "fmt" @@ -21,7 +22,7 @@ import ( type User struct { Email string Registration *acme.RegistrationResource - key *rsa.PrivateKey + key crypto.PrivateKey } // GetEmail gets u's email. @@ -64,7 +65,7 @@ func getUser(email string) (User, error) { } // load their private key - user.key, err = loadRSAPrivateKey(storage.UserKeyFile(email)) + user.key, err = loadPrivateKey(storage.UserKeyFile(email)) if err != nil { return user, err } @@ -83,7 +84,7 @@ func saveUser(user User) error { } // save private key file - err = saveRSAPrivateKey(user.key, storage.UserKeyFile(user.Email)) + err = savePrivateKey(user.key, storage.UserKeyFile(user.Email)) if err != nil { return err } @@ -104,7 +105,7 @@ func saveUser(user User) error { // instead. It does NOT prompt the user. func newUser(email string) (User, error) { user := User{Email: email} - privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse) + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { return user, errors.New("error generating private key: " + err.Error()) } diff --git a/caddy/https/user_test.go b/caddy/https/user_test.go index 5bc28b04..c1d115e1 100644 --- a/caddy/https/user_test.go +++ b/caddy/https/user_test.go @@ -114,7 +114,7 @@ func TestGetUserAlreadyExists(t *testing.T) { } // Assert keys are the same - if !rsaPrivateKeysSame(user.key, user2.key) { + if !PrivateKeysSame(user.key, user2.key) { t.Error("Expected private key to be the same after loading, but it wasn't") }