sigtool now supports openssh ed25519 public and private keys.
* Added support to read openssh public keys and encrypted private keys * reworked private key handling * made password the default; generating keys without password requires explicit "--no-password"
This commit is contained in:
parent
b14f9d1e53
commit
f82c1336ac
8 changed files with 581 additions and 78 deletions
27
README.md
27
README.md
|
@ -21,6 +21,9 @@ during the encryption process - this has the benefit of also authenticating
|
|||
the sender (and the receiver can verify the sender if they possess the
|
||||
corresponding public key).
|
||||
|
||||
The sign, verify, encrypt, decrypt operations can use OpenSSH Ed25519 keys
|
||||
*or* the keys generated by sigtool. This means, you can send encrypted
|
||||
files to any recipient identified by their comment in `~/.ssh/authorized_keys`.
|
||||
|
||||
## How do I build it?
|
||||
With Go 1.5 and later:
|
||||
|
@ -61,15 +64,15 @@ e.g., to sign `archive.tar.gz` with private key `/tmp/testkey.key`:
|
|||
|
||||
sigtool sign /tmp/testkey.key archive.tar.gz
|
||||
|
||||
If *testkey.key* was encrypted with a user pass phrase:
|
||||
If *testkey.key* was encrypted without a user pass phrase:
|
||||
|
||||
sigtool sign -p /tmp/testkey.key archive.tar.gz
|
||||
sigtool sign --no-password /tmp/testkey.key archive.tar.gz
|
||||
|
||||
|
||||
The signature can also be written directly to a user supplied output
|
||||
file.
|
||||
|
||||
sigtool sign -p -o archive.sig /tmp/testkey.key archive.tar.gz
|
||||
sigtool sign -o archive.sig /tmp/testkey.key archive.tar.gz
|
||||
|
||||
|
||||
### Verify a signature against a file
|
||||
|
@ -85,6 +88,10 @@ e.g., to verify the signature of *archive.tar.gz* against
|
|||
|
||||
sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz
|
||||
|
||||
|
||||
Note that signing and verifying can also work with OpenSSH ed25519
|
||||
keys.
|
||||
|
||||
### Encrypt a file by authenticating the sender
|
||||
If the sender wishes to prove to the recipient that they encrypted
|
||||
a file:
|
||||
|
@ -115,6 +122,20 @@ the receiver doesn't need to authenticate the sender:
|
|||
This will create an encrypted file *archive.tar.gz.enc* such that the
|
||||
recipient in possession of *to.key* can decrypt it.
|
||||
|
||||
### Encrypt a file to an OpenSSH recipient *without* authenticating the sender
|
||||
Suppose you want to send an encrypted file where the recipient's
|
||||
public key is in `~/.ssh/authorized_keys`. Such a recipient is identified
|
||||
by their OpenSSH key comment (typically `name@domain`):
|
||||
|
||||
sigtool encrypt user@domain -o archive.tar.gz.enc archive.tar.gz
|
||||
|
||||
If you have their public key in file "name-domain.pub", you can do:
|
||||
|
||||
sigtool encrypt name-domain.pub -o archive.tar.gz.enc archive.tar.gz
|
||||
|
||||
This will create an encrypted file *archive.tar.gz.enc* such that the
|
||||
recipient can decrypt using their private key.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### How is the private key protected?
|
||||
|
|
91
crypt.go
91
crypt.go
|
@ -16,7 +16,9 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/opencoff/go-utils"
|
||||
flag "github.com/opencoff/pflag"
|
||||
|
@ -34,12 +36,12 @@ func encrypt(args []string) {
|
|||
var outfile string
|
||||
var keyfile string
|
||||
var envpw string
|
||||
var pw bool
|
||||
var nopw bool
|
||||
var sblksize string
|
||||
|
||||
fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`")
|
||||
fs.StringVarP(&keyfile, "sign", "s", "", "Sign using private key `S`")
|
||||
fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key")
|
||||
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key")
|
||||
fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`")
|
||||
fs.StringVarP(&sblksize, "block-size", "B", "4M", "Use `S` as the encryption block size")
|
||||
|
||||
|
@ -55,19 +57,23 @@ func encrypt(args []string) {
|
|||
|
||||
var pws, infile string
|
||||
|
||||
var sk *sign.PrivateKey
|
||||
|
||||
if len(keyfile) > 0 {
|
||||
sk, err = sign.ReadPrivateKey(keyfile, func() ([]byte, error) {
|
||||
if nopw {
|
||||
return nil, nil
|
||||
}
|
||||
if len(envpw) > 0 {
|
||||
pws = os.Getenv(envpw)
|
||||
} else if pw {
|
||||
} else {
|
||||
pws, err = utils.Askpass("Enter passphrase for private key", false)
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
var sk *sign.PrivateKey
|
||||
|
||||
if len(keyfile) > 0 {
|
||||
sk, err = sign.ReadPrivateKey(keyfile, pws)
|
||||
return []byte(pws), nil
|
||||
})
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
|
@ -75,7 +81,7 @@ func encrypt(args []string) {
|
|||
|
||||
args = fs.Args()
|
||||
if len(args) < 2 {
|
||||
die("Insufficient args. Try '%s --help'")
|
||||
die("Insufficient args. Try '%s --help'", os.Args[0])
|
||||
}
|
||||
|
||||
var infd io.Reader = os.Stdin
|
||||
|
@ -92,6 +98,27 @@ func encrypt(args []string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Lets try to read the authorized files
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
die("can't find homedir for this user")
|
||||
}
|
||||
|
||||
authkeys := fmt.Sprintf("%s/.ssh/authorized_keys", home)
|
||||
authdata, err := ioutil.ReadFile(authkeys)
|
||||
if err != nil {
|
||||
if err != os.ErrNotExist {
|
||||
die("can't open %s: %s", authkeys, err)
|
||||
}
|
||||
}
|
||||
|
||||
pka, err := sign.ParseAuthorizedKeys(authdata)
|
||||
keymap := make(map[string]*sign.PublicKey)
|
||||
|
||||
for _, pk := range pka {
|
||||
keymap[pk.Comment] = pk
|
||||
}
|
||||
|
||||
if len(outfile) > 0 && outfile != "-" {
|
||||
if inf != nil {
|
||||
ost, err := os.Stat(outfile)
|
||||
|
@ -120,11 +147,27 @@ func encrypt(args []string) {
|
|||
die("%s", err)
|
||||
}
|
||||
|
||||
errs := 0
|
||||
for i := 0; i < len(args)-1; i++ {
|
||||
var err error
|
||||
var pk *sign.PublicKey
|
||||
|
||||
fn := args[i]
|
||||
pk, err := sign.ReadPublicKey(fn)
|
||||
if strings.Index(fn, "@") > 0 {
|
||||
var ok bool
|
||||
pk, ok = keymap[fn]
|
||||
if !ok {
|
||||
warn("can't find user %s in %s", fn, authkeys)
|
||||
errs += 1
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
pk, err = sign.ReadPublicKey(fn)
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
warn("%s", err)
|
||||
errs += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = en.AddRecipient(pk)
|
||||
|
@ -133,6 +176,10 @@ func encrypt(args []string) {
|
|||
}
|
||||
}
|
||||
|
||||
if errs > 0 {
|
||||
die("Too many errors!")
|
||||
}
|
||||
|
||||
err = en.Encrypt(infd, outfd)
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
|
@ -149,10 +196,10 @@ func decrypt(args []string) {
|
|||
var envpw string
|
||||
var outfile string
|
||||
var pubkey string
|
||||
var pw bool
|
||||
var nopw bool
|
||||
|
||||
fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`")
|
||||
fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key")
|
||||
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key")
|
||||
fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`")
|
||||
fs.StringVarP(&pubkey, "verify-sender", "v", "", "Verify that the sender matches public key in `F`")
|
||||
|
||||
|
@ -163,25 +210,31 @@ func decrypt(args []string) {
|
|||
|
||||
args = fs.Args()
|
||||
if len(args) < 1 {
|
||||
die("Insufficient args. Try '%s --help'")
|
||||
die("Insufficient args. Try '%s --help'", os.Args[0])
|
||||
}
|
||||
|
||||
var infd io.Reader = os.Stdin
|
||||
var outfd io.Writer = os.Stdout
|
||||
var inf *os.File
|
||||
var pws, infile string
|
||||
var infile string
|
||||
|
||||
keyfile := args[0]
|
||||
sk, err := sign.ReadPrivateKey(keyfile, func() ([]byte, error) {
|
||||
var pws string
|
||||
if nopw {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(envpw) > 0 {
|
||||
pws = os.Getenv(envpw)
|
||||
} else if pw {
|
||||
} else {
|
||||
pws, err = utils.Askpass("Enter passphrase for private key", false)
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
keyfile := args[0]
|
||||
sk, err := sign.ReadPrivateKey(keyfile, pws)
|
||||
return []byte(pws), nil
|
||||
})
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/opencoff/sigtool
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a
|
||||
github.com/gogo/protobuf v1.3.1
|
||||
github.com/opencoff/go-utils v0.4.0
|
||||
github.com/opencoff/pflag v0.3.3
|
||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
|
|
58
sign/sign.go
58
sign/sign.go
|
@ -20,6 +20,7 @@
|
|||
package sign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
|
@ -55,6 +56,9 @@ type PrivateKey struct {
|
|||
type PublicKey struct {
|
||||
Pk []byte
|
||||
|
||||
// Comment string
|
||||
Comment string
|
||||
|
||||
// Curve25519 point corresponding to this Ed25519 key
|
||||
ck []byte
|
||||
|
||||
|
@ -163,7 +167,7 @@ func NewKeypair() (*Keypair, error) {
|
|||
// goes in $bn.key.
|
||||
// If password is non-empty, then the private key is encrypted
|
||||
// before writing to disk.
|
||||
func (kp *Keypair) Serialize(bn, comment string, pw string) error {
|
||||
func (kp *Keypair) Serialize(bn, comment string, getpw func() ([]byte, error)) error {
|
||||
|
||||
sk := &kp.Sec
|
||||
pk := &kp.Pub
|
||||
|
@ -176,7 +180,7 @@ func (kp *Keypair) Serialize(bn, comment string, pw string) error {
|
|||
return fmt.Errorf("Can't serialize to %s: %s", pkf, err)
|
||||
}
|
||||
|
||||
err = sk.serialize(skf, comment, pw)
|
||||
err = sk.serialize(skf, comment, getpw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Can't serialize to %s: %s", pkf, err)
|
||||
}
|
||||
|
@ -186,18 +190,25 @@ func (kp *Keypair) Serialize(bn, comment string, pw string) error {
|
|||
|
||||
// Read the private key in 'fn', optionally decrypting it using
|
||||
// password 'pw' and create new instance of PrivateKey
|
||||
func ReadPrivateKey(fn string, pw string) (*PrivateKey, error) {
|
||||
func ReadPrivateKey(fn string, getpw func() ([]byte, error)) (*PrivateKey, error) {
|
||||
yml, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Index(yml, []byte("OPENSSH PRIVATE KEY-")) > 0 {
|
||||
return parseSSHPrivateKey(yml, getpw)
|
||||
}
|
||||
|
||||
if pw, err := getpw(); err == nil {
|
||||
return MakePrivateKey(yml, pw)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make a private key from bytes 'yml' and password 'pw'. The bytes
|
||||
// are assumed to be serialized version of the private key.
|
||||
func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) {
|
||||
func MakePrivateKey(yml []byte, pw []byte) (*PrivateKey, error) {
|
||||
var ssk serializedPrivKey
|
||||
|
||||
err := yaml.Unmarshal(yml, &ssk)
|
||||
|
@ -224,7 +235,7 @@ func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) {
|
|||
}
|
||||
|
||||
// We take short passwords and extend them
|
||||
pwb := sha512.Sum512([]byte(pw))
|
||||
pwb := sha512.Sum512(pw)
|
||||
|
||||
xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(esk.Esk))
|
||||
if err != nil {
|
||||
|
@ -250,11 +261,14 @@ func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) {
|
|||
}
|
||||
|
||||
// Make a private key from 64-bytes of extended Ed25519 key
|
||||
func PrivateKeyFromBytes(skb []byte) (*PrivateKey, error) {
|
||||
if len(skb) != 64 {
|
||||
return nil, fmt.Errorf("private key is malformed (len %d!)", len(skb))
|
||||
func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) {
|
||||
if len(buf) != 64 {
|
||||
return nil, fmt.Errorf("private key is malformed (len %d!)", len(buf))
|
||||
}
|
||||
|
||||
skb := make([]byte, 64)
|
||||
copy(skb, buf)
|
||||
|
||||
edsk := Ed.PrivateKey(skb)
|
||||
edpk := edsk.Public().(Ed.PublicKey)
|
||||
|
||||
|
@ -283,16 +297,19 @@ func (pk *PublicKey) Hash() []byte {
|
|||
// Serialize the private key to a file
|
||||
// Format: YAML
|
||||
// All []byte are in base64 (RawEncoding)
|
||||
func (sk *PrivateKey) serialize(fn, comment string, pw string) error {
|
||||
func (sk *PrivateKey) serialize(fn, comment string, getpw func() ([]byte, error)) error {
|
||||
|
||||
pw, err := getpw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b64 := base64.StdEncoding.EncodeToString
|
||||
esk := &encPrivKey{}
|
||||
ssk := &serializedPrivKey{Comment: comment}
|
||||
|
||||
// Even with an empty password, we still encrypt and store.
|
||||
|
||||
// expand the password into 64 bytes
|
||||
pwb := sha512.Sum512([]byte(pw))
|
||||
pwb := sha512.Sum512(pw)
|
||||
|
||||
esk.N = _N
|
||||
esk.r = _r
|
||||
|
@ -455,7 +472,12 @@ func ReadPublicKey(fn string) (*PublicKey, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return MakePublicKey(yml)
|
||||
// first try to parse as a ssh key
|
||||
pk, err := parseSSHPublicKey(yml)
|
||||
if err != nil {
|
||||
pk, err = MakePublicKey(yml)
|
||||
}
|
||||
return pk, err
|
||||
}
|
||||
|
||||
// Parse a serialized public in 'yml' and return the resulting
|
||||
|
@ -469,13 +491,17 @@ func MakePublicKey(yml []byte) (*PublicKey, error) {
|
|||
}
|
||||
|
||||
b64 := base64.StdEncoding.DecodeString
|
||||
var pk []byte
|
||||
var pkb []byte
|
||||
|
||||
if pk, err = b64(spk.Pk); err != nil {
|
||||
if pkb, err = b64(spk.Pk); err != nil {
|
||||
return nil, fmt.Errorf("can't decode YAML:Pk: %s", err)
|
||||
}
|
||||
|
||||
return PublicKeyFromBytes(pk)
|
||||
if pk, err := PublicKeyFromBytes(pkb); err == nil {
|
||||
pk.Comment = spk.Comment
|
||||
return pk, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make a public key from a byte string
|
||||
|
|
389
sign/ssh.go
Normal file
389
sign/ssh.go
Normal file
|
@ -0,0 +1,389 @@
|
|||
// ssh.go - support for reading ssh private and public keys
|
||||
//
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file is a bastardization of github.com/ScaleFT/sshkeys and
|
||||
// golang.org/x/crypto/ssh/keys.go
|
||||
//
|
||||
// It is licensed under the terms of the original go source code
|
||||
// OR the Apache 2.0 license (terms of sshkeys).
|
||||
//
|
||||
// Changes from that version:
|
||||
// - don't use password but call a func() to get the password as needed
|
||||
// - narrowly scope the key support for ONLY ed25519 keys
|
||||
// - support reading multiple public keys from authorized_keys
|
||||
|
||||
package sign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dchest/bcrypt_pbkdf"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrIncorrectPassword = errors.New("ssh: Invalid Passphrase")
|
||||
ErrNoPEMFound = errors.New("no PEM block found")
|
||||
ErrBadPublicKey = errors.New("ssh: malformed public key")
|
||||
ErrKeyTooShort = errors.New("ssh: public key too short")
|
||||
ErrBadTrailers = errors.New("ssh: trailing junk in public key")
|
||||
ErrBadFormat = errors.New("ssh: invalid openssh private key format")
|
||||
ErrBadLength = errors.New("ssh: private key unexpected length")
|
||||
ErrBadPadding = errors.New("ssh: padding not as expected")
|
||||
)
|
||||
|
||||
const keySizeAES256 = 32
|
||||
|
||||
// ParseEncryptedRawPrivateKey returns a private key from an
|
||||
// encrypted ed25519 private key.
|
||||
func parseSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*PrivateKey, error) {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, ErrNoPEMFound
|
||||
}
|
||||
|
||||
if x509.IsEncryptedPEMBlock(block) {
|
||||
return nil, fmt.Errorf("ssh: no support for legacy PEM encrypted keys")
|
||||
}
|
||||
|
||||
switch block.Type {
|
||||
case "OPENSSH PRIVATE KEY":
|
||||
return parseOpenSSHPrivateKey(block.Bytes, getpw)
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func parseSSHPublicKey(in []byte) (*PublicKey, error) {
|
||||
v := bytes.Split(in, []byte(" \t"))
|
||||
if len(v) != 3 {
|
||||
return nil, ErrBadPublicKey
|
||||
}
|
||||
|
||||
return parseEncPubKey(v[1], string(v[2]))
|
||||
}
|
||||
|
||||
// parse a wire encoded public key
|
||||
func parseEncPubKey(in []byte, comm string) (*PublicKey, error) {
|
||||
in, err := base64.StdEncoding.DecodeString(string(in))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
algo, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return nil, ErrKeyTooShort
|
||||
|
||||
}
|
||||
if string(algo) != ssh.KeyAlgoED25519 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var w struct {
|
||||
KeyBytes []byte
|
||||
Rest []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
if err := ssh.Unmarshal(in, &w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(w.Rest) > 0 {
|
||||
return nil, ErrBadTrailers
|
||||
}
|
||||
|
||||
pk, err := PublicKeyFromBytes(w.KeyBytes)
|
||||
if err == nil {
|
||||
pk.Comment = strings.TrimSpace(comm)
|
||||
}
|
||||
return pk, err
|
||||
}
|
||||
|
||||
func parseString(in []byte) (out, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(in)
|
||||
in = in[4:]
|
||||
if uint32(len(in)) < length {
|
||||
return
|
||||
}
|
||||
out = in[:length]
|
||||
rest = in[length:]
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// parseAuthorizedKey parses a public key in OpenSSH binary format and decodes it.
|
||||
// removed.
|
||||
func parseAuthorizedKey(in []byte) (*PublicKey, error) {
|
||||
in = bytes.TrimSpace(in)
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
i = len(in)
|
||||
}
|
||||
|
||||
pk, err := parseEncPubKey(in[:i], string(in[i:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
// ParseAuthorizedKeys parses a public key from an authorized_keys
|
||||
// file used in OpenSSH according to the sshd(8) manual page.
|
||||
func ParseAuthorizedKeys(in []byte) ([]*PublicKey, error) {
|
||||
var pka []*PublicKey
|
||||
var rest []byte
|
||||
|
||||
for len(in) > 0 {
|
||||
end := bytes.IndexByte(in, '\n')
|
||||
if end != -1 {
|
||||
rest = in[end+1:]
|
||||
in = in[:end]
|
||||
} else {
|
||||
rest = nil
|
||||
}
|
||||
|
||||
end = bytes.IndexByte(in, '\r')
|
||||
if end != -1 {
|
||||
in = in[:end]
|
||||
}
|
||||
|
||||
in = bytes.TrimSpace(in)
|
||||
if len(in) == 0 || in[0] == '#' {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if pk, err := parseAuthorizedKey(in[i:]); err == nil {
|
||||
if pk != nil {
|
||||
pka = append(pka, pk)
|
||||
}
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
// No key type recognised. Maybe there's an options field at
|
||||
// the beginning.
|
||||
var b byte
|
||||
inQuote := false
|
||||
var candidateOptions []string
|
||||
optionStart := 0
|
||||
for i, b = range in {
|
||||
isEnd := !inQuote && (b == ' ' || b == '\t')
|
||||
if (b == ',' && !inQuote) || isEnd {
|
||||
if i-optionStart > 0 {
|
||||
candidateOptions = append(candidateOptions, string(in[optionStart:i]))
|
||||
}
|
||||
optionStart = i + 1
|
||||
}
|
||||
if isEnd {
|
||||
break
|
||||
}
|
||||
if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
|
||||
inQuote = !inQuote
|
||||
}
|
||||
}
|
||||
for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i == len(in) {
|
||||
// Invalid line: unmatched quote
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
in = in[i:]
|
||||
i = bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if pk, err := parseAuthorizedKey(in[i:]); err == nil {
|
||||
if pk != nil {
|
||||
pka = append(pka, pk)
|
||||
}
|
||||
}
|
||||
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
return pka, nil
|
||||
}
|
||||
|
||||
const opensshv1Magic = "openssh-key-v1"
|
||||
|
||||
type opensshHeader struct {
|
||||
CipherName string
|
||||
KdfName string
|
||||
KdfOpts string
|
||||
NumKeys uint32
|
||||
PubKey string
|
||||
PrivKeyBlock string
|
||||
}
|
||||
|
||||
type opensshKey struct {
|
||||
Check1 uint32
|
||||
Check2 uint32
|
||||
Keytype string
|
||||
Rest []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
type opensshED25519 struct {
|
||||
Pub []byte
|
||||
Priv []byte
|
||||
Comment string
|
||||
Pad []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
func parseOpenSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*PrivateKey, error) {
|
||||
magic := append([]byte(opensshv1Magic), 0)
|
||||
if !bytes.Equal(magic, data[0:len(magic)]) {
|
||||
return nil, ErrBadFormat
|
||||
}
|
||||
remaining := data[len(magic):]
|
||||
|
||||
w := opensshHeader{}
|
||||
|
||||
if err := ssh.Unmarshal(remaining, &w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if w.NumKeys != 1 {
|
||||
return nil, fmt.Errorf("ssh: NumKeys must be 1: %d", w.NumKeys)
|
||||
}
|
||||
|
||||
var privateKeyBytes []byte
|
||||
var encrypted bool
|
||||
|
||||
switch {
|
||||
// OpenSSH supports bcrypt KDF w/ AES256-CBC or AES256-CTR mode
|
||||
case w.KdfName == "bcrypt" && w.CipherName == "aes256-cbc":
|
||||
pw, err := getpw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iv, block, err := extractBcryptIvBlock(pw, &w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cbc := cipher.NewCBCDecrypter(block, iv)
|
||||
privateKeyBytes = []byte(w.PrivKeyBlock)
|
||||
cbc.CryptBlocks(privateKeyBytes, privateKeyBytes)
|
||||
|
||||
encrypted = true
|
||||
|
||||
case w.KdfName == "bcrypt" && w.CipherName == "aes256-ctr":
|
||||
pw, err := getpw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iv, block, err := extractBcryptIvBlock(pw, &w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream := cipher.NewCTR(block, iv)
|
||||
privateKeyBytes = []byte(w.PrivKeyBlock)
|
||||
stream.XORKeyStream(privateKeyBytes, privateKeyBytes)
|
||||
|
||||
encrypted = true
|
||||
|
||||
case w.KdfName == "none" && w.CipherName == "none":
|
||||
privateKeyBytes = []byte(w.PrivKeyBlock)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unknown Cipher/KDF: %s:%s", w.CipherName, w.KdfName)
|
||||
}
|
||||
|
||||
pk1 := opensshKey{}
|
||||
|
||||
if err := ssh.Unmarshal(privateKeyBytes, &pk1); err != nil {
|
||||
if encrypted {
|
||||
return nil, ErrIncorrectPassword
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pk1.Check1 != pk1.Check2 {
|
||||
return nil, ErrIncorrectPassword
|
||||
}
|
||||
|
||||
// we only handle ed25519 and rsa keys currently
|
||||
switch pk1.Keytype {
|
||||
case ssh.KeyAlgoED25519:
|
||||
key := opensshED25519{}
|
||||
|
||||
err := ssh.Unmarshal(pk1.Rest, &key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(key.Priv) != ed25519.PrivateKeySize {
|
||||
return nil, ErrBadLength
|
||||
}
|
||||
|
||||
for i, b := range key.Pad {
|
||||
if int(b) != i+1 {
|
||||
return nil, ErrBadPadding
|
||||
}
|
||||
}
|
||||
|
||||
pk, err := PrivateKeyFromBytes(key.Priv)
|
||||
return pk, err
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unhandled key type: %v", pk1.Keytype)
|
||||
}
|
||||
}
|
||||
|
||||
func extractBcryptIvBlock(passphrase []byte, w *opensshHeader) ([]byte, cipher.Block, error) {
|
||||
cipherKeylen := keySizeAES256
|
||||
cipherIvLen := aes.BlockSize
|
||||
|
||||
var opts struct {
|
||||
Salt []byte
|
||||
Rounds uint32
|
||||
}
|
||||
|
||||
if err := ssh.Unmarshal([]byte(w.KdfOpts), &opts); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
kdfdata, err := bcrypt_pbkdf.Key(passphrase, opts.Salt, int(opts.Rounds), cipherKeylen+cipherIvLen)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
iv := kdfdata[cipherKeylen : cipherIvLen+cipherKeylen]
|
||||
aeskey := kdfdata[0:cipherKeylen]
|
||||
block, err := aes.NewCipher(aeskey)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return iv, block, nil
|
||||
}
|
55
sigtool.go
55
sigtool.go
|
@ -87,13 +87,13 @@ func main() {
|
|||
// Run the generate command
|
||||
func gen(args []string) {
|
||||
|
||||
var pw, help, force bool
|
||||
var nopw, help, force bool
|
||||
var comment string
|
||||
var envpw string
|
||||
|
||||
fs := flag.NewFlagSet("generate", flag.ExitOnError)
|
||||
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
||||
fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to encrypt the private key")
|
||||
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for a password for the private key")
|
||||
fs.StringVarP(&comment, "comment", "c", "", "Use `C` as the text comment for the keys")
|
||||
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
||||
fs.BoolVarP(&force, "force", "F", false, "Overwrite the output file if it exists")
|
||||
|
@ -124,24 +124,29 @@ Options:
|
|||
die("Public/Private key files (%s.key, %s.pub) exist. Won't overwrite!", bn, bn)
|
||||
}
|
||||
|
||||
var pws string
|
||||
var err error
|
||||
|
||||
if len(envpw) > 0 {
|
||||
pws = os.Getenv(envpw)
|
||||
} else if pw {
|
||||
pws, err = utils.Askpass("Enter passphrase for private key", true)
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
kp, err := sign.NewKeypair()
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
|
||||
err = kp.Serialize(bn, comment, pws)
|
||||
err = kp.Serialize(bn, comment, func() ([]byte, error) {
|
||||
if nopw {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pws string
|
||||
if len(envpw) > 0 {
|
||||
pws = os.Getenv(envpw)
|
||||
} else {
|
||||
pws, err = utils.Askpass("Enter passphrase for private key", true)
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
}
|
||||
return []byte(pws), nil
|
||||
})
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
|
@ -149,13 +154,13 @@ Options:
|
|||
|
||||
// Run the 'sign' command.
|
||||
func signify(args []string) {
|
||||
var pw, help bool
|
||||
var nopw, help bool
|
||||
var output string
|
||||
var envpw string
|
||||
|
||||
fs := flag.NewFlagSet("sign", flag.ExitOnError)
|
||||
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
||||
fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key")
|
||||
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for a password for the private key")
|
||||
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
||||
fs.StringVarP(&output, "output", "o", "", "Write signature to file `F`")
|
||||
|
||||
|
@ -182,23 +187,29 @@ Options:
|
|||
fn := args[1]
|
||||
outf := fmt.Sprintf("%s.sig", fn)
|
||||
|
||||
var pws string
|
||||
var err error
|
||||
|
||||
if len(output) > 0 {
|
||||
outf = output
|
||||
}
|
||||
|
||||
sk, err := sign.ReadPrivateKey(kn, func() ([]byte, error) {
|
||||
if nopw {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pws string
|
||||
if len(envpw) > 0 {
|
||||
pws = os.Getenv(envpw)
|
||||
} else if pw {
|
||||
} else {
|
||||
pws, err = utils.Askpass("Enter passphrase for private key", false)
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(output) > 0 {
|
||||
outf = output
|
||||
}
|
||||
|
||||
sk, err := sign.ReadPrivateKey(kn, pws)
|
||||
return []byte(pws), nil
|
||||
})
|
||||
if err != nil {
|
||||
die("%s", err)
|
||||
}
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
0.3.1
|
||||
0.4.0
|
||||
|
|
Loading…
Add table
Reference in a new issue