cmd/age: add support for encrypted SSH key files

This commit is contained in:
Filippo Valsorda
2019-11-24 19:15:53 -05:00
parent 2cc62919a6
commit c624abc0ad
9 changed files with 198 additions and 16 deletions
+125
View File
@@ -0,0 +1,125 @@
// Copyright 2019 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package main
import (
"bytes"
"crypto/ed25519"
"crypto/rsa"
"crypto/sha256"
"fmt"
"os"
"github.com/FiloSottile/age/internal/age"
"github.com/FiloSottile/age/internal/format"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
)
type EncryptedSSHIdentity struct {
pubKey ssh.PublicKey
pemBytes []byte
passphrase func() ([]byte, error)
decrypted age.Identity
}
func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {
switch t := pubKey.Type(); t {
case "ssh-ed25519", "ssh-rsa":
default:
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
}
return &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}, nil
}
var _ age.IdentityMatcher = &EncryptedSSHIdentity{}
func (i *EncryptedSSHIdentity) Type() string {
return i.pubKey.Type()
}
func (i *EncryptedSSHIdentity) Unwrap(block *format.Recipient) (fileKey []byte, err error) {
if i.decrypted != nil {
return i.decrypted.Unwrap(block)
}
passphrase, err := i.passphrase()
if err != nil {
return nil, fmt.Errorf("failed to obtain passphrase: %v", err)
}
k, err := ssh.ParseRawPrivateKeyWithPassphrase(i.pemBytes, passphrase)
if err != nil {
return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err)
}
switch k := k.(type) {
case *ed25519.PrivateKey:
i.decrypted, err = age.NewSSHEd25519Identity(*k)
case *rsa.PrivateKey:
i.decrypted, err = age.NewSSHRSAIdentity(k)
default:
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
}
if err != nil {
return nil, fmt.Errorf("invalid SSH key: %v", err)
}
if i.decrypted.Type() != i.pubKey.Type() {
return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", i.decrypted.Type(), i.pubKey.Type())
}
return i.decrypted.Unwrap(block)
}
func (i *EncryptedSSHIdentity) Matches(block *format.Recipient) error {
if block.Type != i.Type() {
return age.ErrIncorrectIdentity
}
if len(block.Args) != 1 {
return fmt.Errorf("invalid %v recipient block", i.Type())
}
hash, err := format.DecodeString(block.Args[0])
if err != nil {
return fmt.Errorf("failed to parse %v recipient: %v", i.Type(), err)
}
if len(hash) != 4 {
return fmt.Errorf("invalid %v recipient block", i.Type())
}
sH := sha256.New()
sH.Write(i.pubKey.Marshal())
hh := sH.Sum(nil)
if !bytes.Equal(hh[:4], hash) {
return age.ErrIncorrectIdentity
}
return nil
}
func passphrasePrompt(name string) func() ([]byte, error) {
return func() ([]byte, error) {
fd := int(os.Stdin.Fd())
if !terminal.IsTerminal(fd) {
tty, err := os.Open("/dev/tty")
if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: standard input is not a terminal, and opening /dev/tty failed: %v", name, err)
}
defer tty.Close()
fd = int(tty.Fd())
}
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name)
defer fmt.Fprintf(os.Stderr, "\n")
p, err := terminal.ReadPassword(fd)
if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
}
return p, nil
}
}
+34
View File
@@ -16,6 +16,7 @@ import (
"strings"
"github.com/FiloSottile/age/internal/age"
"golang.org/x/crypto/ssh"
)
func parseRecipient(arg string) (age.Recipient, error) {
@@ -82,9 +83,42 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) {
func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
id, err := age.ParseSSHIdentity(pemBytes)
if sshErr, ok := err.(*ssh.PassphraseNeededError); ok {
pubKey := sshErr.PublicKey
if pubKey == nil {
pubKey, err = readPubFile(name)
if err != nil {
return nil, err
}
}
i, err := NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt(name))
if err != nil {
return nil, err
}
return []age.Identity{i}, nil
}
if err != nil {
return nil, fmt.Errorf("malformed SSH identity in %q: %v", name, err)
}
return []age.Identity{id}, nil
}
func readPubFile(name string) (ssh.PublicKey, error) {
f, err := os.Open(name + ".pub")
if err != nil {
return nil, fmt.Errorf(`failed to obtain public key for %q SSH key: %v
Ensure %q exists, or convert the private key %q to a modern format with "ssh-keygen -p -m RFC4716"`, name, err, name+".pub", name)
}
defer f.Close()
contents, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name+".pub", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents)
if err != nil {
return nil, fmt.Errorf("failed to parse %q: %v", name+".pub", err)
}
return pubKey, nil
}