File: challenge.go

package info (click to toggle)
golang-github-muesli-sasquatch 0.0~git20210519.30aff9d-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 184 kB
  • sloc: makefile: 2
file content (153 lines) | stat: -rw-r--r-- 3,700 bytes parent folder | download
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
}