1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
|
package sasquatch
import (
"crypto/rand"
"errors"
"fmt"
"strconv"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/scrypt"
"golang.org/x/crypto/ssh"
)
const challengeLabel = "charm.sh/v1/challenge"
type ChallengeRecipient struct {
signer ssh.Signer
workFactor int
}
var _ Recipient = &ChallengeRecipient{}
func (*ChallengeRecipient) Type() string { return "challenge" }
// NewChallengeRecipient returns a new ChallengeRecipient with the provided
// signer.
func NewChallengeRecipient(signer ssh.Signer) (*ChallengeRecipient, error) {
r := &ChallengeRecipient{
signer: signer,
// TODO: automatically scale this to 0.5s (with a min) in the CLI.
workFactor: 8, // 0.5s on a modern machine
}
return r, nil
}
func (r *ChallengeRecipient) Wrap(fileKey []byte) (*Stanza, error) {
challenge := make([]byte, 64)
if _, err := rand.Read(challenge[:]); err != nil {
return nil, err
}
sig, err := r.signer.Sign(rand.Reader, challenge)
if err != nil {
return nil, err
}
salt := make([]byte, 16)
if _, err := rand.Read(salt[:]); err != nil {
return nil, err
}
logN := r.workFactor
l := &Stanza{
Type: "challenge",
Args: []string{
sshFingerprint(r.signer.PublicKey()),
EncodeToString(salt),
strconv.Itoa(logN),
EncodeToString(challenge),
},
}
salt = append([]byte(challengeLabel), salt...)
k, err := scrypt.Key(sig.Blob, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
if err != nil {
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
}
wrappedKey, err := aeadEncrypt(k, fileKey)
if err != nil {
return nil, err
}
l.Body = wrappedKey
return l, nil
}
// ChallengeIdentity is a challenge-based identity, supporting SSH agents.
type ChallengeIdentity struct {
signer ssh.Signer
}
var _ Identity = &ChallengeIdentity{}
func (*ChallengeIdentity) Type() string { return "challenge" }
// NewChallengeIdentity returns a new ChallengeIdentity with the provided
// challenge signer.
func NewChallengeIdentity(signer ssh.Signer) (*ChallengeIdentity, error) {
i := &ChallengeIdentity{
signer: signer,
}
return i, nil
}
func (i *ChallengeIdentity) Unwrap(block *Stanza) ([]byte, error) {
if block.Type != "challenge" {
return nil, ErrIncorrectIdentity
}
if len(block.Args) != 4 {
return nil, errors.New("invalid challenge recipient block")
}
challenge, err := DecodeString(block.Args[3])
if err != nil {
return nil, fmt.Errorf("failed to parse challenge challenge: %v", err)
}
sig, err := i.signer.Sign(rand.Reader, challenge)
if err != nil {
return nil, err
}
salt, err := DecodeString(block.Args[1])
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt salt: %v", err)
}
if len(salt) != 16 {
return nil, errors.New("invalid scrypt recipient block")
}
logN, err := strconv.Atoi(block.Args[2])
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err)
}
salt = append([]byte(challengeLabel), salt...)
k, err := scrypt.Key(sig.Blob, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
if err != nil {
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
}
fileKey, err := aeadDecrypt(k, block.Body)
if err != nil {
return nil, ErrIncorrectIdentity
}
return fileKey, nil
}
// Match implements IdentityMatcher without decrypting the payload, to
// ensure the agent is only contacted if necessary.
func (i *ChallengeIdentity) Match(block *Stanza) error {
if block.Type != i.Type() {
return ErrIncorrectIdentity
}
if len(block.Args) != 4 {
return fmt.Errorf("invalid %v recipient block", i.Type())
}
if block.Args[0] != sshFingerprint(i.signer.PublicKey()) {
return ErrIncorrectIdentity
}
return nil
}
|