// Copyright 2016 The Mellium Contributors. // Use of this source code is governed by the BSD 2-clause license that can be // found in the LICENSE file. package sasl import ( "crypto/sha1" "crypto/sha256" "crypto/tls" "strconv" "testing" ) // saslStep is from the perspective of a client, challenge is issued by the // server and resp is the clients response (the first challenge will generally // be empty because SASL is a client-first protocol). type saslStep struct { challenge []byte resp []byte more bool clientErr bool serverErr bool } type saslTest struct { mechanism Mechanism clientOpts []Option serverOpts []Option perm func(*Negotiator) bool steps []saslStep skipClient bool skipServer bool } func getStepName(n *Negotiator) string { switch n.State() & StepMask { case Initial: return "Initial" case AuthTextSent: return "AuthTextSent" case ResponseSent: return "ResponseSent" case ValidServerResponse: return "ValidServerResponse" default: panic("Step part of state byte apparently has too many bits") } } var ( plainResp = []byte("Ursel\x00Kurt\x00xipj3plmq") testNonce = []byte("fyko+d2lbbFgONRv9qkxdawL") ) func acceptAll(_ *Negotiator) bool { return true } var saslTestCases = [...]saslTest{ 0: { skipServer: true, mechanism: plain, clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("Kurt"), []byte("xipj3plmq"), []byte("Ursel") })}, steps: []saslStep{ {resp: plainResp, more: false}, {challenge: nil, resp: nil, clientErr: true, more: false}, }, }, 1: { skipServer: true, mechanism: scram("SCRAM-SHA-1", sha1.New), clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("user"), []byte("pencil"), []byte{} })}, steps: []saslStep{ { resp: []byte(`n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL`), more: true, }, { challenge: []byte(`r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096`), resp: []byte(`c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=`), more: true, }, { challenge: []byte(`v=rmF9pqV8S7suAoZWja4dJRkFsKQ=`), resp: nil, more: false, }, }, }, 2: { skipServer: true, // Mechanism is not SCRAM-SHA-1-PLUS, but has connstate and remote mechanisms. mechanism: scram("SCRAM-SHA-1", sha1.New), clientOpts: []Option{ Credentials(func() ([]byte, []byte, []byte) { return []byte("user"), []byte("pencil"), []byte{} }), RemoteMechanisms("SCRAM-SHA-1-PLUS", "SCRAM-SHA-1"), TLSState(tls.ConnectionState{TLSUnique: []byte{0, 1, 2, 3, 4}}), }, steps: []saslStep{ { resp: []byte(`n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL`), more: true, }, { challenge: []byte(`r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096`), resp: []byte(`c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=`), more: true, }, { challenge: []byte(`v=rmF9pqV8S7suAoZWja4dJRkFsKQ=`), resp: nil, more: false, }, }, }, 3: { skipServer: true, mechanism: scram("SCRAM-SHA-1-PLUS", sha1.New), clientOpts: []Option{ Credentials(func() ([]byte, []byte, []byte) { return []byte("user"), []byte("pencil"), []byte{} }), RemoteMechanisms("SCRAM-SHA-1-PLUS"), TLSState(tls.ConnectionState{TLSUnique: []byte{0, 1, 2, 3, 4}}), }, steps: []saslStep{ { resp: []byte(`p=tls-unique,,n=user,r=fyko+d2lbbFgONRv9qkxdawL`), more: true, }, { challenge: []byte(`r=fyko+d2lbbFgONRv9qkxdawL16090868851744577,s=QSXCR+Q6sek8bf92,i=4096`), resp: []byte(`c=cD10bHMtdW5pcXVlLCwAAQIDBA==,r=fyko+d2lbbFgONRv9qkxdawL16090868851744577,p=kD6Wfe1kGICYN08YH7oONG2Enb0=`), more: true, }, { challenge: []byte(`v=QI0Ihj/QJv+VSyezLtd/d5PrYy0=`), resp: nil, more: false, }, }, }, 4: { skipServer: true, mechanism: scram("SCRAM-SHA-256", sha256.New), clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("user"), []byte("pencil"), []byte{} })}, steps: []saslStep{ { resp: []byte("n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"), more: true, }, { challenge: []byte(`r=fyko+d2lbbFgONRv9qkxdawL%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096`), resp: []byte(`c=biws,r=fyko+d2lbbFgONRv9qkxdawL%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=2FUSN0pPcS7P8hBhsxBJOiUDbRoW4KVNGZT0LxVnSek=`), more: true, }, { challenge: []byte(`v=zJZjsVp2g+W9jd01vgbsshippfH1sM0tLdBvs+e3DF4=`), resp: nil, more: false, }, }, }, 5: { skipServer: true, mechanism: scram("SCRAM-SHA-256-PLUS", sha256.New), clientOpts: []Option{ Credentials(func() ([]byte, []byte, []byte) { return []byte("user"), []byte("pencil"), []byte("admin") }), RemoteMechanisms("SCRAM-SOMETHING", "SCRAM-SHA-256-PLUS"), TLSState(tls.ConnectionState{TLSUnique: []byte{0, 1, 2, 3, 4}}), }, steps: []saslStep{ { resp: []byte("p=tls-unique,a=admin,n=user,r=fyko+d2lbbFgONRv9qkxdawL"), more: true, }, { challenge: []byte(`r=fyko+d2lbbFgONRv9qkxdawL,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096`), resp: []byte(`c=cD10bHMtdW5pcXVlLGE9YWRtaW4sAAECAwQ=,r=fyko+d2lbbFgONRv9qkxdawL,p=USNVS9hYD1JWfBOQwzc8o/9vFPQ7kA4CKsocmko/8yU=`), more: true, }, { challenge: []byte(`v=zjC1aKz20rqp7P92qtiJD1+gihbP5dKzIUFlBWgOuss=`), resp: nil, more: false, }, }, }, 6: { skipServer: true, mechanism: scram("SCRAM-SHA-1-PLUS", sha1.New), clientOpts: []Option{ Credentials(func() ([]byte, []byte, []byte) { return []byte(",=,="), []byte("password"), []byte{} }), RemoteMechanisms("SCRAM-SHA-1-PLUS"), TLSState(tls.ConnectionState{TLSUnique: []byte("finishedmessage")}), }, steps: []saslStep{ { resp: []byte("p=tls-unique,,n==2C=3D=2C=3D,r=fyko+d2lbbFgONRv9qkxdawL"), more: true, }, { challenge: []byte(`r=fyko+d2lbbFgONRv9qkxdawLtheirnonce,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096`), resp: []byte(`c=cD10bHMtdW5pcXVlLCxmaW5pc2hlZG1lc3NhZ2U=,r=fyko+d2lbbFgONRv9qkxdawLtheirnonce,p=8t6BJnSAd7Vi+mGZEi+Oqwci11c=`), more: true, }, { challenge: []byte(`v=8IDvl31piL1lkn6XLCqqFVS4EJM=`), resp: nil, more: false, }, }, }, 7: { skipClient: true, mechanism: plain, perm: acceptAll, steps: []saslStep{ {resp: []byte("\x00Ursel\x00Kurt\x00xipj3plmq"), serverErr: true, more: false}, }, }, 8: { mechanism: plain, perm: func(n *Negotiator) bool { user, pass, ident := n.Credentials() switch { case string(user) != "Kurt": return false case string(pass) != "xipj3plmq": return false case string(ident) != "Ursel": return false } return true }, clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("Kurt"), []byte("xipj3plmq"), []byte("Ursel") })}, steps: []saslStep{ {resp: plainResp, more: false}, }, }, 9: { mechanism: plain, perm: func(n *Negotiator) bool { user, _, _ := n.Credentials() return string(user) == "FAIL" }, clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("Kurt"), []byte("xipj3plmq"), []byte("Ursel") })}, steps: []saslStep{ {resp: plainResp, serverErr: true, more: false}, }, }, 10: { mechanism: plain, perm: func(n *Negotiator) bool { _, pass, _ := n.Credentials() return string(pass) == "FAIL" }, clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("Kurt"), []byte("xipj3plmq"), []byte("Ursel") })}, steps: []saslStep{ {resp: plainResp, serverErr: true, more: false}, }, }, 11: { mechanism: plain, perm: func(n *Negotiator) bool { _, _, ident := n.Credentials() return string(ident) == "FAIL" }, clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("Kurt"), []byte("xipj3plmq"), []byte("Ursel") })}, steps: []saslStep{ {resp: plainResp, serverErr: true, more: false}, }, }, 12: { mechanism: plain, clientOpts: []Option{Credentials(func() ([]byte, []byte, []byte) { return []byte("Kurt"), []byte("xipj3plmq"), []byte("Ursel") })}, steps: []saslStep{ {resp: plainResp, serverErr: true, more: false}, }, }, 13: { skipClient: true, mechanism: scram("", nil), perm: acceptAll, steps: []saslStep{ {serverErr: true, more: false}, }, }, 14: { skipClient: true, mechanism: scram("", nil), perm: acceptAll, steps: []saslStep{ {resp: []byte{}, challenge: nil, serverErr: true, more: false}, }, }, 15: { skipClient: true, mechanism: plain, perm: acceptAll, steps: []saslStep{ {resp: []byte("Ursel\x00Kurt\x00xipj3plmq\x00"), serverErr: true, more: false}, }, }, } func testClient(t *testing.T, client *Negotiator, tc saslTest, run int) { t.Run("Client", func(t *testing.T) { for _, step := range tc.steps { more, resp, err := client.Step(step.challenge) switch { case err != nil && client.State()&Errored != Errored: t.Logf("Run %d, Step %s", run, getStepName(client)) t.Fatalf("State machine internal error state was not set, got error: %v", err) case err == nil && client.State()&Errored == Errored: t.Logf("Run %d, Step %s", run, getStepName(client)) t.Fatal("State machine internal error state was set, but no error was returned") case err == nil && step.clientErr: // There was no error, but we expect one t.Logf("Run %d, Step %s", run, getStepName(client)) t.Fatal("Expected SASL step to error") case err != nil && !step.clientErr: // There was an error, but we didn't expect one t.Logf("Run %d, Step %s", run, getStepName(client)) t.Fatalf("Got unexpected SASL error: %v", err) case string(step.resp) != string(resp): t.Logf("Run %d, Step %s", run, getStepName(client)) t.Fatalf("Got invalid response text:\nexpected `%s'\n got `%s'", step.resp, resp) case more != step.more: t.Logf("Run %d, Step %s", run, getStepName(client)) t.Fatalf("Got unexpected value for more: %v", more) } } }) } func testServer(t *testing.T, server *Negotiator, tc saslTest, run int) { t.Run("Server", func(t *testing.T) { for _, step := range tc.steps { more, challenge, err := server.Step(step.resp) switch { case err != nil && server.State()&Errored != Errored: t.Logf("Run %d, Step %s", run, getStepName(server)) t.Fatalf("State machine internal error state was not set, got error: %v", err) case err == nil && server.State()&Errored == Errored: t.Logf("Run %d, Step %s", run, getStepName(server)) t.Fatal("State machine internal error state was set, but no error was returned") case err == nil && step.serverErr: // There was no error, but we expect one t.Logf("Run %d, Step %s", run, getStepName(server)) t.Fatal("Expected SASL step to error") case err != nil && !step.serverErr: // There was an error, but we didn't expect one t.Logf("Run %d, Step %s", run, getStepName(server)) t.Fatalf("Got unexpected SASL error: %v", err) case string(step.challenge) != string(challenge): t.Logf("Run %d, Step %s", run, getStepName(server)) t.Fatalf("Got invalid challenge text:\nexpected `%s'\n got `%s'", step.challenge, challenge) case more != step.more: t.Logf("Run %d, Step %s", run, getStepName(server)) t.Fatalf("Got unexpected value for more: %v", more) } } }) } func TestSASL(t *testing.T) { for i, tc := range saslTestCases { t.Run(strconv.Itoa(i), func(t *testing.T) { client := NewClient(tc.mechanism, tc.clientOpts...) server := NewServer(tc.mechanism, tc.perm, tc.serverOpts...) // Run each test twice to make sure that Reset actually sets the state back // to the initial state. for run := 1; run < 3; run++ { // Reset the nonce to the one used by all of our test vectors. // TODO: this is an internal state detail. Instead of mutating it, make // an option to set the RNG and pass in a dummy one. client.nonce = testNonce server.nonce = testNonce if !tc.skipClient { testClient(t, client, tc, run) } if !tc.skipServer { testServer(t, server, tc, run) } client.Reset() server.Reset() } }) } }