Merge lp:~chipaca/snappy/auth into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by John Lenton
Status: Rejected
Rejected by: John Lenton
Proposed branch: lp:~chipaca/snappy/auth
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Prerequisite: lp:~chipaca/snappy/merged-list
Diff against target: 962 lines (+784/-41)
11 files modified
daemon/api.go (+24/-17)
daemon/basicauth_1.3.go (+54/-0)
daemon/basicauth_1.4.go (+30/-0)
daemon/crypt/bcrypt.go (+38/-0)
daemon/crypt/crypt.go (+120/-0)
daemon/crypt/crypt_test.go (+248/-0)
daemon/crypt/scrypt.go (+78/-0)
daemon/crypt/sha512crypt.go (+120/-0)
daemon/daemon.go (+23/-1)
daemon/daemon_test.go (+48/-23)
daemon/response.go (+1/-0)
To merge this branch: bzr merge lp:~chipaca/snappy/auth
Reviewer Review Type Date Requested Status
Tyler Hicks (community) Needs Fixing
Sergio Schvezov Approve
Review via email: mp+273684@code.launchpad.net

This proposal supersedes a proposal from 2015-10-07.

Commit message

Require authentication for non-guest methods.

Description of the change

Only GET can be used by non-authenticated, and then only for commands flagged with GuestOK. This means that we need some way to bootsrap, which'll come in a followup branch. We also want to change the credentials via the same REST API, which is yet another followup.

I'll repeat it in bigger letters in case you missed it: THIS WILL LOCK YOU OUT OF THE REST API.

This'll probably be breaking integration tests; need to check with the guys.

To post a comment you must log in.
Revision history for this message
Sergio Schvezov (sergiusens) wrote :

This code looks really neat and tidy, kudos!

I added a few irrelevant comments here and there (tl;rl; they are all irrelevant).

The only thing that I might consider trying is forcing garbage collection and getting rid of the 'password' from memory as soon as we have a hash, unless I missed that and is already taken care of.

Revision history for this message
John Lenton (chipaca) wrote :

re gc: probably not possible without major work, as the password will be clear(ish) in the http request headers.

If that's a concern, we should switch to digest auth.

Revision history for this message
Sergio Schvezov (sergiusens) :
review: Approve
lp:~chipaca/snappy/auth updated
752. By John Lenton

annotated 1.3's basicAuth with return variable names

Revision history for this message
Tyler Hicks (tyhicks) wrote :

I have a few inline comments and then some higher level questions below.

1) Why implement scrypt, bcrypt, and sha512crypt all right out of the gate? The design should definitely incorporate algorithm agility so that we can move from scrypt to something else down the road if scrypt is found to be flawed. However, I found it odd that we're bringing the complexity of scrypt plus two "lesser" algorithms in from the start.
 - I've already reviewed the bcrypt implementation and it looks good
 - I still need to review the scrypt implementation
 - I'm ignoring sha512crypt until we've determined that it is needed.

2) This change creates a single username/password combo, which is different than the username/password combo for PAM logins, to be used for all sensitive requests made to snapd. How will that work in the future in a multi-user scenario? All users of the system will need to share the same snapd login information? Would it make sense for snapd to talk to PAM and authenticate these requests in that way?

Note that I haven't yet had a chance to review the tests.

Revision history for this message
Tyler Hicks (tyhicks) wrote :

The username handling doesn't look correct to me. AuthOK() calls crypt.Check([]byte(user + pass)) but crypt.Check() just treats the entire thing as the password.

review: Needs Fixing
Revision history for this message
Tyler Hicks (tyhicks) wrote :

Have you benchmarked scrypt on the BBB or Raspberry Pi 2? I'm curious if there are significant delays when checking the password. I think scrypt is generally the best choice from a security standpoint but it may take considerable time to authenticate these REST API calls.

Revision history for this message
John Lenton (chipaca) wrote :

Wrt colons in username, “a user-id containing a colon character is invalid”, says the rfc about basic auth. Also I don't really care, because

Wrt password vs user+pass, i concatenate user+password from basic auth to create the "password" that crypt needs. As there aren't users, this seemed ok (the alternative being to ask for a username and discard it).

The multiple choice of algorithms are precisely because I benchmarked them on the bbb. sha512crypt is the only one that gives sub-second performance; scrypt takes about 30 seconds.

Revision history for this message
John Lenton (chipaca) wrote :

http://pastebin.ubuntu.com/12697607/
Note it says N but it's actually log2(N). That is, 14 means an N of 16k.

That run was stapping at over 5s; I did a run with N of 1<<14 to check, and it was 23 seconds. Not 30, sorry -- even on that i7 14 is rather slow for a REST API (and on my slightly older laptop i7 it's about 2× that).

Revision history for this message
Tyler Hicks (tyhicks) wrote :

> The multiple choice of algorithms are precisely because I benchmarked them on
> the bbb. sha512crypt is the only one that gives sub-second performance; scrypt
> takes about 30 seconds.

Then is scrypt even an option? This MP has 'var DefaultAlg = SCRYPT' and crypt.Write() unconditionally uses SCRYPT.

Revision history for this message
John Lenton (chipaca) wrote :

>
> Then is scrypt even an option? This MP has 'var DefaultAlg = SCRYPT' and
> crypt.Write() unconditionally uses SCRYPT.

in a later branch I intend to let an oem snap override the default (so on arm you'd tweak it), and it's an scrypt with N=1<<10 so it's ok on even not-so-zippy intels.

Revision history for this message
Tyler Hicks (tyhicks) wrote :

> Wrt colons in username, “a user-id containing a colon character is invalid”,
> says the rfc about basic auth. Also I don't really care, because
>
> Wrt password vs user+pass, i concatenate user+password from basic auth to
> create the "password" that crypt needs. As there aren't users, this seemed ok
> (the alternative being to ask for a username and discard it).

Ok, so that again locks us into a single username and password combo for authenticating to snapd. Maybe that's fine but it would be good for one of the Snappy architects to comment here regarding whether or not multi-user support will be important in the near to medium term for snapd and Snappy, in general.

Revision history for this message
Tyler Hicks (tyhicks) wrote :

Can you add a comment to the code where the socket is created to make it clear that if this API is ever exposed over the network, HTTPS *must* used?

review: Needs Fixing
Revision history for this message
Tyler Hicks (tyhicks) wrote :

I need to start my weekend but I still need to finish looking at the scrypt implementation and research the N, R, and P values chosen. In the meantime, can you reach out to the architect team and get them to comment on how important multi-user support is?

Revision history for this message
Sergio Schvezov (sergiusens) wrote :
Revision history for this message
John Lenton (chipaca) wrote :

We're doing the other thing, switching to named unix sockets. Thank you and sorry to waste your time, Tyler.

Revision history for this message
Tyler Hicks (tyhicks) wrote :

It wasn't a waste of time, at all! I'm glad we got the right design in place for local auth and this will allow me to start thinking about what will be needed for remote auth down the line. Thanks again, John!

Unmerged revisions

752. By John Lenton

annotated 1.3's basicAuth with return variable names

751. By John Lenton

fix for 1.3 not having basicAuth methods on http.Request. Also caught a nasty bug wrt null-termination

750. By John Lenton

restrict REST API calls w/auth

749. By John Lenton

Merged merged-list into auth.

748. By John Lenton

Merged merged-list into auth.

747. By John Lenton

Merged merged-list into auth.

746. By John Lenton

Merged merged-list into auth.

745. By John Lenton

first pass at auth

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'daemon/api.go'
--- daemon/api.go 2015-10-08 12:50:11 +0000
+++ daemon/api.go 2015-10-09 13:33:12 +0000
@@ -57,35 +57,41 @@
5757
58var (58var (
59 rootCmd = &Command{59 rootCmd = &Command{
60 Path: "/",60 GuestOK: true,
61 GET: SyncResponse([]string{"/1.0"}).Self,61 Path: "/",
62 GET: SyncResponse([]string{"/1.0"}).Self,
62 }63 }
6364
64 v1Cmd = &Command{65 v1Cmd = &Command{
65 Path: "/1.0",66 GuestOK: true,
66 GET: v1Get,67 Path: "/1.0",
68 GET: v1Get,
67 }69 }
6870
69 metaIconCmd = &Command{71 metaIconCmd = &Command{
70 Path: "/1.0/icons/{icon}",72 GuestOK: true,
71 GET: metaIconGet,73 Path: "/1.0/icons/{icon}",
74 GET: metaIconGet,
72 }75 }
7376
74 appIconCmd = &Command{77 appIconCmd = &Command{
75 Path: "/1.0/icons/{name}.{origin}/icon",78 GuestOK: true,
76 GET: appIconGet,79 Path: "/1.0/icons/{name}.{origin}/icon",
80 GET: appIconGet,
77 }81 }
7882
79 packagesCmd = &Command{83 packagesCmd = &Command{
80 Path: "/1.0/packages",84 GuestOK: true,
81 GET: getPackagesInfo,85 Path: "/1.0/packages",
82 POST: sideloadPackage,86 GET: getPackagesInfo,
87 POST: sideloadPackage,
83 }88 }
8489
85 packageCmd = &Command{90 packageCmd = &Command{
86 Path: "/1.0/packages/{name}.{origin}",91 GuestOK: true,
87 GET: getPackageInfo,92 Path: "/1.0/packages/{name}.{origin}",
88 POST: postPackage,93 GET: getPackageInfo,
94 POST: postPackage,
89 }95 }
9096
91 packageConfigCmd = &Command{97 packageConfigCmd = &Command{
@@ -95,9 +101,10 @@
95 }101 }
96102
97 packageSvcsCmd = &Command{103 packageSvcsCmd = &Command{
98 Path: "/1.0/packages/{name}.{origin}/services",104 GuestOK: true,
99 GET: packageService,105 Path: "/1.0/packages/{name}.{origin}/services",
100 PUT: packageService,106 GET: packageService,
107 PUT: packageService,
101 }108 }
102109
103 packageSvcCmd = &Command{110 packageSvcCmd = &Command{
104111
=== added file 'daemon/basicauth_1.3.go'
--- daemon/basicauth_1.3.go 1970-01-01 00:00:00 +0000
+++ daemon/basicauth_1.3.go 2015-10-09 13:33:12 +0000
@@ -0,0 +1,54 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2// +build !go1.4
3
4/*
5 * Copyright (C) 2014-2015 Canonical Ltd
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License version 3 as
9 * published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 */
20
21package daemon
22
23import (
24 "bytes"
25 "encoding/base64"
26 "net/http"
27 "strings"
28)
29
30const authScheme = "Basic " // extra space because cheating
31
32func basicAuth(req *http.Request) (username string, password string, ok bool) {
33 auth := req.Header.Get("Authorization")
34 if !strings.HasPrefix(auth, authScheme) {
35 return "", "", false
36 }
37
38 up, err := base64.StdEncoding.DecodeString(auth[len(authScheme):])
39 if err != nil {
40 return "", "", false
41 }
42
43 idx := bytes.IndexByte(up, ':')
44 if idx < 0 {
45 return "", "", false
46 }
47
48 return string(up[:idx]), string(up[idx+1:]), true
49}
50
51func setBasicAuth(req *http.Request, username, password string) {
52 up := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
53 req.Header.Set("Authorization", authScheme+up)
54}
055
=== added file 'daemon/basicauth_1.4.go'
--- daemon/basicauth_1.4.go 1970-01-01 00:00:00 +0000
+++ daemon/basicauth_1.4.go 2015-10-09 13:33:12 +0000
@@ -0,0 +1,30 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2// +build go1.4
3
4/*
5 * Copyright (C) 2014-2015 Canonical Ltd
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License version 3 as
9 * published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 */
20
21package daemon
22
23import (
24 "net/http"
25)
26
27var (
28 basicAuth = (*http.Request).BasicAuth
29 setBasicAuth = (*http.Request).SetBasicAuth
30)
031
=== added directory 'daemon/crypt'
=== added file 'daemon/crypt/bcrypt.go'
--- daemon/crypt/bcrypt.go 1970-01-01 00:00:00 +0000
+++ daemon/crypt/bcrypt.go 2015-10-09 13:33:12 +0000
@@ -0,0 +1,38 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2014-2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package crypt
21
22import (
23 "golang.org/x/crypto/bcrypt"
24)
25
26var bcryptAlg = cryptAlg{
27 gen: func(password []byte) ([]byte, error) {
28 hash, err := bcrypt.GenerateFromPassword(password, -1)
29 if err != nil {
30 return nil, err
31 }
32
33 return hash, nil
34 },
35 cmp: func(hashedPassword, password []byte) error {
36 return bcrypt.CompareHashAndPassword(hashedPassword, password)
37 },
38}
039
=== added file 'daemon/crypt/crypt.go'
--- daemon/crypt/crypt.go 1970-01-01 00:00:00 +0000
+++ daemon/crypt/crypt.go 2015-10-09 13:33:12 +0000
@@ -0,0 +1,120 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2014-2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package crypt
21
22import (
23 "bytes"
24 "errors"
25 "io"
26 "os"
27 "path/filepath"
28
29 "launchpad.net/snappy/dirs"
30 "launchpad.net/snappy/helpers"
31)
32
33type cryptAlg struct {
34 gen func(password []byte) ([]byte, error)
35 cmp func(hashed, password []byte) error
36}
37
38// the known crypt algorithms
39//
40// xxx is algorithm the right name even
41const (
42 SHA512 = "sha512"
43 SCRYPT = "scrypt"
44 BCRYPT = "bcrypt"
45)
46
47// the default algorithm
48var DefaultAlg = SCRYPT
49
50var cryptAlgs = map[string]cryptAlg{
51 SCRYPT: scryptAlg,
52 BCRYPT: bcryptAlg,
53 SHA512: sha512cryptAlg,
54}
55
56var (
57 // ErrBadCreds is a generic "bad password" error
58 ErrBadCreds = errors.New("bad credentials")
59)
60
61const (
62 maxSecretLen = 1024 // upper bound of the length of a secret
63 maxPrefixLen = 8 // upper bound of the prefix length (e.g. "scrypt:")
64
65 minpwlen = 8
66)
67
68func pwFile() string {
69 return filepath.Join(dirs.GlobalRootDir, dirs.SnappyDir, ".snapd_creds")
70}
71
72// Write the given password out to the file, crypting it first
73func Write(password []byte) error {
74 // TODO: cracklib
75 if len(password) < minpwlen {
76 return ErrBadCreds
77 }
78
79 hash, err := cryptAlgs[DefaultAlg].gen(password)
80 if err != nil {
81 return err
82 }
83
84 buf := append([]byte(DefaultAlg+":"), hash...)
85 if err := helpers.AtomicWriteFile(pwFile(), buf, 0600); err != nil {
86 return err
87 }
88
89 return nil
90}
91
92// Check whether this password and the one on file match.
93func Check(password []byte) bool {
94 f, err := os.Open(pwFile())
95 if err != nil {
96 return false
97 }
98
99 buf := make([]byte, maxPrefixLen+maxSecretLen)
100 n, err := f.ReadAt(buf, 0)
101 if err != io.EOF {
102 return false
103 }
104
105 if n < maxPrefixLen {
106 return false
107 }
108
109 idx := bytes.IndexByte(buf[:maxPrefixLen], ':')
110 if idx < 1 {
111 return false
112 }
113
114 alg, ok := cryptAlgs[string(buf[:idx])]
115 if !ok {
116 return false
117 }
118
119 return alg.cmp(buf[idx+1:n], password) == nil
120}
0121
=== added file 'daemon/crypt/crypt_test.go'
--- daemon/crypt/crypt_test.go 1970-01-01 00:00:00 +0000
+++ daemon/crypt/crypt_test.go 2015-10-09 13:33:12 +0000
@@ -0,0 +1,248 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2014-2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package crypt
21
22import (
23 "bytes"
24 "crypto/rand"
25 "errors"
26 "io"
27 "os"
28 "path/filepath"
29 "testing"
30
31 "gopkg.in/check.v1"
32
33 "launchpad.net/snappy/dirs"
34)
35
36// Hook up check.v1 into the "go test" runner
37func Test(t *testing.T) { check.TestingT(t) }
38
39type cryptSuite struct {
40 d string
41 oldReader io.Reader
42 oldN int
43 oldR int
44 oldP int
45}
46
47var _ = check.Suite(&cryptSuite{})
48
49var errBadStuff = errors.New("fail")
50
51type totallyRandom struct {
52 err error
53}
54
55func (t *totallyRandom) Read(p []byte) (int, error) {
56 if t.err != nil {
57 return 0, t.err
58 }
59 if len(p) < 1 {
60 return 0, nil
61 }
62 p[0] = 9 // http://dilbert.com/strip/2001-10-25 -- wait, 2001? Ow.
63 return 1, nil
64}
65
66func (s *cryptSuite) SetUpTest(c *check.C) {
67 s.d = c.MkDir()
68 dirs.SetRootDir(s.d)
69 c.Check(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, dirs.SnappyDir), 0755), check.IsNil)
70
71 s.oldN = scryptN
72 s.oldR = scryptR
73 s.oldP = scryptP
74 s.oldReader = rand.Reader
75 rand.Reader = &totallyRandom{}
76 scryptN = 2
77 scryptR = 1
78 scryptP = 1
79}
80
81func (s *cryptSuite) TearDownTest(c *check.C) {
82 rand.Reader = s.oldReader
83 scryptN = s.oldN
84 scryptR = s.oldR
85 scryptP = s.oldP
86}
87
88func (s *cryptSuite) TestCryptAlgs(c *check.C) {
89
90 password := []byte("password")
91 // know any interesting password to try? call 0800-CHIPACA
92 for k, v := range cryptAlgs {
93 hash, err := v.gen(password)
94 c.Assert(err, check.IsNil, check.Commentf(k))
95 c.Assert(hash, check.NotNil, check.Commentf(k))
96
97 c.Check(v.cmp(hash, password), check.IsNil, check.Commentf(k))
98 c.Check(v.cmp(hash, []byte("what")), check.NotNil, check.Commentf(k))
99 }
100}
101
102func (s *cryptSuite) TestCryptAlgsFailure(c *check.C) {
103 defer func() {
104 rand.Reader.(*totallyRandom).err = nil
105 }()
106
107 password := []byte("password")
108 for k, v := range cryptAlgs {
109 rand.Reader.(*totallyRandom).err = nil
110
111 goodHash, err := v.gen(password)
112 c.Check(err, check.IsNil, check.Commentf(k))
113 c.Check(goodHash, check.NotNil, check.Commentf(k))
114 c.Check(v.cmp(goodHash, password), check.IsNil, check.Commentf(k))
115
116 rand.Reader.(*totallyRandom).err = errBadStuff
117
118 hash, err := v.gen(password)
119 c.Check(err, check.NotNil, check.Commentf(k))
120 c.Check(hash, check.IsNil, check.Commentf(k))
121 }
122}
123
124func (s *cryptSuite) TestScryptFailures(c *check.C) {
125 pass := []byte("password")
126 c.Check(scryptAlg.cmp([]byte("w"), pass), check.ErrorMatches, "illegal ascii85 data .*")
127
128 hash, err := scryptAlg.gen(pass)
129 c.Assert(err, check.IsNil)
130 c.Check(scryptAlg.cmp(hash, pass), check.IsNil)
131
132 scryptN = 3
133 defer func() {
134 scryptN = 2
135 }()
136
137 c.Check(scryptAlg.cmp(hash, pass), check.ErrorMatches, "scrypt: N .*")
138 _, err = scryptAlg.gen(pass)
139 c.Check(err, check.ErrorMatches, "scrypt: N .*")
140}
141
142func (s *cryptSuite) TestSHA512CryptFailrues(c *check.C) {
143 c.Check(sha512cryptAlg.cmp([]byte("$$$$$$$$$$"), []byte("x")), check.ErrorMatches, "invalid argument")
144}
145
146func (s *cryptSuite) TestRoundtrip(c *check.C) {
147 c.Check(Write([]byte("password")), check.IsNil)
148 c.Check(Check([]byte("password")), check.Equals, true)
149 c.Check(Check([]byte("fishfish")), check.Equals, false)
150}
151
152func (s *cryptSuite) TestRoundtripChangedDefaults(c *check.C) {
153 DefaultAlg = "bcrypt"
154 c.Check(Write([]byte("password")), check.IsNil)
155 DefaultAlg = "scrypt"
156 c.Check(Check([]byte("password")), check.Equals, true)
157 c.Check(Check([]byte("fishfish")), check.Equals, false)
158}
159
160var failAlg = cryptAlg{
161 gen: func(password []byte) ([]byte, error) {
162 return nil, errBadStuff
163 },
164 cmp: func([]byte, []byte) error {
165 return errBadStuff
166 },
167}
168
169func (s *cryptSuite) TestCryptFailures(c *check.C) {
170 alg := cryptAlgs[DefaultAlg]
171 defer func() { cryptAlgs[DefaultAlg] = alg }()
172 pass := []byte("password")
173 dirs.SetRootDir("/does/not/exist")
174
175 // first, try to read it without having written it first:
176 c.Check(Check(pass), check.Equals, false)
177
178 // next, write it
179 c.Check(Write(pass), check.ErrorMatches, ".*no such file or directory.*")
180 // oops, my bad
181 dirs.SetRootDir(s.d)
182 // *now* write it
183 c.Check(Write(pass[:3]), check.ErrorMatches, ".*bad credentials.*")
184 // jk, jk
185 c.Check(Write(pass), check.IsNil)
186 // pass, oh joy, etc
187 c.Check(Check(pass), check.Equals, true)
188 // write it again
189 c.Check(Write(pass), check.IsNil)
190 // yep, still passes
191 c.Check(Check(pass), check.Equals, true)
192
193 // now the algorithm starts to fail. darned algorithms, they
194 // don't make 'em like they used.
195 cryptAlgs[DefaultAlg] = failAlg
196
197 c.Check(Write(pass), check.NotNil)
198 c.Check(Check(pass), check.Equals, false)
199
200 // back to normal
201 cryptAlgs[DefaultAlg] = alg
202 c.Check(Check(pass), check.Equals, true)
203
204 f, err := os.OpenFile(pwFile(), os.O_WRONLY, 0)
205 c.Check(err, check.IsNil)
206
207 // but what's this? filesystem corruption!
208 _, err = f.WriteAt([]byte{0}, 0)
209 c.Check(err, check.IsNil)
210 c.Check(Check(pass), check.Equals, false)
211
212 // mental
213 _, err = f.WriteAt(bytes.Repeat([]byte{'x'}, maxPrefixLen*2), 0)
214 c.Check(err, check.IsNil)
215 c.Check(Check(pass), check.Equals, false)
216
217 // all better
218 c.Check(Write(pass), check.IsNil)
219 c.Check(Check(pass), check.Equals, true)
220
221 // stahp!
222 os.Truncate(pwFile(), maxPrefixLen-1)
223 c.Check(Check(pass), check.Equals, false)
224
225 // no, really!
226 os.Truncate(pwFile(), 0)
227 c.Check(Check(pass), check.Equals, false)
228
229 // ok, enough
230 c.Check(os.Remove(pwFile()), check.IsNil)
231 c.Check(os.Mkdir(pwFile(), 0755), check.IsNil)
232 c.Check(Check(pass), check.Equals, false)
233 c.Check(os.Remove(pwFile()), check.IsNil)
234 c.Check(os.Symlink("does-not-exist", pwFile()), check.IsNil)
235 c.Check(Check(pass), check.Equals, false)
236}
237
238func (s *cryptSuite) TestCryptNil(c *check.C) {
239 alg := DefaultAlg
240 defer func() { DefaultAlg = alg }()
241
242 for k := range cryptAlgs {
243 DefaultAlg = k
244
245 c.Check(Write(nil), check.NotNil, check.Commentf(k))
246 c.Check(Check(nil), check.NotNil, check.Commentf(k))
247 }
248}
0249
=== added file 'daemon/crypt/scrypt.go'
--- daemon/crypt/scrypt.go 1970-01-01 00:00:00 +0000
+++ daemon/crypt/scrypt.go 2015-10-09 13:33:12 +0000
@@ -0,0 +1,78 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2014-2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package crypt
21
22import (
23 "bytes"
24 "crypto/rand"
25 "encoding/ascii85"
26 "io"
27
28 "golang.org/x/crypto/scrypt"
29)
30
31const (
32 scryptSaltLen = 32
33 scryptHashLen = 64
34)
35
36var (
37 scryptN = 1 << 10
38 scryptR = 8
39 scryptP = 16
40)
41
42var scryptAlg = cryptAlg{
43 cmp: func(hashedPassword, password []byte) error {
44 saltedHash := make([]byte, scryptSaltLen+scryptHashLen)
45 _, _, err := ascii85.Decode(saltedHash, hashedPassword, true)
46 if err != nil {
47 return err
48 }
49
50 salt := saltedHash[:scryptSaltLen]
51 hash, err := scrypt.Key([]byte(password), salt, scryptN, scryptR, scryptP, scryptHashLen)
52 if err != nil {
53 return err
54 }
55
56 if !bytes.Equal(hash, saltedHash[scryptSaltLen:]) {
57 return ErrBadCreds
58 }
59
60 return nil
61 },
62 gen: func(password []byte) ([]byte, error) {
63 salt := make([]byte, scryptSaltLen)
64 if _, err := io.ReadFull(rand.Reader, salt); err != nil {
65 return nil, err
66 }
67
68 hash, err := scrypt.Key([]byte(password), salt, scryptN, scryptR, scryptP, scryptHashLen)
69 if err != nil {
70 return nil, err
71 }
72
73 coded := make([]byte, ascii85.MaxEncodedLen(scryptSaltLen+scryptHashLen))
74 n := ascii85.Encode(coded, append(salt, hash...))
75
76 return coded[:n], nil
77 },
78}
079
=== added file 'daemon/crypt/sha512crypt.go'
--- daemon/crypt/sha512crypt.go 1970-01-01 00:00:00 +0000
+++ daemon/crypt/sha512crypt.go 2015-10-09 13:33:12 +0000
@@ -0,0 +1,120 @@
1// -*- Mode: Go; indent-tabs-mode: t -*-
2
3/*
4 * Copyright (C) 2014-2015 Canonical Ltd
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3 as
8 * published by the Free Software Foundation.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20package crypt
21
22/*
23#cgo CFLAGS: -D_GNU_SOURCE -O3 -Wextra -Werror -pedantic
24#cgo LDFLAGS: -lcrypt
25#include <crypt.h>
26#include <string.h>
27*/
28import "C"
29
30import (
31 "bytes"
32 "crypto/rand"
33 "encoding/binary"
34 "unsafe"
35)
36
37const (
38 sha512saltLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"
39 sha512saltNChars = 16
40)
41
42func mksalt() ([]byte, error) {
43 raw := make([]uint32, 3)
44 if err := binary.Read(rand.Reader, binary.LittleEndian, &raw); err != nil {
45 return nil, err
46 }
47
48 full := make([]byte, sha512saltNChars+4)
49 full[0] = byte('$')
50 full[1] = byte('6')
51 full[2] = byte('$')
52 full[3+sha512saltNChars] = byte('$')
53 salt := full[3 : 3+sha512saltNChars]
54
55 m := uint32(0)
56 for i, n := range raw {
57 for j := 1; j < 6; j++ {
58 salt[i*5+j] = sha512saltLetters[n&63]
59 n >>= 6
60 }
61 m += n << (2 * uint(i))
62 }
63 salt[0] = sha512saltLetters[m]
64
65 return full, nil
66}
67
68func crypt(password, saltOrHashedPassword []byte, checkOnly bool) ([]byte, error) {
69 s := C.struct_crypt_data{}
70 s.initialized = 0
71
72 if password[len(password)-1] != 0 {
73 password = append(password, 0)
74 }
75 if saltOrHashedPassword[len(saltOrHashedPassword)-1] != 0 {
76 saltOrHashedPassword = append(saltOrHashedPassword, 0)
77 }
78
79 p, err := C.crypt_r(
80 (*C.char)(unsafe.Pointer(&password[0])),
81 (*C.char)(unsafe.Pointer(&saltOrHashedPassword[0])),
82 &s,
83 )
84 if err != nil {
85 return nil, err
86 }
87
88 n := C.strnlen(p, maxSecretLen)
89 if n == maxSecretLen {
90 // just in case
91 return nil, ErrBadCreds
92 }
93
94 buf := (*[maxSecretLen]byte)(unsafe.Pointer(p))[:n+1] // null-terminated
95
96 if checkOnly {
97 if bytes.Equal(buf, saltOrHashedPassword) {
98 return nil, nil
99 }
100 return nil, ErrBadCreds
101 }
102
103 return append([]byte(nil), buf...), nil
104}
105
106var sha512cryptAlg = cryptAlg{
107 gen: func(password []byte) ([]byte, error) {
108 salt, err := mksalt()
109 if err != nil {
110 return nil, err
111 }
112
113 return crypt(password, salt, false)
114 },
115 cmp: func(hashedPassword, password []byte) error {
116 _, err := crypt(password, hashedPassword, true)
117
118 return err
119 },
120}
0121
=== modified file 'daemon/daemon.go'
--- daemon/daemon.go 2015-10-05 23:10:59 +0000
+++ daemon/daemon.go 2015-10-09 13:33:12 +0000
@@ -30,6 +30,7 @@
30 "github.com/gorilla/mux"30 "github.com/gorilla/mux"
31 "gopkg.in/tomb.v2"31 "gopkg.in/tomb.v2"
3232
33 "launchpad.net/snappy/daemon/crypt"
33 "launchpad.net/snappy/logger"34 "launchpad.net/snappy/logger"
34)35)
3536
@@ -47,7 +48,9 @@
4748
48// A Command routes a request to an individual per-verb ResponseFUnc49// A Command routes a request to an individual per-verb ResponseFUnc
49type Command struct {50type Command struct {
50 Path string51 // GuestOK: whether GET requests can proceed without auth
52 GuestOK bool
53 Path string
51 //54 //
52 GET ResponseFunc55 GET ResponseFunc
53 PUT ResponseFunc56 PUT ResponseFunc
@@ -57,7 +60,26 @@
57 d *Daemon60 d *Daemon
58}61}
5962
63// AuthOK checks whether the request has enough authorization for the command
64func (c *Command) AuthOK(r *http.Request) bool {
65 if c.GuestOK && r.Method == "GET" {
66 return true
67 }
68
69 user, pass, ok := basicAuth(r)
70 if !ok {
71 return false
72 }
73
74 return crypt.Check([]byte(user + pass))
75}
76
60func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {77func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
78 if !c.AuthOK(r) {
79 Unauthorized.ServeHTTP(w, r)
80 return
81 }
82
61 var rspf ResponseFunc83 var rspf ResponseFunc
62 var rsp Response = BadMethod84 var rsp Response = BadMethod
6385
6486
=== modified file 'daemon/daemon_test.go'
--- daemon/daemon_test.go 2015-10-05 23:10:59 +0000
+++ daemon/daemon_test.go 2015-10-09 13:33:12 +0000
@@ -23,52 +23,77 @@
23 "fmt"23 "fmt"
24 "net/http"24 "net/http"
25 "net/http/httptest"25 "net/http/httptest"
26 "os"
27 "path/filepath"
2628
27 "github.com/gorilla/mux"29 "github.com/gorilla/mux"
28 "gopkg.in/check.v1"30 "gopkg.in/check.v1"
31
32 "launchpad.net/snappy/daemon/crypt"
33 "launchpad.net/snappy/dirs"
29)34)
3035
31type daemonSuite struct{}36type daemonSuite struct{}
3237
33var _ = check.Suite(&daemonSuite{})38var _ = check.Suite(&daemonSuite{})
3439
35type mockHandler struct {40type nullHandler struct{}
36 cmd *Command41
37 lastMethod string42func (nu *nullHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
38}43}
3944func (nu *nullHandler) Self(*Command, *http.Request) Response {
40func (mck *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {45 return nu
41 mck.lastMethod = r.Method46}
42}47
43func (mck *mockHandler) Self(*Command, *http.Request) Response {48func mkRF(c *check.C, cmd *Command, method string) ResponseFunc {
44 return mck
45}
46
47func mkRF(c *check.C, cmd *Command, mck *mockHandler) ResponseFunc {
48 return func(innerCmd *Command, req *http.Request) Response {49 return func(innerCmd *Command, req *http.Request) Response {
49 c.Assert(cmd, check.Equals, innerCmd)50 c.Assert(cmd, check.Equals, innerCmd)
50 return mck51 c.Assert(method, check.Equals, req.Method)
52 return &nullHandler{}
51 }53 }
52}54}
5355
54func (s *daemonSuite) TestCommandMethodDispatch(c *check.C) {56func (s *daemonSuite) TestCommandMethodDispatch(c *check.C) {
57 dirs.SetRootDir(c.MkDir())
58 c.Check(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, dirs.SnappyDir), 0755), check.IsNil)
59
55 cmd := &Command{}60 cmd := &Command{}
56 mck := &mockHandler{cmd: cmd}61 cmd.GET = mkRF(c, cmd, "GET")
57 rf := mkRF(c, cmd, mck)62 cmd.PUT = mkRF(c, cmd, "PUT")
58 cmd.GET = rf63 cmd.POST = mkRF(c, cmd, "POST")
59 cmd.PUT = rf64 cmd.DELETE = mkRF(c, cmd, "DELETE")
60 cmd.POST = rf65
61 cmd.DELETE = rf66 crypt.DefaultAlg = crypt.SHA512
67 c.Check(crypt.Write([]byte("password")), check.IsNil)
6268
63 for _, method := range []string{"GET", "POST", "PUT", "DELETE"} {69 for _, method := range []string{"GET", "POST", "PUT", "DELETE"} {
64 req, err := http.NewRequest(method, "", nil)70 req, err := http.NewRequest(method, "", nil)
65 c.Assert(err, check.IsNil)71 c.Assert(err, check.IsNil, check.Commentf(method))
66 cmd.ServeHTTP(nil, req)72
67 c.Check(mck.lastMethod, check.Equals, method)73 cmd.GuestOK = false
74 rec := httptest.NewRecorder()
75 cmd.ServeHTTP(rec, req)
76 c.Check(rec.Code, check.Equals, http.StatusUnauthorized, check.Commentf(method))
77
78 cmd.GuestOK = true
79 rec = httptest.NewRecorder()
80 cmd.ServeHTTP(rec, req)
81 if method == "GET" {
82 c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf(method))
83 } else {
84 c.Check(rec.Code, check.Equals, http.StatusUnauthorized, check.Commentf(method))
85 }
86 cmd.GuestOK = false
87
88 rec = httptest.NewRecorder()
89 setBasicAuth(req, "pass", "word")
90 cmd.ServeHTTP(rec, req)
91 c.Check(rec.Code, check.Equals, http.StatusOK, check.Commentf(method))
68 }92 }
6993
70 req, err := http.NewRequest("POTATO", "", nil)94 req, err := http.NewRequest("POTATO", "", nil)
71 c.Assert(err, check.IsNil)95 c.Assert(err, check.IsNil)
96 setBasicAuth(req, "pass", "word")
72 rec := httptest.NewRecorder()97 rec := httptest.NewRecorder()
73 cmd.ServeHTTP(rec, req)98 cmd.ServeHTTP(rec, req)
74 c.Check(rec.Code, check.Equals, http.StatusMethodNotAllowed)99 c.Check(rec.Code, check.Equals, http.StatusMethodNotAllowed)
75100
=== modified file 'daemon/response.go'
--- daemon/response.go 2015-10-08 12:52:40 +0000
+++ daemon/response.go 2015-10-09 13:33:12 +0000
@@ -177,4 +177,5 @@
177 BadMethod = ErrorResponse(http.StatusMethodNotAllowed)177 BadMethod = ErrorResponse(http.StatusMethodNotAllowed)
178 InternalError = ErrorResponse(http.StatusInternalServerError)178 InternalError = ErrorResponse(http.StatusInternalServerError)
179 NotImplemented = ErrorResponse(http.StatusNotImplemented)179 NotImplemented = ErrorResponse(http.StatusNotImplemented)
180 Unauthorized = ErrorResponse(http.StatusUnauthorized)
180)181)

Subscribers

People subscribed via source and target branches