v0.5.1 pin scram wire format and edge tests
Test and Fuzz / Build, Test, and Fuzz (push) Canceled after 0s
Test and Fuzz / Build, Test, and Fuzz (push) Canceled after 0s
This commit is contained in:
1 parent
b5524a3b9d
commit
0dc8189147
4 files changed
+40
-3
No files matched your search
@@ -60,6 +60,11 @@ never agree, so there is no silent downgrade; an empty or oversized (>64 bytes)
|
|||||||
binding returns `ErrSCRAMChannelBinding`. A mismatch is reported as
|
binding returns `ErrSCRAMChannelBinding`. A mismatch is reported as
|
||||||
`ErrInvalidCredentials`, like a wrong password. When later requests travel on
|
`ErrInvalidCredentials`, like a wrong password. When later requests travel on
|
||||||
new connections (HTTP), the client must also pin the bound certificate on them.
|
new connections (HTTP), the client must also pin the bound certificate on them.
|
||||||
|
Nil options are ignored; a repeated `WithChannelBinding` keeps the last one.
|
||||||
|
|
||||||
|
v0.5.0 added the variadic `...ExchangeOption` parameter to both methods. Calls
|
||||||
|
compile unchanged; interfaces or method values declaring the old signatures must
|
||||||
|
be updated.
|
||||||
|
|
||||||
Check every error and abort the exchange on failure. Only a successful **server
|
Check every error and abort the exchange on failure. Only a successful **server
|
||||||
final** authenticates a user; use its `Username`, never a separate identity from
|
final** authenticates a user; use its `Username`, never a separate identity from
|
||||||
|
|||||||
@@ -784,6 +784,7 @@ func (c *ScramClient) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExchangeOption configures the proof step of one exchange, on either side.
|
// ExchangeOption configures the proof step of one exchange, on either side.
|
||||||
|
// Nil options are ignored; a repeated WithChannelBinding keeps the last one.
|
||||||
type ExchangeOption func(*exchangeOptions)
|
type ExchangeOption func(*exchangeOptions)
|
||||||
|
|
||||||
type exchangeOptions struct {
|
type exchangeOptions struct {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ func FuzzScramServerMessages(f *testing.F) {
|
|||||||
f.Add("unknown", "nonce", base64.StdEncoding.EncodeToString(make([]byte, 32)), []byte(nil))
|
f.Add("unknown", "nonce", base64.StdEncoding.EncodeToString(make([]byte, 32)), []byte(nil))
|
||||||
f.Add("", "", "", []byte{})
|
f.Add("", "", "", []byte{})
|
||||||
f.Add("a,n=b", "nonce", "!!!", make([]byte, 32))
|
f.Add("a,n=b", "nonce", "!!!", make([]byte, 32))
|
||||||
|
f.Add("unknown", "nonce", base64.StdEncoding.EncodeToString(make([]byte, 32)), make([]byte, MaxChannelBindingLen))
|
||||||
f.Fuzz(func(t *testing.T, username, nonce, proof string, binding []byte) {
|
f.Fuzz(func(t *testing.T, username, nonce, proof string, binding []byte) {
|
||||||
var opts []ExchangeOption
|
var opts []ExchangeOption
|
||||||
if len(binding) > 0 && len(binding) <= MaxChannelBindingLen {
|
if len(binding) > 0 && len(binding) <= MaxChannelBindingLen {
|
||||||
|
|||||||
+33
-3
@@ -198,13 +198,14 @@ func TestScramProofBinding(t *testing.T) {
|
|||||||
// The binding enters both AuthMessages, so peers authenticate only when they
|
// The binding enters both AuthMessages, so peers authenticate only when they
|
||||||
// derive identical bytes; a one-sided binding fails instead of downgrading.
|
// derive identical bytes; a one-sided binding fails instead of downgrading.
|
||||||
func TestScramChannelBinding(t *testing.T) {
|
func TestScramChannelBinding(t *testing.T) {
|
||||||
a, b := bytes.Repeat([]byte{1}, 32), bytes.Repeat([]byte{2}, 32)
|
a, b, max := bytes.Repeat([]byte{1}, 32), bytes.Repeat([]byte{2}, 32), bytes.Repeat([]byte{3}, MaxChannelBindingLen)
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
client, server []ExchangeOption
|
client, server []ExchangeOption
|
||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
{"matching", []ExchangeOption{WithChannelBinding(a)}, []ExchangeOption{WithChannelBinding(a)}, true},
|
{"matching", []ExchangeOption{WithChannelBinding(a)}, []ExchangeOption{WithChannelBinding(a)}, true},
|
||||||
|
{"matching at the 64-byte limit", []ExchangeOption{WithChannelBinding(max)}, []ExchangeOption{WithChannelBinding(max)}, true},
|
||||||
{"mismatched", []ExchangeOption{WithChannelBinding(a)}, []ExchangeOption{WithChannelBinding(b)}, false},
|
{"mismatched", []ExchangeOption{WithChannelBinding(a)}, []ExchangeOption{WithChannelBinding(b)}, false},
|
||||||
{"client only", []ExchangeOption{WithChannelBinding(a)}, nil, false},
|
{"client only", []ExchangeOption{WithChannelBinding(a)}, nil, false},
|
||||||
{"server only", nil, []ExchangeOption{WithChannelBinding(a)}, false},
|
{"server only", nil, []ExchangeOption{WithChannelBinding(a)}, false},
|
||||||
@@ -228,8 +229,35 @@ func TestScramChannelBinding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// An empty or oversized binding is a caller bug: both sides refuse it before
|
// Known-answer proofs pin the AuthMessage wire format: the unbound value is the
|
||||||
// touching exchange state rather than treating it as "unbound".
|
// one v0.4.1 produced, so older peers keep interoperating, and the bound form
|
||||||
|
// cannot drift while both sides change in step.
|
||||||
|
func TestScramProofKnownAnswers(t *testing.T) {
|
||||||
|
msg := ServerFirstMessage{
|
||||||
|
FullNonce: "client-nonce" + "server-nonce", Salt: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{7}, 16)),
|
||||||
|
ArgonTime: 1, ArgonMemory: 8, ArgonThreads: 1,
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
opts []ExchangeOption
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"unbound", nil, "JCVOdKAYwVu3YZx+6nfRfBUcnsTCgydOchnQq1G7Ry0="},
|
||||||
|
{"bound", []ExchangeOption{WithChannelBinding(bytes.Repeat([]byte{9}, 32))}, "uhfWaHDQ4yUIcY3qWbaFhBBXWO+HtBlujy7z9u92a20="},
|
||||||
|
} {
|
||||||
|
c := NewScramClient("alice", "password123", WithMinArgonCost(1, 8))
|
||||||
|
_, err := c.StartAuthentication()
|
||||||
|
noErr(t, err, tc.name+": start")
|
||||||
|
c.clientNonce = "client-nonce"
|
||||||
|
proof, err := c.ProcessServerFirstMessage(msg, tc.opts...)
|
||||||
|
noErr(t, err, tc.name)
|
||||||
|
eq(t, proof.ClientProof, tc.want, tc.name+": proof")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty or oversized binding is a caller bug, refused rather than treated as
|
||||||
|
// "unbound". The server keeps its handshake; the client is consumed, as on any
|
||||||
|
// client error.
|
||||||
func TestScramChannelBindingLength(t *testing.T) {
|
func TestScramChannelBindingLength(t *testing.T) {
|
||||||
s, user, pw, _ := setupScram(t)
|
s, user, pw, _ := setupScram(t)
|
||||||
for _, cb := range [][]byte{nil, {}, make([]byte, MaxChannelBindingLen+1)} {
|
for _, cb := range [][]byte{nil, {}, make([]byte, MaxChannelBindingLen+1)} {
|
||||||
@@ -242,6 +270,8 @@ func TestScramChannelBindingLength(t *testing.T) {
|
|||||||
errIs(t, err, ErrSCRAMChannelBinding, "server binding length")
|
errIs(t, err, ErrSCRAMChannelBinding, "server binding length")
|
||||||
_, err = c.ProcessServerFirstMessage(challenge, WithChannelBinding(cb))
|
_, err = c.ProcessServerFirstMessage(challenge, WithChannelBinding(cb))
|
||||||
errIs(t, err, ErrSCRAMChannelBinding, "client binding length")
|
errIs(t, err, ErrSCRAMChannelBinding, "client binding length")
|
||||||
|
_, err = c.ProcessServerFirstMessage(challenge, WithChannelBinding([]byte{1}))
|
||||||
|
errIs(t, err, ErrSCRAMInvalidState, "client state survived a binding error")
|
||||||
}
|
}
|
||||||
eq(t, handshakeCount(s), 3, "server consumed a handshake on a binding error")
|
eq(t, handshakeCount(s), 3, "server consumed a handshake on a binding error")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user