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

feat: generate ecdsa keys #3

Merged
merged 2 commits into from Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions keygen.go
Expand Up @@ -3,7 +3,9 @@ package keygen

import (
"bytes"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
Expand All @@ -27,6 +29,7 @@ type KeyType string
const (
RSA KeyType = "rsa"
Ed25519 KeyType = "ed25519"
ECDSA KeyType = "ecdsa"
)

const rsaDefaultBits = 4096
Expand Down Expand Up @@ -100,6 +103,8 @@ func New(path, name string, passphrase []byte, keyType KeyType) (*SSHKeyPair, er
err = s.generateEd25519Keys()
case RSA:
err = s.generateRSAKeys(rsaDefaultBits, passphrase)
case ECDSA:
err = s.generateECDSAKeys()
default:
return nil, fmt.Errorf("unsupported key type %s", keyType)
}
Expand Down Expand Up @@ -151,6 +156,38 @@ func (s *SSHKeyPair) generateEd25519Keys() error {
return nil
}

// generateEd25519Keys creates a pair of EdD25519 keys for SSH auth.
func (s *SSHKeyPair) generateECDSAKeys() error {
// Generate keys
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return err
}

// Encode PEM
bts, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return err
}
pemBlock := pem.EncodeToMemory(&pem.Block{
Type: "OPENSSH PRIVATE KEY",
Bytes: bts,
})

// Prepare public key
publicKey, err := ssh.NewPublicKey(privateKey.Public())
if err != nil {
return err
}

// serialize for public key file on disk
serializedPublicKey := ssh.MarshalAuthorizedKey(publicKey)

s.PrivateKeyPEM = pemBlock
s.PublicKey = pubKeyWithMemo(serializedPublicKey)
return nil
}

// generateRSAKeys creates a pair for RSA keys for SSH auth.
func (s *SSHKeyPair) generateRSAKeys(bitSize int, passphrase []byte) error {
// Generate private key
Expand Down
66 changes: 66 additions & 0 deletions keygen_test.go
Expand Up @@ -80,6 +80,72 @@ func TestGenerateEd25519Keys(t *testing.T) {
})
}

func TestGenerateECDSAKeys(t *testing.T) {
// Create temp directory for keys
dir := t.TempDir()

k := &SSHKeyPair{
KeyDir: dir,
Filename: "test",
}

t.Run("test generate SSH keys", func(t *testing.T) {
err := k.generateECDSAKeys()
if err != nil {
t.Errorf("error creating SSH key pair: %v", err)
}

// TODO: is there a good way to validate these? Lengths seem to vary a bit,
// so far now we're just asserting that the keys indeed exist.
if len(k.PrivateKeyPEM) == 0 {
t.Error("error creating SSH private key PEM; key is 0 bytes")
}
if len(k.PublicKey) == 0 {
t.Error("error creating SSH public key; key is 0 bytes")
}
})

t.Run("test write SSH keys", func(t *testing.T) {
k.KeyDir = filepath.Join(dir, "ssh1")
if err := k.prepFilesystem(); err != nil {
t.Errorf("filesystem error: %v\n", err)
}
if err := k.WriteKeys(); err != nil {
t.Errorf("error writing SSH keys to %s: %v", k.KeyDir, err)
}
if testing.Verbose() {
t.Logf("Wrote keys to %s", k.KeyDir)
}
})

t.Run("test not overwriting existing keys", func(t *testing.T) {
k.KeyDir = filepath.Join(dir, "ssh2")
if err := k.prepFilesystem(); err != nil {
t.Errorf("filesystem error: %v\n", err)
}

// Private key
filePath := filepath.Join(k.KeyDir, k.Filename)
if !createEmptyFile(t, filePath) {
return
}
if err := k.WriteKeys(); err == nil {
t.Errorf("we wrote the private key over an existing file, but we were not supposed to")
}
if err := os.Remove(filePath); err != nil {
t.Errorf("could not remove file %s", filePath)
}

// Public key
if !createEmptyFile(t, filePath+".pub") {
return
}
if err := k.WriteKeys(); err == nil {
t.Errorf("we wrote the public key over an existing file, but we were not supposed to")
}
})
}

// touchTestFile is a utility function we're using in testing.
func createEmptyFile(t *testing.T, path string) (ok bool) {
dir := filepath.Dir(path)
Expand Down