├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── TODO ├── bench_test.go ├── crypto.go ├── crypto_test.go ├── dependencies.tsv ├── export_test.go ├── go.mod ├── go.sum ├── macaroon.go ├── macaroon_test.go ├── marshal-v1.go ├── marshal-v2.go ├── marshal.go ├── marshal_test.go ├── packet-v1.go ├── packet-v1_test.go ├── packet-v2.go ├── packet-v2_test.go └── trace.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: "gopkg.in/macaroon.v2" 3 | go: 4 | - "1.7" 5 | - "1.10" 6 | before_install: 7 | - "go get github.com/rogpeppe/godeps" 8 | install: 9 | - "go get -d gopkg.in/macaroon.v2" 10 | - "godeps -u $GOPATH/src/gopkg.in/macaroon.v2/dependencies.tsv" 11 | script: go test ./... 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2014, Roger Peppe 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of this project nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macaroon 2 | -- 3 | import "gopkg.in/macaroon.v2" 4 | 5 | The macaroon package implements macaroons as described in the paper "Macaroons: 6 | Cookies with Contextual Caveats for Decentralized Authorization in the Cloud" 7 | (http://theory.stanford.edu/~ataly/Papers/macaroons.pdf) 8 | 9 | See the macaroon bakery packages at http://godoc.org/gopkg.in/macaroon-bakery.v2 10 | for higher level services and operations that use macaroons. 11 | 12 | ## Usage 13 | 14 | ```go 15 | const ( 16 | TraceInvalid = TraceOpKind(iota) 17 | 18 | // TraceMakeKey represents the operation of calculating a 19 | // fixed length root key from the variable length input key. 20 | TraceMakeKey 21 | 22 | // TraceHash represents a keyed hash operation with one 23 | // or two values. If there is only one value, it will be in Data1. 24 | TraceHash 25 | 26 | // TraceBind represents the operation of binding a discharge macaroon 27 | // to its primary macaroon. Data1 holds the signature of the primary 28 | // macaroon. 29 | TraceBind 30 | 31 | // TraceFail represents a verification failure. If present, this will always 32 | // be the last operation in a trace. 33 | TraceFail 34 | ) 35 | ``` 36 | 37 | #### func Base64Decode 38 | 39 | ```go 40 | func Base64Decode(data []byte) ([]byte, error) 41 | ``` 42 | Base64Decode base64-decodes the given data. It accepts both standard and URL 43 | encodings, both padded and unpadded. 44 | 45 | #### type Caveat 46 | 47 | ```go 48 | type Caveat struct { 49 | // Id holds the id of the caveat. For first 50 | // party caveats this holds the condition; 51 | // for third party caveats this holds the encrypted 52 | // third party caveat. 53 | Id []byte 54 | 55 | // VerificationId holds the verification id. If this is 56 | // non-empty, it's a third party caveat. 57 | VerificationId []byte 58 | 59 | // For third-party caveats, Location holds the 60 | // ocation hint. Note that this is not signature checked 61 | // as part of the caveat, so should only 62 | // be used as a hint. 63 | Location string 64 | } 65 | ``` 66 | 67 | Caveat holds a first person or third party caveat. 68 | 69 | #### type Macaroon 70 | 71 | ```go 72 | type Macaroon struct { 73 | } 74 | ``` 75 | 76 | Macaroon holds a macaroon. See Fig. 7 of 77 | http://theory.stanford.edu/~ataly/Papers/macaroons.pdf for a description of the 78 | data contained within. Macaroons are mutable objects - use Clone as appropriate 79 | to avoid unwanted mutation. 80 | 81 | #### func New 82 | 83 | ```go 84 | func New(rootKey, id []byte, loc string, version Version) (*Macaroon, error) 85 | ``` 86 | New returns a new macaroon with the given root key, identifier, location and 87 | version. 88 | 89 | #### func (*Macaroon) AddFirstPartyCaveat 90 | 91 | ```go 92 | func (m *Macaroon) AddFirstPartyCaveat(condition []byte) error 93 | ``` 94 | AddFirstPartyCaveat adds a caveat that will be verified by the target service. 95 | 96 | #### func (*Macaroon) AddThirdPartyCaveat 97 | 98 | ```go 99 | func (m *Macaroon) AddThirdPartyCaveat(rootKey, caveatId []byte, loc string) error 100 | ``` 101 | AddThirdPartyCaveat adds a third-party caveat to the macaroon, using the given 102 | shared root key, caveat id and location hint. The caveat id should encode the 103 | root key in some way, either by encrypting it with a key known to the third 104 | party or by holding a reference to it stored in the third party's storage. 105 | 106 | #### func (*Macaroon) Bind 107 | 108 | ```go 109 | func (m *Macaroon) Bind(sig []byte) 110 | ``` 111 | Bind prepares the macaroon for being used to discharge the macaroon with the 112 | given signature sig. This must be used before it is used in the discharges 113 | argument to Verify. 114 | 115 | #### func (*Macaroon) Caveats 116 | 117 | ```go 118 | func (m *Macaroon) Caveats() []Caveat 119 | ``` 120 | Caveats returns the macaroon's caveats. This method will probably change, and 121 | it's important not to change the returned caveat. 122 | 123 | #### func (*Macaroon) Clone 124 | 125 | ```go 126 | func (m *Macaroon) Clone() *Macaroon 127 | ``` 128 | Clone returns a copy of the receiving macaroon. 129 | 130 | #### func (*Macaroon) Id 131 | 132 | ```go 133 | func (m *Macaroon) Id() []byte 134 | ``` 135 | Id returns the id of the macaroon. This can hold arbitrary information. 136 | 137 | #### func (*Macaroon) Location 138 | 139 | ```go 140 | func (m *Macaroon) Location() string 141 | ``` 142 | Location returns the macaroon's location hint. This is not verified as part of 143 | the macaroon. 144 | 145 | #### func (*Macaroon) MarshalBinary 146 | 147 | ```go 148 | func (m *Macaroon) MarshalBinary() ([]byte, error) 149 | ``` 150 | MarshalBinary implements encoding.BinaryMarshaler by formatting the macaroon 151 | according to the version specified by MarshalAs. 152 | 153 | #### func (*Macaroon) MarshalJSON 154 | 155 | ```go 156 | func (m *Macaroon) MarshalJSON() ([]byte, error) 157 | ``` 158 | MarshalJSON implements json.Marshaler by marshaling the macaroon in JSON format. 159 | The serialisation format is determined by the macaroon's version. 160 | 161 | #### func (*Macaroon) SetLocation 162 | 163 | ```go 164 | func (m *Macaroon) SetLocation(loc string) 165 | ``` 166 | SetLocation sets the location associated with the macaroon. Note that the 167 | location is not included in the macaroon's hash chain, so this does not change 168 | the signature. 169 | 170 | #### func (*Macaroon) Signature 171 | 172 | ```go 173 | func (m *Macaroon) Signature() []byte 174 | ``` 175 | Signature returns the macaroon's signature. 176 | 177 | #### func (*Macaroon) TraceVerify 178 | 179 | ```go 180 | func (m *Macaroon) TraceVerify(rootKey []byte, discharges []*Macaroon) ([]Trace, error) 181 | ``` 182 | TraceVerify verifies the signature of the macaroon without checking any of the 183 | first party caveats, and returns a slice of Traces holding the operations used 184 | when verifying the macaroons. 185 | 186 | Each element in the returned slice corresponds to the operation for one of the 187 | argument macaroons, with m at index 0, and discharges at 1 onwards. 188 | 189 | #### func (*Macaroon) UnmarshalBinary 190 | 191 | ```go 192 | func (m *Macaroon) UnmarshalBinary(data []byte) error 193 | ``` 194 | UnmarshalBinary implements encoding.BinaryUnmarshaler. It accepts both V1 and V2 195 | binary encodings. 196 | 197 | #### func (*Macaroon) UnmarshalJSON 198 | 199 | ```go 200 | func (m *Macaroon) UnmarshalJSON(data []byte) error 201 | ``` 202 | UnmarshalJSON implements json.Unmarshaller by unmarshaling the given macaroon in 203 | JSON format. It accepts both V1 and V2 forms encoded forms, and also a 204 | base64-encoded JSON string containing the binary-marshaled macaroon. 205 | 206 | After unmarshaling, the macaroon's version will reflect the version that it was 207 | unmarshaled as. 208 | 209 | #### func (*Macaroon) Verify 210 | 211 | ```go 212 | func (m *Macaroon) Verify(rootKey []byte, check func(caveat string) error, discharges []*Macaroon) error 213 | ``` 214 | Verify verifies that the receiving macaroon is valid. The root key must be the 215 | same that the macaroon was originally minted with. The check function is called 216 | to verify each first-party caveat - it should return an error if the condition 217 | is not met. 218 | 219 | The discharge macaroons should be provided in discharges. 220 | 221 | Verify returns nil if the verification succeeds. 222 | 223 | #### func (*Macaroon) VerifySignature 224 | 225 | ```go 226 | func (m *Macaroon) VerifySignature(rootKey []byte, discharges []*Macaroon) ([]string, error) 227 | ``` 228 | VerifySignature verifies the signature of the given macaroon with respect to the 229 | root key, but it does not validate any first-party caveats. Instead it returns 230 | all the applicable first party caveats on success. 231 | 232 | The caller is responsible for checking the returned first party caveat 233 | conditions. 234 | 235 | #### func (*Macaroon) Version 236 | 237 | ```go 238 | func (m *Macaroon) Version() Version 239 | ``` 240 | Version returns the version of the macaroon. 241 | 242 | #### type Slice 243 | 244 | ```go 245 | type Slice []*Macaroon 246 | ``` 247 | 248 | Slice defines a collection of macaroons. By convention, the first macaroon in 249 | the slice is a primary macaroon and the rest are discharges for its third party 250 | caveats. 251 | 252 | #### func (Slice) MarshalBinary 253 | 254 | ```go 255 | func (s Slice) MarshalBinary() ([]byte, error) 256 | ``` 257 | MarshalBinary implements encoding.BinaryMarshaler. 258 | 259 | #### func (*Slice) UnmarshalBinary 260 | 261 | ```go 262 | func (s *Slice) UnmarshalBinary(data []byte) error 263 | ``` 264 | UnmarshalBinary implements encoding.BinaryUnmarshaler. It accepts all known 265 | binary encodings for the data - all the embedded macaroons need not be encoded 266 | in the same format. 267 | 268 | #### type Trace 269 | 270 | ```go 271 | type Trace struct { 272 | RootKey []byte 273 | Ops []TraceOp 274 | } 275 | ``` 276 | 277 | Trace holds all toperations involved in verifying a macaroon, and the root key 278 | used as the initial verification key. This can be useful for debugging macaroon 279 | implementations. 280 | 281 | #### func (Trace) Results 282 | 283 | ```go 284 | func (t Trace) Results() [][]byte 285 | ``` 286 | Results returns the output from all operations in the Trace. The result from 287 | ts.Ops[i] will be in the i'th element of the returned slice. When a trace has 288 | resulted in a failure, the last element will be nil. 289 | 290 | #### type TraceOp 291 | 292 | ```go 293 | type TraceOp struct { 294 | Kind TraceOpKind `json:"kind"` 295 | Data1 []byte `json:"data1,omitempty"` 296 | Data2 []byte `json:"data2,omitempty"` 297 | } 298 | ``` 299 | 300 | TraceOp holds one possible operation when verifying a macaroon. 301 | 302 | #### func (TraceOp) Result 303 | 304 | ```go 305 | func (op TraceOp) Result(input []byte) []byte 306 | ``` 307 | Result returns the result of computing the given operation with the given input 308 | data. If op is TraceFail, it returns nil. 309 | 310 | #### type TraceOpKind 311 | 312 | ```go 313 | type TraceOpKind int 314 | ``` 315 | 316 | TraceOpKind represents the kind of a macaroon verification operation. 317 | 318 | #### func (TraceOpKind) String 319 | 320 | ```go 321 | func (k TraceOpKind) String() string 322 | ``` 323 | String returns a string representation of the operation. 324 | 325 | #### type Version 326 | 327 | ```go 328 | type Version uint16 329 | ``` 330 | 331 | Version specifies the version of a macaroon. In version 1, the macaroon id and 332 | all caveats must be UTF-8-compatible strings, and the size of any part of the 333 | macaroon may not exceed approximately 64K. In version 2, all field may be 334 | arbitrary binary blobs. 335 | 336 | ```go 337 | const ( 338 | // V1 specifies version 1 macaroons. 339 | V1 Version = 1 340 | 341 | // V2 specifies version 2 macaroons. 342 | V2 Version = 2 343 | 344 | // LatestVersion holds the latest supported version. 345 | LatestVersion = V2 346 | ) 347 | ``` 348 | 349 | #### func (Version) String 350 | 351 | ```go 352 | func (v Version) String() string 353 | ``` 354 | String returns a string representation of the version; for example V1 formats as 355 | "v1". 356 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | macaroon: 2 | 3 | - verify that all signature calculations to correspond exactly 4 | with libmacaroons. 5 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package macaroon_test 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "testing" 7 | 8 | "gopkg.in/macaroon.v2" 9 | ) 10 | 11 | func randomBytes(n int) []byte { 12 | b := make([]byte, n) 13 | _, err := rand.Read(b) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return b 18 | } 19 | 20 | func BenchmarkNew(b *testing.B) { 21 | rootKey := randomBytes(24) 22 | id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100))) 23 | loc := base64.StdEncoding.EncodeToString(randomBytes(40)) 24 | b.ResetTimer() 25 | for i := b.N - 1; i >= 0; i-- { 26 | MustNew(rootKey, id, loc, macaroon.LatestVersion) 27 | } 28 | } 29 | 30 | func BenchmarkAddCaveat(b *testing.B) { 31 | rootKey := randomBytes(24) 32 | id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100))) 33 | loc := base64.StdEncoding.EncodeToString(randomBytes(40)) 34 | b.ResetTimer() 35 | for i := b.N - 1; i >= 0; i-- { 36 | b.StopTimer() 37 | m := MustNew(rootKey, id, loc, macaroon.LatestVersion) 38 | b.StartTimer() 39 | m.AddFirstPartyCaveat([]byte("some caveat stuff")) 40 | } 41 | } 42 | 43 | func benchmarkVerify(b *testing.B, mspecs []macaroonSpec) { 44 | rootKey, macaroons := makeMacaroons(mspecs) 45 | check := func(string) error { 46 | return nil 47 | } 48 | b.ResetTimer() 49 | for i := b.N - 1; i >= 0; i-- { 50 | err := macaroons[0].Verify(rootKey, check, macaroons[1:]) 51 | if err != nil { 52 | b.Fatalf("verification failed: %v", err) 53 | } 54 | } 55 | } 56 | 57 | func BenchmarkVerifyLarge(b *testing.B) { 58 | benchmarkVerify(b, multilevelThirdPartyCaveatMacaroons) 59 | } 60 | 61 | func BenchmarkVerifySmall(b *testing.B) { 62 | benchmarkVerify(b, []macaroonSpec{{ 63 | rootKey: "root-key", 64 | id: "root-id", 65 | caveats: []caveat{{ 66 | condition: "wonderful", 67 | }}, 68 | }}) 69 | } 70 | 71 | func BenchmarkMarshalJSON(b *testing.B) { 72 | rootKey := randomBytes(24) 73 | id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100))) 74 | loc := base64.StdEncoding.EncodeToString(randomBytes(40)) 75 | m := MustNew(rootKey, id, loc, macaroon.LatestVersion) 76 | b.ResetTimer() 77 | for i := b.N - 1; i >= 0; i-- { 78 | _, err := m.MarshalJSON() 79 | if err != nil { 80 | b.Fatalf("cannot marshal JSON: %v", err) 81 | } 82 | } 83 | } 84 | 85 | func MustNew(rootKey, id []byte, loc string, vers macaroon.Version) *macaroon.Macaroon { 86 | m, err := macaroon.New(rootKey, id, loc, vers) 87 | if err != nil { 88 | panic(err) 89 | } 90 | return m 91 | } 92 | 93 | func BenchmarkUnmarshalJSON(b *testing.B) { 94 | rootKey := randomBytes(24) 95 | id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100))) 96 | loc := base64.StdEncoding.EncodeToString(randomBytes(40)) 97 | m := MustNew(rootKey, id, loc, macaroon.LatestVersion) 98 | data, err := m.MarshalJSON() 99 | if err != nil { 100 | b.Fatalf("cannot marshal JSON: %v", err) 101 | } 102 | for i := b.N - 1; i >= 0; i-- { 103 | var m macaroon.Macaroon 104 | err := m.UnmarshalJSON(data) 105 | if err != nil { 106 | b.Fatalf("cannot unmarshal JSON: %v", err) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "fmt" 7 | "hash" 8 | "io" 9 | 10 | "golang.org/x/crypto/nacl/secretbox" 11 | ) 12 | 13 | func keyedHash(key *[hashLen]byte, text []byte) *[hashLen]byte { 14 | h := keyedHasher(key) 15 | h.Write([]byte(text)) 16 | var sum [hashLen]byte 17 | hashSum(h, &sum) 18 | return &sum 19 | } 20 | 21 | func keyedHasher(key *[hashLen]byte) hash.Hash { 22 | return hmac.New(sha256.New, key[:]) 23 | } 24 | 25 | var keyGen = []byte("macaroons-key-generator") 26 | 27 | // makeKey derives a fixed length key from a variable 28 | // length key. The keyGen constant is the same 29 | // as that used in libmacaroons. 30 | func makeKey(variableKey []byte) *[keyLen]byte { 31 | h := hmac.New(sha256.New, keyGen) 32 | h.Write(variableKey) 33 | var key [keyLen]byte 34 | hashSum(h, &key) 35 | return &key 36 | } 37 | 38 | // hashSum calls h.Sum to put the sum into 39 | // the given destination. It also sanity 40 | // checks that the result really is the expected 41 | // size. 42 | func hashSum(h hash.Hash, dest *[hashLen]byte) { 43 | r := h.Sum(dest[:0]) 44 | if len(r) != len(dest) { 45 | panic("hash size inconsistency") 46 | } 47 | } 48 | 49 | const ( 50 | keyLen = 32 51 | nonceLen = 24 52 | hashLen = sha256.Size 53 | ) 54 | 55 | func newNonce(r io.Reader) (*[nonceLen]byte, error) { 56 | var nonce [nonceLen]byte 57 | _, err := r.Read(nonce[:]) 58 | if err != nil { 59 | return nil, fmt.Errorf("cannot generate random bytes: %v", err) 60 | } 61 | return &nonce, nil 62 | } 63 | 64 | func encrypt(key *[keyLen]byte, text *[hashLen]byte, r io.Reader) ([]byte, error) { 65 | nonce, err := newNonce(r) 66 | if err != nil { 67 | return nil, err 68 | } 69 | out := make([]byte, 0, len(nonce)+secretbox.Overhead+len(text)) 70 | out = append(out, nonce[:]...) 71 | return secretbox.Seal(out, text[:], nonce, key), nil 72 | } 73 | 74 | func decrypt(key *[keyLen]byte, ciphertext []byte) (*[hashLen]byte, error) { 75 | if len(ciphertext) < nonceLen+secretbox.Overhead { 76 | return nil, fmt.Errorf("message too short") 77 | } 78 | var nonce [nonceLen]byte 79 | copy(nonce[:], ciphertext) 80 | ciphertext = ciphertext[nonceLen:] 81 | text, ok := secretbox.Open(nil, ciphertext, &nonce, key) 82 | if !ok { 83 | return nil, fmt.Errorf("decryption failure") 84 | } 85 | if len(text) != hashLen { 86 | return nil, fmt.Errorf("decrypted text is wrong length") 87 | } 88 | var rtext [hashLen]byte 89 | copy(rtext[:], text) 90 | return &rtext, nil 91 | } 92 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "testing" 7 | 8 | qt "github.com/frankban/quicktest" 9 | "golang.org/x/crypto/nacl/secretbox" 10 | ) 11 | 12 | var testCryptKey = &[hashLen]byte{'k', 'e', 'y'} 13 | var testCryptText = &[hashLen]byte{'t', 'e', 'x', 't'} 14 | 15 | func TestEncDec(t *testing.T) { 16 | c := qt.New(t) 17 | b, err := encrypt(testCryptKey, testCryptText, rand.Reader) 18 | c.Assert(err, qt.Equals, nil) 19 | p, err := decrypt(testCryptKey, b) 20 | c.Assert(err, qt.Equals, nil) 21 | c.Assert(string(p[:]), qt.Equals, string(testCryptText[:])) 22 | } 23 | 24 | func TestUniqueNonces(t *testing.T) { 25 | c := qt.New(t) 26 | nonces := make(map[string]struct{}) 27 | for i := 0; i < 100; i++ { 28 | nonce, err := newNonce(rand.Reader) 29 | c.Assert(err, qt.Equals, nil) 30 | nonces[string(nonce[:])] = struct{}{} 31 | } 32 | c.Assert(nonces, qt.HasLen, 100, qt.Commentf("duplicate nonce detected")) 33 | } 34 | 35 | type ErrorReader struct{} 36 | 37 | func (*ErrorReader) Read([]byte) (int, error) { 38 | return 0, fmt.Errorf("fail") 39 | } 40 | 41 | func TestBadRandom(t *testing.T) { 42 | c := qt.New(t) 43 | _, err := newNonce(&ErrorReader{}) 44 | c.Assert(err, qt.ErrorMatches, "^cannot generate random bytes:.*") 45 | 46 | _, err = encrypt(testCryptKey, testCryptText, &ErrorReader{}) 47 | c.Assert(err, qt.ErrorMatches, "^cannot generate random bytes:.*") 48 | } 49 | 50 | func TestBadCiphertext(t *testing.T) { 51 | c := qt.New(t) 52 | buf := randomBytes(nonceLen + secretbox.Overhead) 53 | for i := range buf { 54 | _, err := decrypt(testCryptKey, buf[0:i]) 55 | c.Assert(err, qt.ErrorMatches, "message too short") 56 | } 57 | _, err := decrypt(testCryptKey, buf) 58 | c.Assert(err, qt.ErrorMatches, "decryption failure") 59 | } 60 | 61 | func randomBytes(n int) []byte { 62 | buf := make([]byte, n) 63 | if _, err := rand.Reader.Read(buf); err != nil { 64 | panic(err) 65 | } 66 | return buf 67 | } 68 | -------------------------------------------------------------------------------- /dependencies.tsv: -------------------------------------------------------------------------------- 1 | github.com/frankban/quicktest git 2c6a0d60c05cd2d970f356eee0623ddf1cd0d62d 2018-02-06T12:35:47Z 2 | github.com/google/go-cmp git 5411ab924f9ffa6566244a9e504bc347edacffd3 2018-03-28T20:15:12Z 3 | github.com/kr/pretty git cfb55aafdaf3ec08f0db22699ab822c50091b1c4 2016-08-23T17:07:15Z 4 | github.com/kr/text git 7cafcd837844e784b526369c9bce262804aebc60 2016-05-04T23:40:17Z 5 | golang.org/x/crypto git 96846453c37f0876340a66a47f3f75b1f3a6cd2d 2017-04-21T04:31:20Z 6 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | var ( 4 | AddThirdPartyCaveatWithRand = (*Macaroon).addThirdPartyCaveatWithRand 5 | ) 6 | 7 | type MacaroonJSONV2 macaroonJSONV2 8 | 9 | // SetVersion sets the version field of m to v; 10 | // usually so that we can compare it for deep equality with 11 | // another differently unmarshaled macaroon. 12 | func (m *Macaroon) SetVersion(v Version) { 13 | m.version = v 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gopkg.in/macaroon.v2 2 | 3 | require ( 4 | github.com/frankban/quicktest v1.0.0 5 | github.com/google/go-cmp v0.2.0 6 | golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb 7 | ) 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/frankban/quicktest v1.0.0 h1:QgmxFbprE29UG4oL88tGiiL/7VuiBl5xCcz+wJcJhc0= 2 | github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= 3 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 4 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 5 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb h1:Ah9YqXLj6fEgeKqcmBuLCbAsrF3ScD7dJ/bYM0C6tXI= 10 | golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 11 | -------------------------------------------------------------------------------- /macaroon.go: -------------------------------------------------------------------------------- 1 | // The macaroon package implements macaroons as described in 2 | // the paper "Macaroons: Cookies with Contextual Caveats for 3 | // Decentralized Authorization in the Cloud" 4 | // (http://theory.stanford.edu/~ataly/Papers/macaroons.pdf) 5 | // 6 | // See the macaroon bakery packages at http://godoc.org/gopkg.in/macaroon-bakery.v2 7 | // for higher level services and operations that use macaroons. 8 | package macaroon 9 | 10 | import ( 11 | "bytes" 12 | "crypto/hmac" 13 | "crypto/rand" 14 | "fmt" 15 | "io" 16 | "unicode/utf8" 17 | ) 18 | 19 | // Macaroon holds a macaroon. 20 | // See Fig. 7 of http://theory.stanford.edu/~ataly/Papers/macaroons.pdf 21 | // for a description of the data contained within. 22 | // Macaroons are mutable objects - use Clone as appropriate 23 | // to avoid unwanted mutation. 24 | type Macaroon struct { 25 | location string 26 | id []byte 27 | caveats []Caveat 28 | sig [hashLen]byte 29 | version Version 30 | } 31 | 32 | // Equal reports whether m has exactly the same content as m1. 33 | func (m *Macaroon) Equal(m1 *Macaroon) bool { 34 | if m == m1 || m == nil || m1 == nil { 35 | return m == m1 36 | } 37 | if m.location != m1.location || 38 | !bytes.Equal(m.id, m1.id) || 39 | m.sig != m1.sig || 40 | m.version != m1.version || 41 | len(m.caveats) != len(m1.caveats) { 42 | return false 43 | } 44 | for i, c := range m.caveats { 45 | if !c.Equal(m1.caveats[i]) { 46 | return false 47 | } 48 | } 49 | return true 50 | } 51 | 52 | // Caveat holds a first party or third party caveat. 53 | type Caveat struct { 54 | // Id holds the id of the caveat. For first 55 | // party caveats this holds the condition; 56 | // for third party caveats this holds the encrypted 57 | // third party caveat. 58 | Id []byte 59 | 60 | // VerificationId holds the verification id. If this is 61 | // non-empty, it's a third party caveat. 62 | VerificationId []byte 63 | 64 | // For third-party caveats, Location holds the 65 | // ocation hint. Note that this is not signature checked 66 | // as part of the caveat, so should only 67 | // be used as a hint. 68 | Location string 69 | } 70 | 71 | // Equal reports whether c is equal to c1. 72 | func (c Caveat) Equal(c1 Caveat) bool { 73 | return bytes.Equal(c.Id, c1.Id) && 74 | bytes.Equal(c.VerificationId, c1.VerificationId) && 75 | c.Location == c1.Location 76 | } 77 | 78 | // isThirdParty reports whether the caveat must be satisfied 79 | // by some third party (if not, it's a first person caveat). 80 | func (cav *Caveat) isThirdParty() bool { 81 | return len(cav.VerificationId) > 0 82 | } 83 | 84 | // New returns a new macaroon with the given root key, 85 | // identifier, location and version. 86 | func New(rootKey, id []byte, loc string, version Version) (*Macaroon, error) { 87 | var m Macaroon 88 | if version < V2 { 89 | if !utf8.Valid(id) { 90 | return nil, fmt.Errorf("invalid id for %v macaroon", id) 91 | } 92 | // TODO check id length too. 93 | } 94 | if version < V1 || version > LatestVersion { 95 | return nil, fmt.Errorf("invalid version %v", version) 96 | } 97 | m.version = version 98 | m.init(append([]byte(nil), id...), loc, version) 99 | derivedKey := makeKey(rootKey) 100 | m.sig = *keyedHash(derivedKey, m.id) 101 | return &m, nil 102 | } 103 | 104 | // init initializes the macaroon. It retains a reference to id. 105 | func (m *Macaroon) init(id []byte, loc string, vers Version) { 106 | m.location = loc 107 | m.id = append([]byte(nil), id...) 108 | m.version = vers 109 | } 110 | 111 | // SetLocation sets the location associated with the macaroon. 112 | // Note that the location is not included in the macaroon's 113 | // hash chain, so this does not change the signature. 114 | func (m *Macaroon) SetLocation(loc string) { 115 | m.location = loc 116 | } 117 | 118 | // Clone returns a copy of the receiving macaroon. 119 | func (m *Macaroon) Clone() *Macaroon { 120 | m1 := *m 121 | // Ensure that if any caveats are appended to the new 122 | // macaroon, it will copy the caveats. 123 | m1.caveats = m1.caveats[0:len(m1.caveats):len(m1.caveats)] 124 | return &m1 125 | } 126 | 127 | // Location returns the macaroon's location hint. This is 128 | // not verified as part of the macaroon. 129 | func (m *Macaroon) Location() string { 130 | return m.location 131 | } 132 | 133 | // Id returns the id of the macaroon. This can hold 134 | // arbitrary information. 135 | func (m *Macaroon) Id() []byte { 136 | return append([]byte(nil), m.id...) 137 | } 138 | 139 | // Signature returns the macaroon's signature. 140 | func (m *Macaroon) Signature() []byte { 141 | // sig := m.sig 142 | // return sig[:] 143 | // Work around https://github.com/golang/go/issues/9537 144 | sig := new([hashLen]byte) 145 | *sig = m.sig 146 | return sig[:] 147 | } 148 | 149 | // Caveats returns the macaroon's caveats. 150 | // This method will probably change, and it's important not to change the returned caveat. 151 | func (m *Macaroon) Caveats() []Caveat { 152 | return m.caveats[0:len(m.caveats):len(m.caveats)] 153 | } 154 | 155 | // appendCaveat appends a caveat without modifying the macaroon's signature. 156 | func (m *Macaroon) appendCaveat(caveatId, verificationId []byte, loc string) { 157 | if len(verificationId) == 0 { 158 | // Ensure that an empty vid is always represented by nil, 159 | // so that marshalers don't procuce spurious zero-length 160 | // vid fields which can confuse some verifiers. 161 | verificationId = nil 162 | } 163 | m.caveats = append(m.caveats, Caveat{ 164 | Id: caveatId, 165 | VerificationId: verificationId, 166 | Location: loc, 167 | }) 168 | } 169 | 170 | func (m *Macaroon) addCaveat(caveatId, verificationId []byte, loc string) error { 171 | if m.version < V2 { 172 | if !utf8.Valid(caveatId) { 173 | return fmt.Errorf("invalid caveat id for %v macaroon", m.version) 174 | } 175 | // TODO check caveat length too. 176 | } 177 | m.appendCaveat(caveatId, verificationId, loc) 178 | if len(verificationId) == 0 { 179 | m.sig = *keyedHash(&m.sig, caveatId) 180 | } else { 181 | m.sig = *keyedHash2(&m.sig, verificationId, caveatId) 182 | } 183 | return nil 184 | } 185 | 186 | func keyedHash2(key *[keyLen]byte, d1, d2 []byte) *[hashLen]byte { 187 | var data [hashLen * 2]byte 188 | copy(data[0:], keyedHash(key, d1)[:]) 189 | copy(data[hashLen:], keyedHash(key, d2)[:]) 190 | return keyedHash(key, data[:]) 191 | } 192 | 193 | // Bind prepares the macaroon for being used to discharge the 194 | // macaroon with the given signature sig. This must be 195 | // used before it is used in the discharges argument to Verify. 196 | func (m *Macaroon) Bind(sig []byte) { 197 | m.sig = *bindForRequest(sig, &m.sig) 198 | } 199 | 200 | // AddFirstPartyCaveat adds a caveat that will be verified 201 | // by the target service. 202 | func (m *Macaroon) AddFirstPartyCaveat(condition []byte) error { 203 | m.addCaveat(condition, nil, "") 204 | return nil 205 | } 206 | 207 | // AddThirdPartyCaveat adds a third-party caveat to the macaroon, 208 | // using the given shared root key, caveat id and location hint. 209 | // The caveat id should encode the root key in some 210 | // way, either by encrypting it with a key known to the third party 211 | // or by holding a reference to it stored in the third party's 212 | // storage. 213 | func (m *Macaroon) AddThirdPartyCaveat(rootKey, caveatId []byte, loc string) error { 214 | return m.addThirdPartyCaveatWithRand(rootKey, caveatId, loc, rand.Reader) 215 | } 216 | 217 | // addThirdPartyCaveatWithRand adds a third-party caveat to the macaroon, using 218 | // the given source of randomness for encrypting the caveat id. 219 | func (m *Macaroon) addThirdPartyCaveatWithRand(rootKey, caveatId []byte, loc string, r io.Reader) error { 220 | derivedKey := makeKey(rootKey) 221 | verificationId, err := encrypt(&m.sig, derivedKey, r) 222 | if err != nil { 223 | return err 224 | } 225 | m.addCaveat(caveatId, verificationId, loc) 226 | return nil 227 | } 228 | 229 | var zeroKey [hashLen]byte 230 | 231 | // bindForRequest binds the given macaroon 232 | // to the given signature of its parent macaroon. 233 | func bindForRequest(rootSig []byte, dischargeSig *[hashLen]byte) *[hashLen]byte { 234 | if bytes.Equal(rootSig, dischargeSig[:]) { 235 | return dischargeSig 236 | } 237 | return keyedHash2(&zeroKey, rootSig, dischargeSig[:]) 238 | } 239 | 240 | // Verify verifies that the receiving macaroon is valid. 241 | // The root key must be the same that the macaroon was originally 242 | // minted with. The check function is called to verify each 243 | // first-party caveat - it should return an error if the 244 | // condition is not met. 245 | // 246 | // The discharge macaroons should be provided in discharges. 247 | // 248 | // Verify returns nil if the verification succeeds. 249 | func (m *Macaroon) Verify(rootKey []byte, check func(caveat string) error, discharges []*Macaroon) error { 250 | var vctx verificationContext 251 | vctx.init(rootKey, m, discharges, check) 252 | return vctx.verify(m, rootKey) 253 | } 254 | 255 | // VerifySignature verifies the signature of the given macaroon with respect 256 | // to the root key, but it does not validate any first-party caveats. Instead 257 | // it returns all the applicable first party caveats on success. 258 | // 259 | // The caller is responsible for checking the returned first party caveat 260 | // conditions. 261 | func (m *Macaroon) VerifySignature(rootKey []byte, discharges []*Macaroon) ([]string, error) { 262 | n := len(m.caveats) 263 | for _, dm := range discharges { 264 | n += len(dm.caveats) 265 | } 266 | conds := make([]string, 0, n) 267 | var vctx verificationContext 268 | vctx.init(rootKey, m, discharges, func(cond string) error { 269 | conds = append(conds, cond) 270 | return nil 271 | }) 272 | err := vctx.verify(m, rootKey) 273 | if err != nil { 274 | return nil, err 275 | } 276 | return conds, nil 277 | } 278 | 279 | // TraceVerify verifies the signature of the macaroon without checking 280 | // any of the first party caveats, and returns a slice of Traces holding 281 | // the operations used when verifying the macaroons. 282 | // 283 | // Each element in the returned slice corresponds to the 284 | // operation for one of the argument macaroons, with m at index 0, 285 | // and discharges at 1 onwards. 286 | func (m *Macaroon) TraceVerify(rootKey []byte, discharges []*Macaroon) ([]Trace, error) { 287 | var vctx verificationContext 288 | vctx.init(rootKey, m, discharges, func(string) error { return nil }) 289 | vctx.traces = make([]Trace, len(discharges)+1) 290 | err := vctx.verify(m, rootKey) 291 | return vctx.traces, err 292 | } 293 | 294 | type verificationContext struct { 295 | used []bool 296 | discharges []*Macaroon 297 | rootSig *[hashLen]byte 298 | traces []Trace 299 | check func(caveat string) error 300 | } 301 | 302 | func (vctx *verificationContext) init(rootKey []byte, root *Macaroon, discharges []*Macaroon, check func(caveat string) error) { 303 | *vctx = verificationContext{ 304 | discharges: discharges, 305 | used: make([]bool, len(discharges)), 306 | rootSig: &root.sig, 307 | check: check, 308 | } 309 | } 310 | 311 | func (vctx *verificationContext) verify(root *Macaroon, rootKey []byte) error { 312 | vctx.traceRootKey(0, rootKey) 313 | vctx.trace(0, TraceMakeKey, rootKey, nil) 314 | derivedKey := makeKey(rootKey) 315 | if err := vctx.verify0(root, 0, derivedKey); err != nil { 316 | vctx.trace(0, TraceFail, nil, nil) 317 | return err 318 | } 319 | for i, wasUsed := range vctx.used { 320 | if !wasUsed { 321 | vctx.trace(i+1, TraceFail, nil, nil) 322 | return fmt.Errorf("discharge macaroon %q was not used", vctx.discharges[i].Id()) 323 | } 324 | } 325 | return nil 326 | } 327 | 328 | func (vctx *verificationContext) verify0(m *Macaroon, index int, rootKey *[hashLen]byte) error { 329 | vctx.trace(index, TraceHash, m.id, nil) 330 | caveatSig := keyedHash(rootKey, m.id) 331 | for i, cav := range m.caveats { 332 | if cav.isThirdParty() { 333 | cavKey, err := decrypt(caveatSig, cav.VerificationId) 334 | if err != nil { 335 | return fmt.Errorf("failed to decrypt caveat %d signature: %v", i, err) 336 | } 337 | dm, di, err := vctx.findDischarge(cav.Id) 338 | if err != nil { 339 | return err 340 | } 341 | vctx.traceRootKey(di+1, cavKey[:]) 342 | if err := vctx.verify0(dm, di+1, cavKey); err != nil { 343 | vctx.trace(di+1, TraceFail, nil, nil) 344 | return err 345 | } 346 | vctx.trace(index, TraceHash, cav.VerificationId, cav.Id) 347 | caveatSig = keyedHash2(caveatSig, cav.VerificationId, cav.Id) 348 | } else { 349 | vctx.trace(index, TraceHash, cav.Id, nil) 350 | caveatSig = keyedHash(caveatSig, cav.Id) 351 | if err := vctx.check(string(cav.Id)); err != nil { 352 | return err 353 | } 354 | } 355 | } 356 | if index > 0 { 357 | vctx.trace(index, TraceBind, vctx.rootSig[:], caveatSig[:]) 358 | caveatSig = bindForRequest(vctx.rootSig[:], caveatSig) 359 | } 360 | // TODO perhaps we should actually do this check before doing 361 | // all the potentially expensive caveat checks. 362 | if !hmac.Equal(caveatSig[:], m.sig[:]) { 363 | return fmt.Errorf("signature mismatch after caveat verification") 364 | } 365 | return nil 366 | } 367 | 368 | func (vctx *verificationContext) findDischarge(id []byte) (dm *Macaroon, index int, err error) { 369 | for di, dm := range vctx.discharges { 370 | if !bytes.Equal(dm.id, id) { 371 | continue 372 | } 373 | // Don't use a discharge macaroon more than once. 374 | // It's important that we do this check here rather than after 375 | // verify as it prevents potentially infinite recursion. 376 | if vctx.used[di] { 377 | return nil, 0, fmt.Errorf("discharge macaroon %q was used more than once", dm.Id()) 378 | } 379 | vctx.used[di] = true 380 | return dm, di, nil 381 | } 382 | return nil, 0, fmt.Errorf("cannot find discharge macaroon for caveat %x", id) 383 | } 384 | 385 | func (vctx *verificationContext) trace(index int, op TraceOpKind, data1, data2 []byte) { 386 | if vctx.traces != nil { 387 | vctx.traces[index].Ops = append(vctx.traces[index].Ops, TraceOp{ 388 | Kind: op, 389 | Data1: data1, 390 | Data2: data2, 391 | }) 392 | } 393 | } 394 | 395 | func (vctx *verificationContext) traceRootKey(index int, rootKey []byte) { 396 | if vctx.traces != nil { 397 | vctx.traces[index].RootKey = rootKey[:] 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /macaroon_test.go: -------------------------------------------------------------------------------- 1 | package macaroon_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "testing" 9 | 10 | qt "github.com/frankban/quicktest" 11 | 12 | "gopkg.in/macaroon.v2" 13 | ) 14 | 15 | func never(string) error { 16 | return fmt.Errorf("condition is never true") 17 | } 18 | 19 | func TestNoCaveats(t *testing.T) { 20 | c := qt.New(t) 21 | rootKey := []byte("secret") 22 | m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion) 23 | c.Assert(m.Location(), qt.Equals, "a location") 24 | c.Assert(m.Id(), qt.DeepEquals, []byte("some id")) 25 | 26 | err := m.Verify(rootKey, never, nil) 27 | c.Assert(err, qt.IsNil) 28 | } 29 | 30 | func TestFirstPartyCaveat(t *testing.T) { 31 | c := qt.New(t) 32 | rootKey := []byte("secret") 33 | m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion) 34 | 35 | caveats := map[string]bool{ 36 | "a caveat": true, 37 | "another caveat": true, 38 | } 39 | tested := make(map[string]bool) 40 | 41 | for cav := range caveats { 42 | m.AddFirstPartyCaveat([]byte(cav)) 43 | } 44 | expectErr := fmt.Errorf("condition not met") 45 | check := func(cav string) error { 46 | tested[cav] = true 47 | if caveats[cav] { 48 | return nil 49 | } 50 | return expectErr 51 | } 52 | err := m.Verify(rootKey, check, nil) 53 | c.Assert(err, qt.IsNil) 54 | 55 | c.Assert(tested, qt.DeepEquals, caveats) 56 | 57 | m.AddFirstPartyCaveat([]byte("not met")) 58 | err = m.Verify(rootKey, check, nil) 59 | c.Assert(err, qt.Equals, expectErr) 60 | 61 | c.Assert(tested["not met"], qt.Equals, true) 62 | } 63 | 64 | func TestThirdPartyCaveat(t *testing.T) { 65 | c := qt.New(t) 66 | rootKey := []byte("secret") 67 | m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion) 68 | 69 | dischargeRootKey := []byte("shared root key") 70 | thirdPartyCaveatId := []byte("3rd party caveat") 71 | err := m.AddThirdPartyCaveat(dischargeRootKey, thirdPartyCaveatId, "remote.com") 72 | c.Assert(err, qt.IsNil) 73 | 74 | dm := MustNew(dischargeRootKey, thirdPartyCaveatId, "remote location", macaroon.LatestVersion) 75 | dm.Bind(m.Signature()) 76 | err = m.Verify(rootKey, never, []*macaroon.Macaroon{dm}) 77 | c.Assert(err, qt.IsNil) 78 | } 79 | 80 | func TestThirdPartyCaveatBadRandom(t *testing.T) { 81 | c := qt.New(t) 82 | rootKey := []byte("secret") 83 | m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion) 84 | dischargeRootKey := []byte("shared root key") 85 | thirdPartyCaveatId := []byte("3rd party caveat") 86 | 87 | err := macaroon.AddThirdPartyCaveatWithRand(m, dischargeRootKey, thirdPartyCaveatId, "remote.com", &macaroon.ErrorReader{}) 88 | c.Assert(err, qt.ErrorMatches, "cannot generate random bytes: fail") 89 | } 90 | 91 | func TestSetLocation(t *testing.T) { 92 | c := qt.New(t) 93 | rootKey := []byte("secret") 94 | m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion) 95 | c.Assert(m.Location(), qt.Equals, "a location") 96 | m.SetLocation("another location") 97 | c.Assert(m.Location(), qt.Equals, "another location") 98 | } 99 | 100 | var equalTests = []struct { 101 | about string 102 | m1, m2 macaroonSpec 103 | expect bool 104 | }{{ 105 | about: "same_everywhere", 106 | m1: macaroonSpec{ 107 | rootKey: "root-key", 108 | id: "root-id", 109 | caveats: []caveat{{ 110 | condition: "a", 111 | }, { 112 | condition: "d", 113 | }}, 114 | }, 115 | m2: macaroonSpec{ 116 | rootKey: "root-key", 117 | id: "root-id", 118 | caveats: []caveat{{ 119 | condition: "a", 120 | }, { 121 | condition: "d", 122 | }}, 123 | }, 124 | expect: true, 125 | }, { 126 | about: "root_key_differs", 127 | m1: macaroonSpec{ 128 | rootKey: "root-key", 129 | id: "root-id", 130 | caveats: []caveat{{ 131 | condition: "a", 132 | }, { 133 | condition: "d", 134 | }}, 135 | }, 136 | m2: macaroonSpec{ 137 | rootKey: "root-key1", 138 | id: "root-id", 139 | caveats: []caveat{{ 140 | condition: "a", 141 | }, { 142 | condition: "d", 143 | }}, 144 | }, 145 | expect: false, 146 | }, { 147 | about: "id_differs", 148 | m1: macaroonSpec{ 149 | rootKey: "root-key", 150 | id: "root-id", 151 | }, 152 | m2: macaroonSpec{ 153 | rootKey: "root-key", 154 | id: "root-id1", 155 | }, 156 | expect: false, 157 | }, { 158 | about: "extra_caveat_1", 159 | m1: macaroonSpec{ 160 | rootKey: "root-key", 161 | id: "root-id", 162 | caveats: []caveat{{ 163 | condition: "a", 164 | }, { 165 | condition: "d", 166 | }, { 167 | condition: "d", 168 | }}, 169 | }, 170 | m2: macaroonSpec{ 171 | rootKey: "root-key", 172 | id: "root-id", 173 | caveats: []caveat{{ 174 | condition: "a", 175 | }, { 176 | condition: "d", 177 | }}, 178 | }, 179 | expect: false, 180 | }, { 181 | about: "extra_caveat_2", 182 | m1: macaroonSpec{ 183 | rootKey: "root-key1", 184 | id: "root-id", 185 | caveats: []caveat{{ 186 | condition: "a", 187 | }, { 188 | condition: "d", 189 | }}, 190 | }, 191 | m2: macaroonSpec{ 192 | rootKey: "root-key", 193 | id: "root-id", 194 | caveats: []caveat{{ 195 | condition: "a", 196 | }, { 197 | condition: "d", 198 | }, { 199 | condition: "d", 200 | }}, 201 | }, 202 | expect: false, 203 | }, { 204 | about: "caveat_condition_differs", 205 | m1: macaroonSpec{ 206 | rootKey: "root-key", 207 | id: "root-id", 208 | caveats: []caveat{{ 209 | condition: "a", 210 | location: "b", 211 | rootKey: "c", 212 | }}, 213 | }, 214 | m2: macaroonSpec{ 215 | rootKey: "root-key", 216 | id: "root-id", 217 | caveats: []caveat{{ 218 | condition: "a1", 219 | location: "b", 220 | rootKey: "c", 221 | }}, 222 | }, 223 | expect: false, 224 | }} 225 | 226 | func TestEqual(t *testing.T) { 227 | c := qt.New(t) 228 | for _, test := range equalTests { 229 | c.Run(test.about, func(c *qt.C) { 230 | m1 := makeMacaroon(test.m1) 231 | m2 := makeMacaroon(test.m2) 232 | c.Assert(m1.Equal(m2), qt.Equals, test.expect) 233 | }) 234 | } 235 | } 236 | 237 | func TestEqualNil(t *testing.T) { 238 | c := qt.New(t) 239 | var nilm *macaroon.Macaroon 240 | var m = MustNew([]byte("k"), []byte("x"), "l", macaroon.LatestVersion) 241 | c.Assert(nilm.Equal(nilm), qt.Equals, true) 242 | c.Assert(nilm.Equal(m), qt.Equals, false) 243 | c.Assert(m.Equal(nilm), qt.Equals, false) 244 | } 245 | 246 | type conditionTest struct { 247 | conditions map[string]bool 248 | expectErr string 249 | } 250 | 251 | var verifyTests = []struct { 252 | about string 253 | macaroons []macaroonSpec 254 | conditions []conditionTest 255 | }{{ 256 | about: "single third party caveat without discharge", 257 | macaroons: []macaroonSpec{{ 258 | rootKey: "root-key", 259 | id: "root-id", 260 | caveats: []caveat{{ 261 | condition: "wonderful", 262 | }, { 263 | condition: "bob-is-great", 264 | location: "bob", 265 | rootKey: "bob-caveat-root-key", 266 | }}, 267 | }}, 268 | conditions: []conditionTest{{ 269 | conditions: map[string]bool{ 270 | "wonderful": true, 271 | }, 272 | expectErr: fmt.Sprintf(`cannot find discharge macaroon for caveat %x`, "bob-is-great"), 273 | }}, 274 | }, { 275 | about: "single third party caveat with discharge", 276 | macaroons: []macaroonSpec{{ 277 | rootKey: "root-key", 278 | id: "root-id", 279 | caveats: []caveat{{ 280 | condition: "wonderful", 281 | }, { 282 | condition: "bob-is-great", 283 | location: "bob", 284 | rootKey: "bob-caveat-root-key", 285 | }}, 286 | }, { 287 | location: "bob", 288 | rootKey: "bob-caveat-root-key", 289 | id: "bob-is-great", 290 | }}, 291 | conditions: []conditionTest{{ 292 | conditions: map[string]bool{ 293 | "wonderful": true, 294 | }, 295 | }, { 296 | conditions: map[string]bool{ 297 | "wonderful": false, 298 | }, 299 | expectErr: `condition "wonderful" not met`, 300 | }}, 301 | }, { 302 | about: "single third party caveat with discharge with mismatching root key", 303 | macaroons: []macaroonSpec{{ 304 | rootKey: "root-key", 305 | id: "root-id", 306 | caveats: []caveat{{ 307 | condition: "wonderful", 308 | }, { 309 | condition: "bob-is-great", 310 | location: "bob", 311 | rootKey: "bob-caveat-root-key", 312 | }}, 313 | }, { 314 | location: "bob", 315 | rootKey: "bob-caveat-root-key-wrong", 316 | id: "bob-is-great", 317 | }}, 318 | conditions: []conditionTest{{ 319 | conditions: map[string]bool{ 320 | "wonderful": true, 321 | }, 322 | expectErr: `signature mismatch after caveat verification`, 323 | }}, 324 | }, { 325 | about: "single third party caveat with two discharges", 326 | macaroons: []macaroonSpec{{ 327 | rootKey: "root-key", 328 | id: "root-id", 329 | caveats: []caveat{{ 330 | condition: "wonderful", 331 | }, { 332 | condition: "bob-is-great", 333 | location: "bob", 334 | rootKey: "bob-caveat-root-key", 335 | }}, 336 | }, { 337 | location: "bob", 338 | rootKey: "bob-caveat-root-key", 339 | id: "bob-is-great", 340 | caveats: []caveat{{ 341 | condition: "splendid", 342 | }}, 343 | }, { 344 | location: "bob", 345 | rootKey: "bob-caveat-root-key", 346 | id: "bob-is-great", 347 | caveats: []caveat{{ 348 | condition: "top of the world", 349 | }}, 350 | }}, 351 | conditions: []conditionTest{{ 352 | conditions: map[string]bool{ 353 | "wonderful": true, 354 | }, 355 | expectErr: `condition "splendid" not met`, 356 | }, { 357 | conditions: map[string]bool{ 358 | "wonderful": true, 359 | "splendid": true, 360 | "top of the world": true, 361 | }, 362 | expectErr: `discharge macaroon "bob-is-great" was not used`, 363 | }, { 364 | conditions: map[string]bool{ 365 | "wonderful": true, 366 | "splendid": false, 367 | "top of the world": true, 368 | }, 369 | expectErr: `condition "splendid" not met`, 370 | }, { 371 | conditions: map[string]bool{ 372 | "wonderful": true, 373 | "splendid": true, 374 | "top of the world": false, 375 | }, 376 | expectErr: `discharge macaroon "bob-is-great" was not used`, 377 | }}, 378 | }, { 379 | about: "one discharge used for two macaroons", 380 | macaroons: []macaroonSpec{{ 381 | rootKey: "root-key", 382 | id: "root-id", 383 | caveats: []caveat{{ 384 | condition: "somewhere else", 385 | location: "bob", 386 | rootKey: "bob-caveat-root-key", 387 | }, { 388 | condition: "bob-is-great", 389 | location: "charlie", 390 | rootKey: "bob-caveat-root-key", 391 | }}, 392 | }, { 393 | location: "bob", 394 | rootKey: "bob-caveat-root-key", 395 | id: "somewhere else", 396 | caveats: []caveat{{ 397 | condition: "bob-is-great", 398 | location: "charlie", 399 | rootKey: "bob-caveat-root-key", 400 | }}, 401 | }, { 402 | location: "bob", 403 | rootKey: "bob-caveat-root-key", 404 | id: "bob-is-great", 405 | }}, 406 | conditions: []conditionTest{{ 407 | expectErr: `discharge macaroon "bob-is-great" was used more than once`, 408 | }}, 409 | }, { 410 | about: "recursive third party caveat", 411 | macaroons: []macaroonSpec{{ 412 | rootKey: "root-key", 413 | id: "root-id", 414 | caveats: []caveat{{ 415 | condition: "bob-is-great", 416 | location: "bob", 417 | rootKey: "bob-caveat-root-key", 418 | }}, 419 | }, { 420 | location: "bob", 421 | rootKey: "bob-caveat-root-key", 422 | id: "bob-is-great", 423 | caveats: []caveat{{ 424 | condition: "bob-is-great", 425 | location: "charlie", 426 | rootKey: "bob-caveat-root-key", 427 | }}, 428 | }}, 429 | conditions: []conditionTest{{ 430 | expectErr: `discharge macaroon "bob-is-great" was used more than once`, 431 | }}, 432 | }, { 433 | about: "two third party caveats", 434 | macaroons: []macaroonSpec{{ 435 | rootKey: "root-key", 436 | id: "root-id", 437 | caveats: []caveat{{ 438 | condition: "wonderful", 439 | }, { 440 | condition: "bob-is-great", 441 | location: "bob", 442 | rootKey: "bob-caveat-root-key", 443 | }, { 444 | condition: "charlie-is-great", 445 | location: "charlie", 446 | rootKey: "charlie-caveat-root-key", 447 | }}, 448 | }, { 449 | location: "bob", 450 | rootKey: "bob-caveat-root-key", 451 | id: "bob-is-great", 452 | caveats: []caveat{{ 453 | condition: "splendid", 454 | }}, 455 | }, { 456 | location: "charlie", 457 | rootKey: "charlie-caveat-root-key", 458 | id: "charlie-is-great", 459 | caveats: []caveat{{ 460 | condition: "top of the world", 461 | }}, 462 | }}, 463 | conditions: []conditionTest{{ 464 | conditions: map[string]bool{ 465 | "wonderful": true, 466 | "splendid": true, 467 | "top of the world": true, 468 | }, 469 | }, { 470 | conditions: map[string]bool{ 471 | "wonderful": true, 472 | "splendid": false, 473 | "top of the world": true, 474 | }, 475 | expectErr: `condition "splendid" not met`, 476 | }, { 477 | conditions: map[string]bool{ 478 | "wonderful": true, 479 | "splendid": true, 480 | "top of the world": false, 481 | }, 482 | expectErr: `condition "top of the world" not met`, 483 | }}, 484 | }, { 485 | about: "third party caveat with undischarged third party caveat", 486 | macaroons: []macaroonSpec{{ 487 | rootKey: "root-key", 488 | id: "root-id", 489 | caveats: []caveat{{ 490 | condition: "wonderful", 491 | }, { 492 | condition: "bob-is-great", 493 | location: "bob", 494 | rootKey: "bob-caveat-root-key", 495 | }}, 496 | }, { 497 | location: "bob", 498 | rootKey: "bob-caveat-root-key", 499 | id: "bob-is-great", 500 | caveats: []caveat{{ 501 | condition: "splendid", 502 | }, { 503 | condition: "barbara-is-great", 504 | location: "barbara", 505 | rootKey: "barbara-caveat-root-key", 506 | }}, 507 | }}, 508 | conditions: []conditionTest{{ 509 | conditions: map[string]bool{ 510 | "wonderful": true, 511 | "splendid": true, 512 | }, 513 | expectErr: fmt.Sprintf(`cannot find discharge macaroon for caveat %x`, "barbara-is-great"), 514 | }}, 515 | }, { 516 | about: "multilevel third party caveats", 517 | macaroons: multilevelThirdPartyCaveatMacaroons, 518 | conditions: []conditionTest{{ 519 | conditions: map[string]bool{ 520 | "wonderful": true, 521 | "splendid": true, 522 | "high-fiving": true, 523 | "spiffing": true, 524 | }, 525 | }, { 526 | conditions: map[string]bool{ 527 | "wonderful": true, 528 | "splendid": true, 529 | "high-fiving": false, 530 | "spiffing": true, 531 | }, 532 | expectErr: `condition "high-fiving" not met`, 533 | }}, 534 | }, { 535 | about: "unused discharge", 536 | macaroons: []macaroonSpec{{ 537 | rootKey: "root-key", 538 | id: "root-id", 539 | }, { 540 | rootKey: "other-key", 541 | id: "unused", 542 | }}, 543 | conditions: []conditionTest{{ 544 | expectErr: `discharge macaroon "unused" was not used`, 545 | }}, 546 | }} 547 | 548 | var multilevelThirdPartyCaveatMacaroons = []macaroonSpec{{ 549 | rootKey: "root-key", 550 | id: "root-id", 551 | caveats: []caveat{{ 552 | condition: "wonderful", 553 | }, { 554 | condition: "bob-is-great", 555 | location: "bob", 556 | rootKey: "bob-caveat-root-key", 557 | }, { 558 | condition: "charlie-is-great", 559 | location: "charlie", 560 | rootKey: "charlie-caveat-root-key", 561 | }}, 562 | }, { 563 | location: "bob", 564 | rootKey: "bob-caveat-root-key", 565 | id: "bob-is-great", 566 | caveats: []caveat{{ 567 | condition: "splendid", 568 | }, { 569 | condition: "barbara-is-great", 570 | location: "barbara", 571 | rootKey: "barbara-caveat-root-key", 572 | }}, 573 | }, { 574 | location: "charlie", 575 | rootKey: "charlie-caveat-root-key", 576 | id: "charlie-is-great", 577 | caveats: []caveat{{ 578 | condition: "splendid", 579 | }, { 580 | condition: "celine-is-great", 581 | location: "celine", 582 | rootKey: "celine-caveat-root-key", 583 | }}, 584 | }, { 585 | location: "barbara", 586 | rootKey: "barbara-caveat-root-key", 587 | id: "barbara-is-great", 588 | caveats: []caveat{{ 589 | condition: "spiffing", 590 | }, { 591 | condition: "ben-is-great", 592 | location: "ben", 593 | rootKey: "ben-caveat-root-key", 594 | }}, 595 | }, { 596 | location: "ben", 597 | rootKey: "ben-caveat-root-key", 598 | id: "ben-is-great", 599 | }, { 600 | location: "celine", 601 | rootKey: "celine-caveat-root-key", 602 | id: "celine-is-great", 603 | caveats: []caveat{{ 604 | condition: "high-fiving", 605 | }}, 606 | }} 607 | 608 | func TestVerify(t *testing.T) { 609 | c := qt.New(t) 610 | for i, test := range verifyTests { 611 | c.Logf("test %d: %s", i, test.about) 612 | rootKey, macaroons := makeMacaroons(test.macaroons) 613 | for _, cond := range test.conditions { 614 | c.Logf("conditions %#v", cond.conditions) 615 | check := func(cav string) error { 616 | if cond.conditions[cav] { 617 | return nil 618 | } 619 | return fmt.Errorf("condition %q not met", cav) 620 | } 621 | err := macaroons[0].Verify( 622 | rootKey, 623 | check, 624 | macaroons[1:], 625 | ) 626 | if cond.expectErr != "" { 627 | c.Assert(err, qt.ErrorMatches, cond.expectErr) 628 | } else { 629 | c.Assert(err, qt.IsNil) 630 | } 631 | 632 | // Cloned macaroon should have same verify result. 633 | cloneErr := macaroons[0].Clone().Verify(rootKey, check, macaroons[1:]) 634 | if err == nil { 635 | c.Assert(cloneErr, qt.Equals, nil) 636 | } else { 637 | c.Assert(cloneErr.Error(), qt.Equals, err.Error()) 638 | } 639 | } 640 | } 641 | } 642 | 643 | func TestTraceVerify(t *testing.T) { 644 | c := qt.New(t) 645 | rootKey, macaroons := makeMacaroons(multilevelThirdPartyCaveatMacaroons) 646 | traces, err := macaroons[0].TraceVerify(rootKey, macaroons[1:]) 647 | c.Assert(err, qt.Equals, nil) 648 | c.Assert(traces, qt.HasLen, len(macaroons)) 649 | // Check that we can run through the resulting operations and 650 | // arrive at the same signature. 651 | for i, m := range macaroons { 652 | r := traces[i].Results() 653 | c.Assert(b64str(r[len(r)-1]), qt.Equals, b64str(m.Signature()), qt.Commentf("macaroon %d", i)) 654 | } 655 | } 656 | 657 | func TestTraceVerifyFailure(t *testing.T) { 658 | c := qt.New(t) 659 | rootKey, macaroons := makeMacaroons([]macaroonSpec{{ 660 | rootKey: "xxx", 661 | id: "hello", 662 | caveats: []caveat{{ 663 | condition: "cond1", 664 | }, { 665 | condition: "cond2", 666 | }, { 667 | condition: "cond3", 668 | }}, 669 | }}) 670 | // Marshal the macaroon, corrupt a condition, then unmarshal 671 | // it and check we see the expected trace failure. 672 | data, err := json.Marshal(macaroons[0]) 673 | c.Assert(err, qt.Equals, nil) 674 | var jm macaroon.MacaroonJSONV2 675 | err = json.Unmarshal(data, &jm) 676 | c.Assert(err, qt.Equals, nil) 677 | jm.Caveats[1].CID = "cond2 corrupted" 678 | data, err = json.Marshal(jm) 679 | c.Assert(err, qt.Equals, nil) 680 | 681 | var corruptm *macaroon.Macaroon 682 | err = json.Unmarshal(data, &corruptm) 683 | c.Assert(err, qt.Equals, nil) 684 | 685 | traces, err := corruptm.TraceVerify(rootKey, nil) 686 | c.Assert(err, qt.ErrorMatches, `signature mismatch after caveat verification`) 687 | c.Assert(traces, qt.HasLen, 1) 688 | var kinds []macaroon.TraceOpKind 689 | for _, op := range traces[0].Ops { 690 | kinds = append(kinds, op.Kind) 691 | } 692 | c.Assert(kinds, qt.DeepEquals, []macaroon.TraceOpKind{ 693 | macaroon.TraceMakeKey, 694 | macaroon.TraceHash, // id 695 | macaroon.TraceHash, // cond1 696 | macaroon.TraceHash, // cond2 697 | macaroon.TraceHash, // cond3 698 | macaroon.TraceFail, // sig mismatch 699 | }) 700 | } 701 | 702 | func b64str(b []byte) string { 703 | return base64.StdEncoding.EncodeToString(b) 704 | } 705 | 706 | func TestVerifySignature(t *testing.T) { 707 | c := qt.New(t) 708 | rootKey, macaroons := makeMacaroons([]macaroonSpec{{ 709 | rootKey: "xxx", 710 | id: "hello", 711 | caveats: []caveat{{ 712 | rootKey: "y", 713 | condition: "something", 714 | location: "somewhere", 715 | }, { 716 | condition: "cond1", 717 | }, { 718 | condition: "cond2", 719 | }}, 720 | }, { 721 | rootKey: "y", 722 | id: "something", 723 | caveats: []caveat{{ 724 | condition: "cond3", 725 | }, { 726 | condition: "cond4", 727 | }}, 728 | }}) 729 | conds, err := macaroons[0].VerifySignature(rootKey, macaroons[1:]) 730 | c.Assert(err, qt.IsNil) 731 | c.Assert(conds, qt.DeepEquals, []string{"cond3", "cond4", "cond1", "cond2"}) 732 | 733 | conds, err = macaroons[0].VerifySignature(nil, macaroons[1:]) 734 | c.Assert(err, qt.ErrorMatches, `failed to decrypt caveat 0 signature: decryption failure`) 735 | c.Assert(conds, qt.IsNil) 736 | } 737 | 738 | // TODO(rog) move the following JSON-marshal tests into marshal_test.go. 739 | 740 | // jsonTestVersions holds the various possible ways of marshaling a macaroon 741 | // to JSON. 742 | var jsonTestVersions = []macaroon.Version{ 743 | macaroon.V1, 744 | macaroon.V2, 745 | } 746 | 747 | func TestMarshalJSON(t *testing.T) { 748 | c := qt.New(t) 749 | for _, vers := range jsonTestVersions { 750 | c.Run(fmt.Sprintf("version_%v", vers), func(c *qt.C) { 751 | testMarshalJSONWithVersion(c, vers) 752 | }) 753 | } 754 | } 755 | 756 | func testMarshalJSONWithVersion(c *qt.C, vers macaroon.Version) { 757 | rootKey := []byte("secret") 758 | m0 := MustNew(rootKey, []byte("some id"), "a location", vers) 759 | m0.AddFirstPartyCaveat([]byte("account = 3735928559")) 760 | m0JSON, err := json.Marshal(m0) 761 | c.Assert(err, qt.IsNil) 762 | var m1 macaroon.Macaroon 763 | err = json.Unmarshal(m0JSON, &m1) 764 | c.Assert(err, qt.IsNil) 765 | c.Assert(m0.Location(), qt.Equals, m1.Location()) 766 | c.Assert(string(m0.Id()), qt.Equals, string(m1.Id())) 767 | c.Assert( 768 | hex.EncodeToString(m0.Signature()), 769 | qt.Equals, 770 | hex.EncodeToString(m1.Signature())) 771 | c.Assert(m1.Version(), qt.Equals, vers) 772 | } 773 | 774 | var jsonRoundTripTests = []struct { 775 | about string 776 | // data holds the marshaled data. All the data values hold 777 | // different encodings of the same macaroon - the same as produced 778 | // from the second example in libmacaroons 779 | // example README with the following libmacaroons code: 780 | // 781 | // secret = 'this is a different super-secret key; never use the same secret twice' 782 | // public = 'we used our other secret key' 783 | // location = 'http://mybank/' 784 | // M = macaroons.create(location, secret, public) 785 | // M = M.add_first_party_caveat('account = 3735928559') 786 | // caveat_key = '4; guaranteed random by a fair toss of the dice' 787 | // predicate = 'user = Alice' 788 | // identifier = 'this was how we remind auth of key/pred' 789 | // M = M.add_third_party_caveat('http://auth.mybank/', caveat_key, identifier) 790 | // m.serialize_json() 791 | data string 792 | expectExactRoundTrip bool 793 | expectVers macaroon.Version 794 | }{{ 795 | about: "exact_JSON_as_produced_by_libmacaroons", 796 | data: `{"caveats":[{"cid":"account = 3735928559"},{"cid":"this was how we remind auth of key\/pred","vid":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr","cl":"http:\/\/auth.mybank\/"}],"location":"http:\/\/mybank\/","identifier":"we used our other secret key","signature":"d27db2fd1f22760e4c3dae8137e2d8fc1df6c0741c18aed4b97256bf78d1f55c"}`, 797 | expectVers: macaroon.V1, 798 | expectExactRoundTrip: true, 799 | }, { 800 | about: "V2_object_with_std_base-64_binary_values", 801 | data: `{"c":[{"i64":"YWNjb3VudCA9IDM3MzU5Mjg1NTk="},{"i64":"dGhpcyB3YXMgaG93IHdlIHJlbWluZCBhdXRoIG9mIGtleS9wcmVk","v64":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD/w/dedwv4Jjw7UorCREw5rXbRqIKhr","l":"http://auth.mybank/"}],"l":"http://mybank/","i64":"d2UgdXNlZCBvdXIgb3RoZXIgc2VjcmV0IGtleQ==","s64":"0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Vw="}`, 802 | expectVers: macaroon.V2, 803 | }, { 804 | about: "V2_object_with_URL_base-64_binary_values", 805 | data: `{"c":[{"i64":"YWNjb3VudCA9IDM3MzU5Mjg1NTk"},{"i64":"dGhpcyB3YXMgaG93IHdlIHJlbWluZCBhdXRoIG9mIGtleS9wcmVk","v64":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr","l":"http://auth.mybank/"}],"l":"http://mybank/","i64":"d2UgdXNlZCBvdXIgb3RoZXIgc2VjcmV0IGtleQ","s64":"0n2y_R8idg5MPa6BN-LY_B32wHQcGK7UuXJWv3jR9Vw"}`, 806 | expectVers: macaroon.V2, 807 | }, { 808 | about: "V2_object_with_URL_base-64_binary_values_and_strings_for_ASCII", 809 | data: `{"c":[{"i":"account = 3735928559"},{"i":"this was how we remind auth of key/pred","v64":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr","l":"http://auth.mybank/"}],"l":"http://mybank/","i":"we used our other secret key","s64":"0n2y_R8idg5MPa6BN-LY_B32wHQcGK7UuXJWv3jR9Vw"}`, 810 | expectVers: macaroon.V2, 811 | expectExactRoundTrip: true, 812 | }, { 813 | about: "V2_base64_encoded_binary", 814 | data: `"` + 815 | base64.StdEncoding.EncodeToString([]byte( 816 | "\x02"+ 817 | "\x01\x0ehttp://mybank/"+ 818 | "\x02\x1cwe used our other secret key"+ 819 | "\x00"+ 820 | "\x02\x14account = 3735928559"+ 821 | "\x00"+ 822 | "\x01\x13http://auth.mybank/"+ 823 | "\x02'this was how we remind auth of key/pred"+ 824 | "\x04\x48\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x6e\xc5\x02\xe0\x58\x86\xd1\xf0\x27\x9f\x05\x5f\xa5\x25\x54\xd1\x6d\x16\xc1\xb1\x40\x74\xbb\xb8\x3f\xf0\xfd\xd7\x9d\xc2\xfe\x09\x8f\x0e\xd4\xa2\xb0\x91\x13\x0e\x6b\x5d\xb4\x6a\x20\xa8\x6b"+ 825 | "\x00"+ 826 | "\x00"+ 827 | "\x06\x20\xd2\x7d\xb2\xfd\x1f\x22\x76\x0e\x4c\x3d\xae\x81\x37\xe2\xd8\xfc\x1d\xf6\xc0\x74\x1c\x18\xae\xd4\xb9\x72\x56\xbf\x78\xd1\xf5\x5c", 828 | )) + `"`, 829 | expectVers: macaroon.V2, 830 | }} 831 | 832 | func TestJSONRoundTrip(t *testing.T) { 833 | c := qt.New(t) 834 | for _, test := range jsonRoundTripTests { 835 | c.Run(fmt.Sprintf("v%v_%s", test.expectVers, test.about), func(c *qt.C) { 836 | testJSONRoundTripWithVersion(c, test.data, test.expectVers, test.expectExactRoundTrip) 837 | }) 838 | } 839 | } 840 | 841 | func testJSONRoundTripWithVersion(c *qt.C, jsonData string, vers macaroon.Version, expectExactRoundTrip bool) { 842 | var m macaroon.Macaroon 843 | err := json.Unmarshal([]byte(jsonData), &m) 844 | c.Assert(err, qt.IsNil) 845 | assertLibMacaroonsMacaroon(c, &m) 846 | c.Assert(m.Version(), qt.Equals, vers) 847 | 848 | data, err := m.MarshalJSON() 849 | c.Assert(err, qt.IsNil) 850 | 851 | if expectExactRoundTrip { 852 | // The data is in canonical form, so we can check that 853 | // the round-tripped data is the same as the original 854 | // data when unmarshalled into an interface{}. 855 | var got interface{} 856 | err = json.Unmarshal(data, &got) 857 | c.Assert(err, qt.IsNil) 858 | 859 | var original interface{} 860 | err = json.Unmarshal([]byte(jsonData), &original) 861 | c.Assert(err, qt.IsNil) 862 | 863 | c.Assert(got, qt.DeepEquals, original, qt.Commentf("data: %s", data)) 864 | } 865 | // Check that we can unmarshal the marshaled data anyway 866 | // and the macaroon still looks the same. 867 | var m1 macaroon.Macaroon 868 | err = m1.UnmarshalJSON(data) 869 | c.Assert(err, qt.IsNil) 870 | assertLibMacaroonsMacaroon(c, &m1) 871 | c.Assert(m.Version(), qt.Equals, vers) 872 | } 873 | 874 | // assertLibMacaroonsMacaroon asserts that m looks like the macaroon 875 | // created in the README of the libmacaroons documentation. 876 | // In particular, the signature is the same one reported there. 877 | func assertLibMacaroonsMacaroon(c *qt.C, m *macaroon.Macaroon) { 878 | c.Assert(fmt.Sprintf("%x", m.Signature()), qt.Equals, 879 | "d27db2fd1f22760e4c3dae8137e2d8fc1df6c0741c18aed4b97256bf78d1f55c") 880 | c.Assert(m.Location(), qt.Equals, "http://mybank/") 881 | c.Assert(string(m.Id()), qt.Equals, "we used our other secret key") 882 | c.Assert(m.Caveats(), qt.DeepEquals, []macaroon.Caveat{{ 883 | Id: []byte("account = 3735928559"), 884 | }, { 885 | Id: []byte("this was how we remind auth of key/pred"), 886 | VerificationId: decodeB64("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr"), 887 | Location: "http://auth.mybank/", 888 | }}) 889 | } 890 | 891 | var jsonDecodeErrorTests = []struct { 892 | about string 893 | data string 894 | expectError string 895 | }{{ 896 | about: "ambiguous id #1", 897 | data: `{"i": "hello", "i64": "abcd", "s64": "ZDI3ZGIyZmQxZjIyNzYwZTRjM2RhZTgxMzdlMmQ4ZmMK"}`, 898 | expectError: "invalid identifier: ambiguous field encoding", 899 | }, { 900 | about: "ambiguous signature", 901 | data: `{"i": "hello", "s": "345", "s64": "543467"}`, 902 | expectError: "invalid signature: ambiguous field encoding", 903 | }, { 904 | about: "signature too short", 905 | data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Q"}`, 906 | expectError: "signature has unexpected length 31", 907 | }, { 908 | about: "signature too long", 909 | data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9dP1"}`, 910 | expectError: "signature has unexpected length 33", 911 | }, { 912 | about: "invalid caveat id", 913 | data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Vw", "c": [{"i": "hello", "i64": "00"}]}`, 914 | expectError: "invalid cid in caveat: ambiguous field encoding", 915 | }, { 916 | about: "invalid caveat vid", 917 | data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Vw", "c": [{"i": "hello", "v": "hello", "v64": "00"}]}`, 918 | expectError: "invalid vid in caveat: ambiguous field encoding", 919 | }} 920 | 921 | func TestJSONDecodeError(t *testing.T) { 922 | c := qt.New(t) 923 | for i, test := range jsonDecodeErrorTests { 924 | c.Logf("test %d: %v", i, test.about) 925 | var m macaroon.Macaroon 926 | err := json.Unmarshal([]byte(test.data), &m) 927 | c.Assert(err, qt.ErrorMatches, test.expectError) 928 | } 929 | } 930 | 931 | func TestFirstPartyCaveatWithInvalidUTF8(t *testing.T) { 932 | c := qt.New(t) 933 | rootKey := []byte("secret") 934 | badString := "foo\xff" 935 | 936 | m0 := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion) 937 | err := m0.AddFirstPartyCaveat([]byte(badString)) 938 | c.Assert(err, qt.Equals, nil) 939 | } 940 | 941 | func decodeB64(s string) []byte { 942 | data, err := base64.RawURLEncoding.DecodeString(s) 943 | if err != nil { 944 | panic(err) 945 | } 946 | return data 947 | } 948 | 949 | type caveat struct { 950 | rootKey string 951 | location string 952 | condition string 953 | } 954 | 955 | type macaroonSpec struct { 956 | rootKey string 957 | id string 958 | caveats []caveat 959 | location string 960 | } 961 | 962 | func makeMacaroons(mspecs []macaroonSpec) (rootKey []byte, macaroons macaroon.Slice) { 963 | for _, mspec := range mspecs { 964 | macaroons = append(macaroons, makeMacaroon(mspec)) 965 | } 966 | primary := macaroons[0] 967 | for _, m := range macaroons[1:] { 968 | m.Bind(primary.Signature()) 969 | } 970 | return []byte(mspecs[0].rootKey), macaroons 971 | } 972 | 973 | func makeMacaroon(mspec macaroonSpec) *macaroon.Macaroon { 974 | m := MustNew([]byte(mspec.rootKey), []byte(mspec.id), mspec.location, macaroon.LatestVersion) 975 | for _, cav := range mspec.caveats { 976 | if cav.location != "" { 977 | err := m.AddThirdPartyCaveat([]byte(cav.rootKey), []byte(cav.condition), cav.location) 978 | if err != nil { 979 | panic(err) 980 | } 981 | } else { 982 | m.AddFirstPartyCaveat([]byte(cav.condition)) 983 | } 984 | } 985 | return m 986 | } 987 | 988 | func assertEqualMacaroons(c *qt.C, m0, m1 *macaroon.Macaroon) { 989 | m0json, err := m0.MarshalJSON() 990 | c.Assert(err, qt.IsNil) 991 | m1json, err := m1.MarshalJSON() 992 | var m0val, m1val interface{} 993 | err = json.Unmarshal(m0json, &m0val) 994 | c.Assert(err, qt.IsNil) 995 | err = json.Unmarshal(m1json, &m1val) 996 | c.Assert(err, qt.IsNil) 997 | c.Assert(m0val, qt.DeepEquals, m1val) 998 | } 999 | 1000 | func TestBinaryRoundTrip(t *testing.T) { 1001 | c := qt.New(t) 1002 | // Test the binary marshalling and unmarshalling of a macaroon with 1003 | // first and third party caveats. 1004 | rootKey := []byte("secret") 1005 | m0 := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion) 1006 | err := m0.AddFirstPartyCaveat([]byte("first caveat")) 1007 | c.Assert(err, qt.IsNil) 1008 | err = m0.AddFirstPartyCaveat([]byte("second caveat")) 1009 | c.Assert(err, qt.IsNil) 1010 | err = m0.AddThirdPartyCaveat([]byte("shared root key"), []byte("3rd party caveat"), "remote.com") 1011 | c.Assert(err, qt.IsNil) 1012 | data, err := m0.MarshalBinary() 1013 | c.Assert(err, qt.IsNil) 1014 | var m1 macaroon.Macaroon 1015 | err = m1.UnmarshalBinary(data) 1016 | c.Assert(err, qt.IsNil) 1017 | assertEqualMacaroons(c, m0, &m1) 1018 | } 1019 | 1020 | func TestBinaryMarshalingAgainstLibmacaroon(t *testing.T) { 1021 | c := qt.New(t) 1022 | // Test that a libmacaroon marshalled macaroon can be correctly unmarshaled 1023 | data, err := base64.RawURLEncoding.DecodeString( 1024 | "MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMmNpZGVudGlmaWVyIHdlIHVzZWQgb3VyIG90aGVyIHNlY3JldCBrZXkKMDAxZGNpZCBhY2NvdW50ID0gMzczNTkyODU1OQowMDMwY2lkIHRoaXMgd2FzIGhvdyB3ZSByZW1pbmQgYXV0aCBvZiBrZXkvcHJlZAowMDUxdmlkIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANNuxQLgWIbR8CefBV-lJVTRbRbBsUB0u7g_8P3XncL-CY8O1KKwkRMOa120aiCoawowMDFiY2wgaHR0cDovL2F1dGgubXliYW5rLwowMDJmc2lnbmF0dXJlINJ9sv0fInYOTD2ugTfi2Pwd9sB0HBiu1LlyVr940fVcCg") 1025 | c.Assert(err, qt.IsNil) 1026 | var m0 macaroon.Macaroon 1027 | err = m0.UnmarshalBinary(data) 1028 | c.Assert(err, qt.IsNil) 1029 | jsonData := []byte(`{"caveats":[{"cid":"account = 3735928559"},{"cid":"this was how we remind auth of key\/pred","vid":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr","cl":"http:\/\/auth.mybank\/"}],"location":"http:\/\/mybank\/","identifier":"we used our other secret key","signature":"d27db2fd1f22760e4c3dae8137e2d8fc1df6c0741c18aed4b97256bf78d1f55c"}`) 1030 | var m1 macaroon.Macaroon 1031 | err = m1.UnmarshalJSON(jsonData) 1032 | c.Assert(err, qt.IsNil) 1033 | assertEqualMacaroons(c, &m0, &m1) 1034 | } 1035 | 1036 | var binaryFieldBase64ChoiceTests = []struct { 1037 | id string 1038 | expectBase64 bool 1039 | }{ 1040 | {"x", false}, 1041 | {"\x00", true}, 1042 | {"\x03\x00", true}, 1043 | {"a longer id with more stuff", false}, 1044 | {"a longer id with more stuff and one invalid \xff", true}, 1045 | {"a longer id with more stuff and one encoded \x00", false}, 1046 | } 1047 | 1048 | func TestBinaryFieldBase64Choice(t *testing.T) { 1049 | c := qt.New(t) 1050 | for i, test := range binaryFieldBase64ChoiceTests { 1051 | c.Logf("test %d: %q", i, test.id) 1052 | m := MustNew([]byte{0}, []byte(test.id), "", macaroon.LatestVersion) 1053 | data, err := json.Marshal(m) 1054 | c.Assert(err, qt.Equals, nil) 1055 | var x struct { 1056 | Id *string `json:"i"` 1057 | Id64 *string `json:"i64"` 1058 | } 1059 | err = json.Unmarshal(data, &x) 1060 | c.Assert(err, qt.Equals, nil) 1061 | if test.expectBase64 { 1062 | c.Assert(x.Id64, qt.Not(qt.IsNil)) 1063 | c.Assert(x.Id, qt.IsNil) 1064 | idDec, err := base64.RawURLEncoding.DecodeString(*x.Id64) 1065 | c.Assert(err, qt.Equals, nil) 1066 | c.Assert(string(idDec), qt.Equals, test.id) 1067 | } else { 1068 | c.Assert(x.Id64, qt.IsNil) 1069 | c.Assert(x.Id, qt.Not(qt.IsNil)) 1070 | c.Assert(*x.Id, qt.Equals, test.id) 1071 | } 1072 | } 1073 | } 1074 | -------------------------------------------------------------------------------- /marshal-v1.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "unicode/utf8" 9 | ) 10 | 11 | // macaroonJSONV1 defines the V1 JSON format for macaroons. 12 | type macaroonJSONV1 struct { 13 | Caveats []caveatJSONV1 `json:"caveats"` 14 | Location string `json:"location"` 15 | Identifier string `json:"identifier"` 16 | Signature string `json:"signature"` // hex-encoded 17 | } 18 | 19 | // caveatJSONV1 defines the V1 JSON format for caveats within a macaroon. 20 | type caveatJSONV1 struct { 21 | CID string `json:"cid"` 22 | VID string `json:"vid,omitempty"` 23 | Location string `json:"cl,omitempty"` 24 | } 25 | 26 | // marshalJSONV1 marshals the macaroon to the V1 JSON format. 27 | func (m *Macaroon) marshalJSONV1() ([]byte, error) { 28 | if !utf8.Valid(m.id) { 29 | return nil, fmt.Errorf("macaroon id is not valid UTF-8") 30 | } 31 | mjson := macaroonJSONV1{ 32 | Location: m.location, 33 | Identifier: string(m.id), 34 | Signature: hex.EncodeToString(m.sig[:]), 35 | Caveats: make([]caveatJSONV1, len(m.caveats)), 36 | } 37 | for i, cav := range m.caveats { 38 | if !utf8.Valid(cav.Id) { 39 | return nil, fmt.Errorf("caveat id is not valid UTF-8") 40 | } 41 | mjson.Caveats[i] = caveatJSONV1{ 42 | Location: cav.Location, 43 | CID: string(cav.Id), 44 | VID: base64.RawURLEncoding.EncodeToString(cav.VerificationId), 45 | } 46 | } 47 | data, err := json.Marshal(mjson) 48 | if err != nil { 49 | return nil, fmt.Errorf("cannot marshal json data: %v", err) 50 | } 51 | return data, nil 52 | } 53 | 54 | // initJSONV1 initializes m from the JSON-unmarshaled data 55 | // held in mjson. 56 | func (m *Macaroon) initJSONV1(mjson *macaroonJSONV1) error { 57 | m.init([]byte(mjson.Identifier), mjson.Location, V1) 58 | sig, err := hex.DecodeString(mjson.Signature) 59 | if err != nil { 60 | return fmt.Errorf("cannot decode macaroon signature %q: %v", m.sig, err) 61 | } 62 | if len(sig) != hashLen { 63 | return fmt.Errorf("signature has unexpected length %d", len(sig)) 64 | } 65 | copy(m.sig[:], sig) 66 | m.caveats = m.caveats[:0] 67 | for _, cav := range mjson.Caveats { 68 | vid, err := Base64Decode([]byte(cav.VID)) 69 | if err != nil { 70 | return fmt.Errorf("cannot decode verification id %q: %v", cav.VID, err) 71 | } 72 | m.appendCaveat([]byte(cav.CID), vid, cav.Location) 73 | } 74 | return nil 75 | } 76 | 77 | // The original (v1) binary format of a macaroon is as follows. 78 | // Each identifier represents a v1 packet. 79 | // 80 | // location 81 | // identifier 82 | // ( 83 | // caveatId? 84 | // verificationId? 85 | // caveatLocation? 86 | // )* 87 | // signature 88 | 89 | // parseBinaryV1 parses the given data in V1 format into the macaroon. The macaroon's 90 | // internal data structures will retain references to the data. It 91 | // returns the data after the end of the macaroon. 92 | func (m *Macaroon) parseBinaryV1(data []byte) ([]byte, error) { 93 | var err error 94 | 95 | loc, err := expectPacketV1(data, fieldNameLocation) 96 | if err != nil { 97 | return nil, err 98 | } 99 | data = data[loc.totalLen:] 100 | id, err := expectPacketV1(data, fieldNameIdentifier) 101 | if err != nil { 102 | return nil, err 103 | } 104 | data = data[id.totalLen:] 105 | m.init(id.data, string(loc.data), V1) 106 | var cav Caveat 107 | for { 108 | p, err := parsePacketV1(data) 109 | if err != nil { 110 | return nil, err 111 | } 112 | data = data[p.totalLen:] 113 | switch field := string(p.fieldName); field { 114 | case fieldNameSignature: 115 | // At the end of the caveats we find the signature. 116 | if cav.Id != nil { 117 | m.caveats = append(m.caveats, cav) 118 | } 119 | if len(p.data) != hashLen { 120 | return nil, fmt.Errorf("signature has unexpected length %d", len(p.data)) 121 | } 122 | copy(m.sig[:], p.data) 123 | return data, nil 124 | case fieldNameCaveatId: 125 | if cav.Id != nil { 126 | m.caveats = append(m.caveats, cav) 127 | cav = Caveat{} 128 | } 129 | cav.Id = p.data 130 | case fieldNameVerificationId: 131 | if cav.VerificationId != nil { 132 | return nil, fmt.Errorf("repeated field %q in caveat", fieldNameVerificationId) 133 | } 134 | cav.VerificationId = p.data 135 | case fieldNameCaveatLocation: 136 | if cav.Location != "" { 137 | return nil, fmt.Errorf("repeated field %q in caveat", fieldNameLocation) 138 | } 139 | cav.Location = string(p.data) 140 | default: 141 | return nil, fmt.Errorf("unexpected field %q", field) 142 | } 143 | } 144 | } 145 | 146 | func expectPacketV1(data []byte, kind string) (packetV1, error) { 147 | p, err := parsePacketV1(data) 148 | if err != nil { 149 | return packetV1{}, err 150 | } 151 | if field := string(p.fieldName); field != kind { 152 | return packetV1{}, fmt.Errorf("unexpected field %q; expected %s", field, kind) 153 | } 154 | return p, nil 155 | } 156 | 157 | // appendBinaryV1 appends the binary encoding of m to data. 158 | func (m *Macaroon) appendBinaryV1(data []byte) ([]byte, error) { 159 | var ok bool 160 | data, ok = appendPacketV1(data, fieldNameLocation, []byte(m.location)) 161 | if !ok { 162 | return nil, fmt.Errorf("failed to append location to macaroon, packet is too long") 163 | } 164 | data, ok = appendPacketV1(data, fieldNameIdentifier, m.id) 165 | if !ok { 166 | return nil, fmt.Errorf("failed to append identifier to macaroon, packet is too long") 167 | } 168 | for _, cav := range m.caveats { 169 | data, ok = appendPacketV1(data, fieldNameCaveatId, cav.Id) 170 | if !ok { 171 | return nil, fmt.Errorf("failed to append caveat id to macaroon, packet is too long") 172 | } 173 | if cav.VerificationId == nil { 174 | continue 175 | } 176 | data, ok = appendPacketV1(data, fieldNameVerificationId, cav.VerificationId) 177 | if !ok { 178 | return nil, fmt.Errorf("failed to append verification id to macaroon, packet is too long") 179 | } 180 | data, ok = appendPacketV1(data, fieldNameCaveatLocation, []byte(cav.Location)) 181 | if !ok { 182 | return nil, fmt.Errorf("failed to append verification id to macaroon, packet is too long") 183 | } 184 | } 185 | data, ok = appendPacketV1(data, fieldNameSignature, m.sig[:]) 186 | if !ok { 187 | return nil, fmt.Errorf("failed to append signature to macaroon, packet is too long") 188 | } 189 | return data, nil 190 | } 191 | -------------------------------------------------------------------------------- /marshal-v2.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "unicode/utf8" 8 | ) 9 | 10 | // macaroonJSONV2 defines the V2 JSON format for macaroons. 11 | type macaroonJSONV2 struct { 12 | Caveats []caveatJSONV2 `json:"c,omitempty"` 13 | Location string `json:"l,omitempty"` 14 | Identifier string `json:"i,omitempty"` 15 | Identifier64 string `json:"i64,omitempty"` 16 | Signature string `json:"s,omitempty"` 17 | Signature64 string `json:"s64,omitempty"` 18 | } 19 | 20 | // caveatJSONV2 defines the V2 JSON format for caveats within a macaroon. 21 | type caveatJSONV2 struct { 22 | CID string `json:"i,omitempty"` 23 | CID64 string `json:"i64,omitempty"` 24 | VID string `json:"v,omitempty"` 25 | VID64 string `json:"v64,omitempty"` 26 | Location string `json:"l,omitempty"` 27 | } 28 | 29 | func (m *Macaroon) marshalJSONV2() ([]byte, error) { 30 | mjson := macaroonJSONV2{ 31 | Location: m.location, 32 | Caveats: make([]caveatJSONV2, len(m.caveats)), 33 | } 34 | putJSONBinaryField(m.id, &mjson.Identifier, &mjson.Identifier64) 35 | putJSONBinaryField(m.sig[:], &mjson.Signature, &mjson.Signature64) 36 | for i, cav := range m.caveats { 37 | cavjson := caveatJSONV2{ 38 | Location: cav.Location, 39 | } 40 | putJSONBinaryField(cav.Id, &cavjson.CID, &cavjson.CID64) 41 | putJSONBinaryField(cav.VerificationId, &cavjson.VID, &cavjson.VID64) 42 | mjson.Caveats[i] = cavjson 43 | } 44 | data, err := json.Marshal(mjson) 45 | if err != nil { 46 | return nil, fmt.Errorf("cannot marshal json data: %v", err) 47 | } 48 | return data, nil 49 | } 50 | 51 | // initJSONV2 initializes m from the JSON-unmarshaled data 52 | // held in mjson. 53 | func (m *Macaroon) initJSONV2(mjson *macaroonJSONV2) error { 54 | id, err := jsonBinaryField(mjson.Identifier, mjson.Identifier64) 55 | if err != nil { 56 | return fmt.Errorf("invalid identifier: %v", err) 57 | } 58 | m.init(id, mjson.Location, V2) 59 | sig, err := jsonBinaryField(mjson.Signature, mjson.Signature64) 60 | if err != nil { 61 | return fmt.Errorf("invalid signature: %v", err) 62 | } 63 | if len(sig) != hashLen { 64 | return fmt.Errorf("signature has unexpected length %d", len(sig)) 65 | } 66 | copy(m.sig[:], sig) 67 | m.caveats = make([]Caveat, 0, len(mjson.Caveats)) 68 | for _, cav := range mjson.Caveats { 69 | cid, err := jsonBinaryField(cav.CID, cav.CID64) 70 | if err != nil { 71 | return fmt.Errorf("invalid cid in caveat: %v", err) 72 | } 73 | vid, err := jsonBinaryField(cav.VID, cav.VID64) 74 | if err != nil { 75 | return fmt.Errorf("invalid vid in caveat: %v", err) 76 | } 77 | m.appendCaveat(cid, vid, cav.Location) 78 | } 79 | return nil 80 | } 81 | 82 | // putJSONBinaryField puts the value of x into one 83 | // of the appropriate fields depending on its value. 84 | func putJSONBinaryField(x []byte, s, sb64 *string) { 85 | if !utf8.Valid(x) { 86 | *sb64 = base64.RawURLEncoding.EncodeToString(x) 87 | return 88 | } 89 | // We could use either string or base64 encoding; 90 | // choose the most compact of the two possibilities. 91 | b64len := base64.RawURLEncoding.EncodedLen(len(x)) 92 | sx := string(x) 93 | if jsonEnc, _ := json.Marshal(sx); len(jsonEnc)-2 <= b64len+2 { 94 | // The JSON encoding is smaller than the base 64 encoding. 95 | // NB marshaling a string can never return an error; 96 | // it always includes the two quote characters; 97 | // but using base64 also uses two extra characters for the 98 | // "64" suffix on the field name. If all is equal, prefer string 99 | // encoding because it's more readable. 100 | *s = sx 101 | return 102 | } 103 | *sb64 = base64.RawURLEncoding.EncodeToString(x) 104 | } 105 | 106 | // jsonBinaryField returns the value of a JSON field that may 107 | // be string, hex or base64-encoded. 108 | func jsonBinaryField(s, sb64 string) ([]byte, error) { 109 | switch { 110 | case s != "": 111 | if sb64 != "" { 112 | return nil, fmt.Errorf("ambiguous field encoding") 113 | } 114 | return []byte(s), nil 115 | case sb64 != "": 116 | return Base64Decode([]byte(sb64)) 117 | } 118 | return []byte{}, nil 119 | } 120 | 121 | // The v2 binary format of a macaroon is as follows. 122 | // All entries other than the version are packets as 123 | // parsed by parsePacketV2. 124 | // 125 | // version [1 byte] 126 | // location? 127 | // identifier 128 | // eos 129 | // ( 130 | // location? 131 | // identifier 132 | // verificationId? 133 | // eos 134 | // )* 135 | // eos 136 | // signature 137 | // 138 | // See also https://github.com/rescrv/libmacaroons/blob/master/doc/format.txt 139 | 140 | // parseBinaryV2 parses the given data in V2 format into the macaroon. The macaroon's 141 | // internal data structures will retain references to the data. It 142 | // returns the data after the end of the macaroon. 143 | func (m *Macaroon) parseBinaryV2(data []byte) ([]byte, error) { 144 | // The version has already been checked, so 145 | // skip it. 146 | data = data[1:] 147 | 148 | data, section, err := parseSectionV2(data) 149 | if err != nil { 150 | return nil, err 151 | } 152 | var loc string 153 | if len(section) > 0 && section[0].fieldType == fieldLocation { 154 | loc = string(section[0].data) 155 | section = section[1:] 156 | } 157 | if len(section) != 1 || section[0].fieldType != fieldIdentifier { 158 | return nil, fmt.Errorf("invalid macaroon header") 159 | } 160 | id := section[0].data 161 | m.init(id, loc, V2) 162 | for { 163 | rest, section, err := parseSectionV2(data) 164 | if err != nil { 165 | return nil, err 166 | } 167 | data = rest 168 | if len(section) == 0 { 169 | break 170 | } 171 | var cav Caveat 172 | if len(section) > 0 && section[0].fieldType == fieldLocation { 173 | cav.Location = string(section[0].data) 174 | section = section[1:] 175 | } 176 | if len(section) == 0 || section[0].fieldType != fieldIdentifier { 177 | return nil, fmt.Errorf("no identifier in caveat") 178 | } 179 | cav.Id = section[0].data 180 | section = section[1:] 181 | if len(section) == 0 { 182 | // First party caveat. 183 | if cav.Location != "" { 184 | return nil, fmt.Errorf("location not allowed in first party caveat") 185 | } 186 | m.caveats = append(m.caveats, cav) 187 | continue 188 | } 189 | if len(section) != 1 { 190 | return nil, fmt.Errorf("extra fields found in caveat") 191 | } 192 | if section[0].fieldType != fieldVerificationId { 193 | return nil, fmt.Errorf("invalid field found in caveat") 194 | } 195 | cav.VerificationId = section[0].data 196 | m.caveats = append(m.caveats, cav) 197 | } 198 | data, sig, err := parsePacketV2(data) 199 | if err != nil { 200 | return nil, err 201 | } 202 | if sig.fieldType != fieldSignature { 203 | return nil, fmt.Errorf("unexpected field found instead of signature") 204 | } 205 | if len(sig.data) != hashLen { 206 | return nil, fmt.Errorf("signature has unexpected length") 207 | } 208 | copy(m.sig[:], sig.data) 209 | return data, nil 210 | } 211 | 212 | // appendBinaryV2 appends the binary-encoded macaroon 213 | // in v2 format to data. 214 | func (m *Macaroon) appendBinaryV2(data []byte) []byte { 215 | // Version byte. 216 | data = append(data, 2) 217 | if len(m.location) > 0 { 218 | data = appendPacketV2(data, packetV2{ 219 | fieldType: fieldLocation, 220 | data: []byte(m.location), 221 | }) 222 | } 223 | data = appendPacketV2(data, packetV2{ 224 | fieldType: fieldIdentifier, 225 | data: m.id, 226 | }) 227 | data = appendEOSV2(data) 228 | for _, cav := range m.caveats { 229 | if len(cav.Location) > 0 { 230 | data = appendPacketV2(data, packetV2{ 231 | fieldType: fieldLocation, 232 | data: []byte(cav.Location), 233 | }) 234 | } 235 | data = appendPacketV2(data, packetV2{ 236 | fieldType: fieldIdentifier, 237 | data: cav.Id, 238 | }) 239 | if len(cav.VerificationId) > 0 { 240 | data = appendPacketV2(data, packetV2{ 241 | fieldType: fieldVerificationId, 242 | data: []byte(cav.VerificationId), 243 | }) 244 | } 245 | data = appendEOSV2(data) 246 | } 247 | data = appendEOSV2(data) 248 | data = appendPacketV2(data, packetV2{ 249 | fieldType: fieldSignature, 250 | data: m.sig[:], 251 | }) 252 | return data 253 | } 254 | -------------------------------------------------------------------------------- /marshal.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // Version specifies the version of a macaroon. 10 | // In version 1, the macaroon id and all caveats 11 | // must be UTF-8-compatible strings, and the 12 | // size of any part of the macaroon may not exceed 13 | // approximately 64K. In version 2, 14 | // all field may be arbitrary binary blobs. 15 | type Version uint16 16 | 17 | const ( 18 | // V1 specifies version 1 macaroons. 19 | V1 Version = 1 20 | 21 | // V2 specifies version 2 macaroons. 22 | V2 Version = 2 23 | 24 | // LatestVersion holds the latest supported version. 25 | LatestVersion = V2 26 | ) 27 | 28 | // String returns a string representation of the version; 29 | // for example V1 formats as "v1". 30 | func (v Version) String() string { 31 | return fmt.Sprintf("v%d", v) 32 | } 33 | 34 | // Version returns the version of the macaroon. 35 | func (m *Macaroon) Version() Version { 36 | return m.version 37 | } 38 | 39 | // MarshalJSON implements json.Marshaler by marshaling the 40 | // macaroon in JSON format. The serialisation format is determined 41 | // by the macaroon's version. 42 | func (m *Macaroon) MarshalJSON() ([]byte, error) { 43 | switch m.version { 44 | case V1: 45 | return m.marshalJSONV1() 46 | case V2: 47 | return m.marshalJSONV2() 48 | default: 49 | return nil, fmt.Errorf("unknown version %v", m.version) 50 | } 51 | } 52 | 53 | // UnmarshalJSON implements json.Unmarshaller by unmarshaling 54 | // the given macaroon in JSON format. It accepts both V1 and V2 55 | // forms encoded forms, and also a base64-encoded JSON string 56 | // containing the binary-marshaled macaroon. 57 | // 58 | // After unmarshaling, the macaroon's version will reflect 59 | // the version that it was unmarshaled as. 60 | func (m *Macaroon) UnmarshalJSON(data []byte) error { 61 | if data[0] == '"' { 62 | // It's a string, so it must be a base64-encoded binary form. 63 | var s string 64 | if err := json.Unmarshal(data, &s); err != nil { 65 | return err 66 | } 67 | data, err := Base64Decode([]byte(s)) 68 | if err != nil { 69 | return err 70 | } 71 | if err := m.UnmarshalBinary(data); err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | // Not a string; try to unmarshal into both kinds of macaroon object. 77 | // This assumes that neither format has any fields in common. 78 | // For subsequent versions we may need to change this approach. 79 | type MacaroonJSONV1 macaroonJSONV1 80 | type MacaroonJSONV2 macaroonJSONV2 81 | var both struct { 82 | *MacaroonJSONV1 83 | *MacaroonJSONV2 84 | } 85 | if err := json.Unmarshal(data, &both); err != nil { 86 | return err 87 | } 88 | switch { 89 | case both.MacaroonJSONV1 != nil && both.MacaroonJSONV2 != nil: 90 | return fmt.Errorf("cannot determine macaroon encoding version") 91 | case both.MacaroonJSONV1 != nil: 92 | if err := m.initJSONV1((*macaroonJSONV1)(both.MacaroonJSONV1)); err != nil { 93 | return err 94 | } 95 | m.version = V1 96 | case both.MacaroonJSONV2 != nil: 97 | if err := m.initJSONV2((*macaroonJSONV2)(both.MacaroonJSONV2)); err != nil { 98 | return err 99 | } 100 | m.version = V2 101 | default: 102 | return fmt.Errorf("invalid JSON macaroon encoding") 103 | } 104 | return nil 105 | } 106 | 107 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 108 | // It accepts both V1 and V2 binary encodings. 109 | func (m *Macaroon) UnmarshalBinary(data []byte) error { 110 | // Copy the data to avoid retaining references to it 111 | // in the internal data structures. 112 | data = append([]byte(nil), data...) 113 | _, err := m.parseBinary(data) 114 | return err 115 | } 116 | 117 | // parseBinary parses the macaroon in binary format 118 | // from the given data and returns where the parsed data ends. 119 | // 120 | // It retains references to data. 121 | func (m *Macaroon) parseBinary(data []byte) ([]byte, error) { 122 | if len(data) == 0 { 123 | return nil, fmt.Errorf("empty macaroon data") 124 | } 125 | v := data[0] 126 | if v == 2 { 127 | // Version 2 binary format. 128 | data, err := m.parseBinaryV2(data) 129 | if err != nil { 130 | return nil, fmt.Errorf("unmarshal v2: %v", err) 131 | } 132 | m.version = V2 133 | return data, nil 134 | } 135 | if isASCIIHex(v) { 136 | // It's a hex digit - version 1 binary format 137 | data, err := m.parseBinaryV1(data) 138 | if err != nil { 139 | return nil, fmt.Errorf("unmarshal v1: %v", err) 140 | } 141 | m.version = V1 142 | return data, nil 143 | } 144 | return nil, fmt.Errorf("cannot determine data format of binary-encoded macaroon") 145 | } 146 | 147 | // MarshalBinary implements encoding.BinaryMarshaler by 148 | // formatting the macaroon according to the version specified 149 | // by MarshalAs. 150 | func (m *Macaroon) MarshalBinary() ([]byte, error) { 151 | return m.appendBinary(nil) 152 | } 153 | 154 | // appendBinary appends the binary-formatted macaroon to 155 | // the given data, formatting it according to the macaroon's 156 | // version. 157 | func (m *Macaroon) appendBinary(data []byte) ([]byte, error) { 158 | switch m.version { 159 | case V1: 160 | return m.appendBinaryV1(data) 161 | case V2: 162 | return m.appendBinaryV2(data), nil 163 | default: 164 | return nil, fmt.Errorf("bad macaroon version %v", m.version) 165 | } 166 | } 167 | 168 | // Slice defines a collection of macaroons. By convention, the 169 | // first macaroon in the slice is a primary macaroon and the rest 170 | // are discharges for its third party caveats. 171 | type Slice []*Macaroon 172 | 173 | // MarshalBinary implements encoding.BinaryMarshaler. 174 | func (s Slice) MarshalBinary() ([]byte, error) { 175 | var data []byte 176 | var err error 177 | for _, m := range s { 178 | data, err = m.appendBinary(data) 179 | if err != nil { 180 | return nil, fmt.Errorf("failed to marshal macaroon %q: %v", m.Id(), err) 181 | } 182 | } 183 | return data, nil 184 | } 185 | 186 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 187 | // It accepts all known binary encodings for the data - all the 188 | // embedded macaroons need not be encoded in the same format. 189 | func (s *Slice) UnmarshalBinary(data []byte) error { 190 | // Prevent the internal data structures from holding onto the 191 | // slice by copying it first. 192 | data = append([]byte(nil), data...) 193 | *s = (*s)[:0] 194 | for len(data) > 0 { 195 | var m Macaroon 196 | rest, err := m.parseBinary(data) 197 | if err != nil { 198 | return fmt.Errorf("cannot unmarshal macaroon: %v", err) 199 | } 200 | *s = append(*s, &m) 201 | data = rest 202 | } 203 | return nil 204 | } 205 | 206 | const ( 207 | padded = 1 << iota 208 | stdEncoding 209 | ) 210 | 211 | var codecs = [4]*base64.Encoding{ 212 | 0: base64.RawURLEncoding, 213 | padded: base64.URLEncoding, 214 | stdEncoding: base64.RawStdEncoding, 215 | stdEncoding | padded: base64.StdEncoding, 216 | } 217 | 218 | // Base64Decode base64-decodes the given data. 219 | // It accepts both standard and URL encodings, both 220 | // padded and unpadded. 221 | func Base64Decode(data []byte) ([]byte, error) { 222 | encoding := 0 223 | if len(data) > 0 && data[len(data)-1] == '=' { 224 | encoding |= padded 225 | } 226 | for _, b := range data { 227 | if b == '/' || b == '+' { 228 | encoding |= stdEncoding 229 | break 230 | } 231 | } 232 | codec := codecs[encoding] 233 | buf := make([]byte, codec.DecodedLen(len(data))) 234 | n, err := codec.Decode(buf, data) 235 | if err == nil { 236 | return buf[0:n], nil 237 | } 238 | return nil, err 239 | } 240 | -------------------------------------------------------------------------------- /marshal_test.go: -------------------------------------------------------------------------------- 1 | package macaroon_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | 9 | "gopkg.in/macaroon.v2" 10 | ) 11 | 12 | func TestMarshalUnmarshalMacaroonV1(t *testing.T) { 13 | c := qt.New(t) 14 | testMarshalUnmarshalWithVersion(c, macaroon.V1) 15 | } 16 | 17 | func TestMarshalUnmarshalMacaroonV2(t *testing.T) { 18 | c := qt.New(t) 19 | testMarshalUnmarshalWithVersion(c, macaroon.V2) 20 | } 21 | 22 | func testMarshalUnmarshalWithVersion(c *qt.C, vers macaroon.Version) { 23 | rootKey := []byte("secret") 24 | m := MustNew(rootKey, []byte("some id"), "a location", vers) 25 | 26 | // Adding the third party caveat before the first party caveat 27 | // tests a former bug where the caveat wasn't zeroed 28 | // before moving to the next caveat. 29 | err := m.AddThirdPartyCaveat([]byte("shared root key"), []byte("3rd party caveat"), "remote.com") 30 | c.Assert(err, qt.Equals, nil) 31 | 32 | err = m.AddFirstPartyCaveat([]byte("a caveat")) 33 | c.Assert(err, qt.Equals, nil) 34 | 35 | b, err := m.MarshalBinary() 36 | c.Assert(err, qt.Equals, nil) 37 | 38 | var um macaroon.Macaroon 39 | err = um.UnmarshalBinary(b) 40 | c.Assert(err, qt.Equals, nil) 41 | 42 | c.Assert(um.Location(), qt.Equals, m.Location()) 43 | c.Assert(string(um.Id()), qt.Equals, string(m.Id())) 44 | c.Assert(um.Signature(), qt.DeepEquals, m.Signature()) 45 | c.Assert(um.Caveats(), qt.DeepEquals, m.Caveats()) 46 | c.Assert(um.Version(), qt.Equals, vers) 47 | um.SetVersion(m.Version()) 48 | c.Assert(m, qt.DeepEquals, &um) 49 | } 50 | 51 | func TestMarshalBinaryRoundTrip(t *testing.T) { 52 | c := qt.New(t) 53 | // This data holds the V2 binary encoding of 54 | data := []byte( 55 | "\x02" + 56 | "\x01\x0ehttp://mybank/" + 57 | "\x02\x1cwe used our other secret key" + 58 | "\x00" + 59 | "\x02\x14account = 3735928559" + 60 | "\x00" + 61 | "\x01\x13http://auth.mybank/" + 62 | "\x02'this was how we remind auth of key/pred" + 63 | "\x04\x48\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd3\x6e\xc5\x02\xe0\x58\x86\xd1\xf0\x27\x9f\x05\x5f\xa5\x25\x54\xd1\x6d\x16\xc1\xb1\x40\x74\xbb\xb8\x3f\xf0\xfd\xd7\x9d\xc2\xfe\x09\x8f\x0e\xd4\xa2\xb0\x91\x13\x0e\x6b\x5d\xb4\x6a\x20\xa8\x6b" + 64 | "\x00" + 65 | "\x00" + 66 | "\x06\x20\xd2\x7d\xb2\xfd\x1f\x22\x76\x0e\x4c\x3d\xae\x81\x37\xe2\xd8\xfc\x1d\xf6\xc0\x74\x1c\x18\xae\xd4\xb9\x72\x56\xbf\x78\xd1\xf5\x5c", 67 | ) 68 | var m macaroon.Macaroon 69 | err := m.UnmarshalBinary(data) 70 | c.Assert(err, qt.Equals, nil) 71 | assertLibMacaroonsMacaroon(c, &m) 72 | c.Assert(m.Version(), qt.Equals, macaroon.V2) 73 | 74 | data1, err := m.MarshalBinary() 75 | c.Assert(err, qt.Equals, nil) 76 | c.Assert(data1, qt.DeepEquals, data) 77 | } 78 | 79 | func TestBinaryJSONRoundTripV1(t *testing.T) { 80 | c := qt.New(t) 81 | testBinaryJSONRoundTrip(c, macaroon.V1) 82 | } 83 | 84 | func TestBinaryJSONRoundTripV2(t *testing.T) { 85 | c := qt.New(t) 86 | testBinaryJSONRoundTrip(c, macaroon.V2) 87 | } 88 | 89 | func testBinaryJSONRoundTrip(c *qt.C, vers macaroon.Version) { 90 | m1 := MustNew([]byte("rootkey"), []byte("some id"), "a location", vers) 91 | err := m1.AddFirstPartyCaveat([]byte("a caveat")) 92 | c.Assert(err, qt.Equals, nil) 93 | err = m1.AddThirdPartyCaveat([]byte("shared root key"), []byte("3rd party caveat"), "remote.com") 94 | c.Assert(err, qt.Equals, nil) 95 | 96 | binData1, err := m1.MarshalBinary() 97 | c.Assert(err, qt.Equals, nil) 98 | 99 | jsonData1, err := json.Marshal(m1) 100 | c.Assert(err, qt.Equals, nil) 101 | 102 | var m2 *macaroon.Macaroon 103 | err = json.Unmarshal(jsonData1, &m2) 104 | c.Assert(err, qt.Equals, nil) 105 | 106 | binData2, err := m2.MarshalBinary() 107 | c.Assert(err, qt.Equals, nil) 108 | 109 | c.Assert(binData1, qt.DeepEquals, binData2) 110 | } 111 | 112 | func TestMarshalUnmarshalSliceV1(t *testing.T) { 113 | c := qt.New(t) 114 | testMarshalUnmarshalSliceWithVersion(c, macaroon.V1) 115 | } 116 | 117 | func TestMarshalUnmarshalSliceV2(t *testing.T) { 118 | c := qt.New(t) 119 | testMarshalUnmarshalSliceWithVersion(c, macaroon.V2) 120 | } 121 | 122 | func testMarshalUnmarshalSliceWithVersion(c *qt.C, vers macaroon.Version) { 123 | rootKey := []byte("secret") 124 | m1 := MustNew(rootKey, []byte("some id"), "a location", vers) 125 | m2 := MustNew(rootKey, []byte("some other id"), "another location", vers) 126 | 127 | err := m1.AddFirstPartyCaveat([]byte("a caveat")) 128 | c.Assert(err, qt.Equals, nil) 129 | err = m2.AddFirstPartyCaveat([]byte("another caveat")) 130 | c.Assert(err, qt.Equals, nil) 131 | 132 | macaroons := macaroon.Slice{m1, m2} 133 | 134 | b, err := macaroons.MarshalBinary() 135 | c.Assert(err, qt.Equals, nil) 136 | 137 | var unmarshaledMacs macaroon.Slice 138 | err = unmarshaledMacs.UnmarshalBinary(b) 139 | c.Assert(err, qt.Equals, nil) 140 | 141 | c.Assert(unmarshaledMacs, qt.HasLen, len(macaroons)) 142 | for i, m := range macaroons { 143 | um := unmarshaledMacs[i] 144 | c.Assert(um.Location(), qt.Equals, m.Location()) 145 | c.Assert(string(um.Id()), qt.Equals, string(m.Id())) 146 | c.Assert(um.Signature(), qt.DeepEquals, m.Signature()) 147 | c.Assert(um.Caveats(), qt.DeepEquals, m.Caveats()) 148 | c.Assert(um.Version(), qt.Equals, vers) 149 | um.SetVersion(m.Version()) 150 | } 151 | c.Assert(macaroons, qt.DeepEquals, unmarshaledMacs) 152 | 153 | // Check that appending a caveat to the first does not 154 | // affect the second. 155 | for i := 0; i < 10; i++ { 156 | err = unmarshaledMacs[0].AddFirstPartyCaveat([]byte("caveat")) 157 | c.Assert(err, qt.Equals, nil) 158 | } 159 | unmarshaledMacs[1].SetVersion(macaroons[1].Version()) 160 | c.Assert(unmarshaledMacs[1], qt.DeepEquals, macaroons[1]) 161 | c.Assert(err, qt.Equals, nil) 162 | } 163 | 164 | func TestSliceRoundTripV1(t *testing.T) { 165 | c := qt.New(t) 166 | testSliceRoundTripWithVersion(c, macaroon.V1) 167 | } 168 | 169 | func TestSliceRoundTripV2(t *testing.T) { 170 | c := qt.New(t) 171 | testSliceRoundTripWithVersion(c, macaroon.V2) 172 | } 173 | 174 | func testSliceRoundTripWithVersion(c *qt.C, vers macaroon.Version) { 175 | rootKey := []byte("secret") 176 | m1 := MustNew(rootKey, []byte("some id"), "a location", vers) 177 | m2 := MustNew(rootKey, []byte("some other id"), "another location", vers) 178 | 179 | err := m1.AddFirstPartyCaveat([]byte("a caveat")) 180 | c.Assert(err, qt.Equals, nil) 181 | err = m2.AddFirstPartyCaveat([]byte("another caveat")) 182 | c.Assert(err, qt.Equals, nil) 183 | 184 | macaroons := macaroon.Slice{m1, m2} 185 | 186 | b, err := macaroons.MarshalBinary() 187 | c.Assert(err, qt.Equals, nil) 188 | 189 | var unmarshaledMacs macaroon.Slice 190 | err = unmarshaledMacs.UnmarshalBinary(b) 191 | c.Assert(err, qt.Equals, nil) 192 | 193 | marshaledMacs, err := unmarshaledMacs.MarshalBinary() 194 | c.Assert(err, qt.Equals, nil) 195 | 196 | c.Assert(b, qt.DeepEquals, marshaledMacs) 197 | } 198 | 199 | var base64DecodeTests = []struct { 200 | about string 201 | input string 202 | expect string 203 | expectError string 204 | }{{ 205 | about: "empty string", 206 | input: "", 207 | expect: "", 208 | }, { 209 | about: "standard encoding, padded", 210 | input: "Z29+IQ==", 211 | expect: "go~!", 212 | }, { 213 | about: "URL encoding, padded", 214 | input: "Z29-IQ==", 215 | expect: "go~!", 216 | }, { 217 | about: "standard encoding, not padded", 218 | input: "Z29+IQ", 219 | expect: "go~!", 220 | }, { 221 | about: "URL encoding, not padded", 222 | input: "Z29-IQ", 223 | expect: "go~!", 224 | }, { 225 | about: "standard encoding, too much padding", 226 | input: "Z29+IQ===", 227 | expectError: `illegal base64 data at input byte 8`, 228 | }} 229 | 230 | func TestBase64Decode(t *testing.T) { 231 | c := qt.New(t) 232 | for i, test := range base64DecodeTests { 233 | c.Logf("test %d: %s", i, test.about) 234 | out, err := macaroon.Base64Decode([]byte(test.input)) 235 | if test.expectError != "" { 236 | c.Assert(err, qt.ErrorMatches, test.expectError) 237 | } else { 238 | c.Assert(err, qt.Equals, nil) 239 | c.Assert(string(out), qt.Equals, test.expect) 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /packet-v1.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | // field names, as defined in libmacaroons 9 | const ( 10 | fieldNameLocation = "location" 11 | fieldNameIdentifier = "identifier" 12 | fieldNameSignature = "signature" 13 | fieldNameCaveatId = "cid" 14 | fieldNameVerificationId = "vid" 15 | fieldNameCaveatLocation = "cl" 16 | ) 17 | 18 | // maxPacketV1Len is the maximum allowed length of a packet in the v1 macaroon 19 | // serialization format. 20 | const maxPacketV1Len = 0xffff 21 | 22 | // The original macaroon binary encoding is made from a sequence 23 | // of "packets", each of which has a field name and some data. 24 | // The encoding is: 25 | // 26 | // - four ascii hex digits holding the entire packet size (including 27 | // the digits themselves). 28 | // 29 | // - the field name, followed by an ascii space. 30 | // 31 | // - the raw data 32 | // 33 | // - a newline (\n) character 34 | // 35 | // The packet struct below holds a reference into Macaroon.data. 36 | type packetV1 struct { 37 | // ftype holds the field name of the packet. 38 | fieldName []byte 39 | 40 | // data holds the packet's data. 41 | data []byte 42 | 43 | // len holds the total length in bytes 44 | // of the packet, including any header. 45 | totalLen int 46 | } 47 | 48 | // parsePacket parses the packet at the start of the 49 | // given data. 50 | func parsePacketV1(data []byte) (packetV1, error) { 51 | if len(data) < 6 { 52 | return packetV1{}, fmt.Errorf("packet too short") 53 | } 54 | plen, ok := parseSizeV1(data) 55 | if !ok { 56 | return packetV1{}, fmt.Errorf("cannot parse size") 57 | } 58 | if plen > len(data) { 59 | return packetV1{}, fmt.Errorf("packet size too big") 60 | } 61 | if plen < 4 { 62 | return packetV1{}, fmt.Errorf("packet size too small") 63 | } 64 | data = data[4:plen] 65 | i := bytes.IndexByte(data, ' ') 66 | if i <= 0 { 67 | return packetV1{}, fmt.Errorf("cannot parse field name") 68 | } 69 | fieldName := data[0:i] 70 | if data[len(data)-1] != '\n' { 71 | return packetV1{}, fmt.Errorf("no terminating newline found") 72 | } 73 | return packetV1{ 74 | fieldName: fieldName, 75 | data: data[i+1 : len(data)-1], 76 | totalLen: plen, 77 | }, nil 78 | } 79 | 80 | // appendPacketV1 appends a packet with the given field name 81 | // and data to the given buffer. If the field and data were 82 | // too long to be encoded, it returns nil, false; otherwise 83 | // it returns the appended buffer. 84 | func appendPacketV1(buf []byte, field string, data []byte) ([]byte, bool) { 85 | plen := packetV1Size(field, data) 86 | if plen > maxPacketV1Len { 87 | return nil, false 88 | } 89 | buf = appendSizeV1(buf, plen) 90 | buf = append(buf, field...) 91 | buf = append(buf, ' ') 92 | buf = append(buf, data...) 93 | buf = append(buf, '\n') 94 | return buf, true 95 | } 96 | 97 | func packetV1Size(field string, data []byte) int { 98 | return 4 + len(field) + 1 + len(data) + 1 99 | } 100 | 101 | var hexDigits = []byte("0123456789abcdef") 102 | 103 | func appendSizeV1(data []byte, size int) []byte { 104 | return append(data, 105 | hexDigits[size>>12], 106 | hexDigits[(size>>8)&0xf], 107 | hexDigits[(size>>4)&0xf], 108 | hexDigits[size&0xf], 109 | ) 110 | } 111 | 112 | func parseSizeV1(data []byte) (int, bool) { 113 | d0, ok0 := asciiHex(data[0]) 114 | d1, ok1 := asciiHex(data[1]) 115 | d2, ok2 := asciiHex(data[2]) 116 | d3, ok3 := asciiHex(data[3]) 117 | return d0<<12 + d1<<8 + d2<<4 + d3, ok0 && ok1 && ok2 && ok3 118 | } 119 | 120 | func asciiHex(b byte) (int, bool) { 121 | switch { 122 | case b >= '0' && b <= '9': 123 | return int(b) - '0', true 124 | case b >= 'a' && b <= 'f': 125 | return int(b) - 'a' + 0xa, true 126 | } 127 | return 0, false 128 | } 129 | 130 | func isASCIIHex(b byte) bool { 131 | _, ok := asciiHex(b) 132 | return ok 133 | } 134 | -------------------------------------------------------------------------------- /packet-v1_test.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | "unicode" 8 | 9 | qt "github.com/frankban/quicktest" 10 | ) 11 | 12 | func TestAppendPacket(t *testing.T) { 13 | c := qt.New(t) 14 | data, ok := appendPacketV1(nil, "field", []byte("some data")) 15 | c.Assert(ok, qt.Equals, true) 16 | c.Assert(string(data), qt.Equals, "0014field some data\n") 17 | 18 | data, ok = appendPacketV1(data, "otherfield", []byte("more and more data")) 19 | c.Assert(ok, qt.Equals, true) 20 | c.Assert(string(data), qt.Equals, "0014field some data\n0022otherfield more and more data\n") 21 | } 22 | 23 | func TestAppendPacketTooBig(t *testing.T) { 24 | c := qt.New(t) 25 | data, ok := appendPacketV1(nil, "field", make([]byte, 65532)) 26 | c.Assert(ok, qt.Equals, false) 27 | c.Assert(data, qt.IsNil) 28 | } 29 | 30 | var parsePacketV1Tests = []struct { 31 | data string 32 | expect packetV1 33 | expectErr string 34 | }{{ 35 | expectErr: "packet too short", 36 | }, { 37 | data: "0014field some data\n", 38 | expect: packetV1{ 39 | fieldName: []byte("field"), 40 | data: []byte("some data"), 41 | totalLen: 20, 42 | }, 43 | }, { 44 | data: "0015field some data\n", 45 | expectErr: "packet size too big", 46 | }, { 47 | data: "0003a\n", 48 | expectErr: "packet size too small", 49 | }, { 50 | data: "0014fieldwithoutanyspaceordata\n", 51 | expectErr: "cannot parse field name", 52 | }, { 53 | data: "fedcsomefield " + strings.Repeat("x", 0xfedc-len("0000somefield \n")) + "\n", 54 | expect: packetV1{ 55 | fieldName: []byte("somefield"), 56 | data: []byte(strings.Repeat("x", 0xfedc-len("0000somefield \n"))), 57 | totalLen: 0xfedc, 58 | }, 59 | }, { 60 | data: "zzzzbadpacketsizenomacaroon", 61 | expectErr: "cannot parse size", 62 | }} 63 | 64 | func TestParsePacketV1(t *testing.T) { 65 | c := qt.New(t) 66 | for i, test := range parsePacketV1Tests { 67 | c.Logf("test %d: %q", i, truncate(test.data)) 68 | p, err := parsePacketV1([]byte(test.data)) 69 | if test.expectErr != "" { 70 | c.Assert(err, qt.ErrorMatches, test.expectErr) 71 | c.Assert(p, packetEquals, packetV1{}) 72 | continue 73 | } 74 | c.Assert(err, qt.Equals, nil) 75 | c.Assert(p, packetEquals, test.expect) 76 | } 77 | } 78 | 79 | func truncate(d string) string { 80 | if len(d) > 50 { 81 | return d[0:50] + "..." 82 | } 83 | return d 84 | } 85 | 86 | func TestAsciiHex(t *testing.T) { 87 | c := qt.New(t) 88 | for b := 0; b < 256; b++ { 89 | n, err := strconv.ParseInt(string(b), 16, 8) 90 | value, ok := asciiHex(byte(b)) 91 | if err != nil || unicode.IsUpper(rune(b)) { 92 | c.Assert(ok, qt.Equals, false) 93 | c.Assert(value, qt.Equals, 0) 94 | } else { 95 | c.Assert(ok, qt.Equals, true) 96 | c.Assert(value, qt.Equals, int(n)) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packet-v2.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type fieldType int 9 | 10 | // Field constants as used in the binary encoding. 11 | const ( 12 | fieldEOS fieldType = 0 13 | fieldLocation fieldType = 1 14 | fieldIdentifier fieldType = 2 15 | fieldVerificationId fieldType = 4 16 | fieldSignature fieldType = 6 17 | ) 18 | 19 | type packetV2 struct { 20 | // fieldType holds the type of the field. 21 | fieldType fieldType 22 | 23 | // data holds the packet's data. 24 | data []byte 25 | } 26 | 27 | // parseSectionV2 parses a sequence of packets 28 | // in data. The sequence is terminated by a packet 29 | // with a field type of fieldEOS. 30 | func parseSectionV2(data []byte) ([]byte, []packetV2, error) { 31 | prevFieldType := fieldType(-1) 32 | var packets []packetV2 33 | for { 34 | if len(data) == 0 { 35 | return nil, nil, fmt.Errorf("section extends past end of buffer") 36 | } 37 | rest, p, err := parsePacketV2(data) 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | if p.fieldType == fieldEOS { 42 | return rest, packets, nil 43 | } 44 | if p.fieldType <= prevFieldType { 45 | return nil, nil, fmt.Errorf("fields out of order") 46 | } 47 | packets = append(packets, p) 48 | prevFieldType = p.fieldType 49 | data = rest 50 | } 51 | } 52 | 53 | // parsePacketV2 parses a V2 data package at the start 54 | // of the given data. 55 | // The format of a packet is as follows: 56 | // 57 | // fieldType(varint) payloadLen(varint) data[payloadLen bytes] 58 | // 59 | // apart from fieldEOS which has no payloadLen or data (it's 60 | // a single zero byte). 61 | func parsePacketV2(data []byte) ([]byte, packetV2, error) { 62 | data, ft, err := parseVarint(data) 63 | if err != nil { 64 | return nil, packetV2{}, err 65 | } 66 | p := packetV2{ 67 | fieldType: fieldType(ft), 68 | } 69 | if p.fieldType == fieldEOS { 70 | return data, p, nil 71 | } 72 | data, payloadLen, err := parseVarint(data) 73 | if err != nil { 74 | return nil, packetV2{}, err 75 | } 76 | if payloadLen > len(data) { 77 | return nil, packetV2{}, fmt.Errorf("field data extends past end of buffer") 78 | } 79 | p.data = data[0:payloadLen] 80 | return data[payloadLen:], p, nil 81 | } 82 | 83 | // parseVarint parses the variable-length integer 84 | // at the start of the given data and returns rest 85 | // of the buffer and the number. 86 | func parseVarint(data []byte) ([]byte, int, error) { 87 | val, n := binary.Uvarint(data) 88 | if n > 0 { 89 | if val > 0x7fffffff { 90 | return nil, 0, fmt.Errorf("varint value out of range") 91 | } 92 | return data[n:], int(val), nil 93 | } 94 | if n == 0 { 95 | return nil, 0, fmt.Errorf("varint value extends past end of buffer") 96 | } 97 | return nil, 0, fmt.Errorf("varint value out of range") 98 | } 99 | 100 | func appendPacketV2(data []byte, p packetV2) []byte { 101 | data = appendVarint(data, int(p.fieldType)) 102 | if p.fieldType != fieldEOS { 103 | data = appendVarint(data, len(p.data)) 104 | data = append(data, p.data...) 105 | } 106 | return data 107 | } 108 | 109 | func appendEOSV2(data []byte) []byte { 110 | return append(data, 0) 111 | } 112 | 113 | func appendVarint(data []byte, x int) []byte { 114 | var buf [binary.MaxVarintLen32]byte 115 | n := binary.PutUvarint(buf[:], uint64(x)) 116 | return append(data, buf[:n]...) 117 | } 118 | -------------------------------------------------------------------------------- /packet-v2_test.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | var packetEquals = qt.CmpEquals(cmp.AllowUnexported(packetV1{}, packetV2{})) 11 | 12 | var parsePacketV2Tests = []struct { 13 | about string 14 | data string 15 | expectPacket packetV2 16 | expectData string 17 | expectError string 18 | }{{ 19 | about: "EOS packet", 20 | data: "\x00", 21 | expectPacket: packetV2{ 22 | fieldType: fieldEOS, 23 | }, 24 | }, { 25 | about: "simple field", 26 | data: "\x02\x03xyz", 27 | expectPacket: packetV2{ 28 | fieldType: 2, 29 | data: []byte("xyz"), 30 | }, 31 | }, { 32 | about: "empty buffer", 33 | data: "", 34 | expectError: "varint value extends past end of buffer", 35 | }, { 36 | about: "varint out of range", 37 | data: "\xff\xff\xff\xff\xff\xff\x7f", 38 | expectError: "varint value out of range", 39 | }, { 40 | about: "varint way out of range", 41 | data: "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x7f", 42 | expectError: "varint value out of range", 43 | }, { 44 | about: "unterminated varint", 45 | data: "\x80", 46 | expectError: "varint value extends past end of buffer", 47 | }, { 48 | about: "field data too long", 49 | data: "\x01\x02a", 50 | expectError: "field data extends past end of buffer", 51 | }, { 52 | about: "bad data length varint", 53 | data: "\x01\xff", 54 | expectError: "varint value extends past end of buffer", 55 | }} 56 | 57 | func TestParsePacketV2(t *testing.T) { 58 | c := qt.New(t) 59 | for i, test := range parsePacketV2Tests { 60 | c.Logf("test %d: %v", i, test.about) 61 | data, p, err := parsePacketV2([]byte(test.data)) 62 | if test.expectError != "" { 63 | c.Assert(err, qt.ErrorMatches, test.expectError) 64 | c.Assert(data, qt.IsNil) 65 | c.Assert(p, packetEquals, packetV2{}) 66 | } else { 67 | c.Assert(err, qt.Equals, nil) 68 | c.Assert(p, packetEquals, test.expectPacket) 69 | } 70 | } 71 | } 72 | 73 | var parseSectionV2Tests = []struct { 74 | about string 75 | data string 76 | 77 | expectData string 78 | expectPackets []packetV2 79 | expectError string 80 | }{{ 81 | about: "no packets", 82 | data: "\x00", 83 | }, { 84 | about: "one packet", 85 | data: "\x02\x03xyz\x00", 86 | expectPackets: []packetV2{{ 87 | fieldType: 2, 88 | data: []byte("xyz"), 89 | }}, 90 | }, { 91 | about: "two packets", 92 | data: "\x02\x03xyz\x07\x05abcde\x00", 93 | expectPackets: []packetV2{{ 94 | fieldType: 2, 95 | data: []byte("xyz"), 96 | }, { 97 | fieldType: 7, 98 | data: []byte("abcde"), 99 | }}, 100 | }, { 101 | about: "unterminated section", 102 | data: "\x02\x03xyz\x07\x05abcde", 103 | expectError: "section extends past end of buffer", 104 | }, { 105 | about: "out of order fields", 106 | data: "\x07\x05abcde\x02\x03xyz\x00", 107 | expectError: "fields out of order", 108 | }, { 109 | about: "bad packet", 110 | data: "\x07\x05abcde\xff", 111 | expectError: "varint value extends past end of buffer", 112 | }} 113 | 114 | func TestParseSectionV2(t *testing.T) { 115 | c := qt.New(t) 116 | for i, test := range parseSectionV2Tests { 117 | c.Logf("test %d: %v", i, test.about) 118 | data, ps, err := parseSectionV2([]byte(test.data)) 119 | if test.expectError != "" { 120 | c.Assert(err, qt.ErrorMatches, test.expectError) 121 | c.Assert(data, qt.IsNil) 122 | c.Assert(ps, qt.IsNil) 123 | } else { 124 | c.Assert(err, qt.Equals, nil) 125 | c.Assert(ps, packetEquals, test.expectPackets) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | package macaroon 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Trace holds all toperations involved in verifying a macaroon, 8 | // and the root key used as the initial verification key. 9 | // This can be useful for debugging macaroon implementations. 10 | type Trace struct { 11 | RootKey []byte 12 | Ops []TraceOp 13 | } 14 | 15 | // Results returns the output from all operations in the Trace. 16 | // The result from ts.Ops[i] will be in the i'th element of the 17 | // returned slice. 18 | // When a trace has resulted in a failure, the 19 | // last element will be nil. 20 | func (t Trace) Results() [][]byte { 21 | r := make([][]byte, len(t.Ops)) 22 | input := t.RootKey 23 | for i, op := range t.Ops { 24 | input = op.Result(input) 25 | r[i] = input 26 | } 27 | return r 28 | } 29 | 30 | // TraceOp holds one possible operation when verifying a macaroon. 31 | type TraceOp struct { 32 | Kind TraceOpKind `json:"kind"` 33 | Data1 []byte `json:"data1,omitempty"` 34 | Data2 []byte `json:"data2,omitempty"` 35 | } 36 | 37 | // Result returns the result of computing the given 38 | // operation with the given input data. 39 | // If op is TraceFail, it returns nil. 40 | func (op TraceOp) Result(input []byte) []byte { 41 | switch op.Kind { 42 | case TraceMakeKey: 43 | return makeKey(input)[:] 44 | case TraceHash: 45 | if len(op.Data2) == 0 { 46 | return keyedHash(bytesToKey(input), op.Data1)[:] 47 | } 48 | return keyedHash2(bytesToKey(input), op.Data1, op.Data2)[:] 49 | case TraceBind: 50 | return bindForRequest(op.Data1, bytesToKey(input))[:] 51 | case TraceFail: 52 | return nil 53 | default: 54 | panic(fmt.Errorf("unknown trace operation kind %d", op.Kind)) 55 | } 56 | } 57 | 58 | func bytesToKey(data []byte) *[keyLen]byte { 59 | var key [keyLen]byte 60 | if len(data) != keyLen { 61 | panic(fmt.Errorf("unexpected input key length; got %d want %d", len(data), keyLen)) 62 | } 63 | copy(key[:], data) 64 | return &key 65 | } 66 | 67 | // TraceOpKind represents the kind of a macaroon verification operation. 68 | type TraceOpKind int 69 | 70 | const ( 71 | TraceInvalid = TraceOpKind(iota) 72 | 73 | // TraceMakeKey represents the operation of calculating a 74 | // fixed length root key from the variable length input key. 75 | TraceMakeKey 76 | 77 | // TraceHash represents a keyed hash operation with one 78 | // or two values. If there is only one value, it will be in Data1. 79 | TraceHash 80 | 81 | // TraceBind represents the operation of binding a discharge macaroon 82 | // to its primary macaroon. Data1 holds the signature of the primary 83 | // macaroon. 84 | TraceBind 85 | 86 | // TraceFail represents a verification failure. If present, this will always 87 | // be the last operation in a trace. 88 | TraceFail 89 | ) 90 | 91 | var traceOps = []string{ 92 | TraceInvalid: "invalid", 93 | TraceMakeKey: "makekey", 94 | TraceHash: "hash", 95 | TraceBind: "bind", 96 | TraceFail: "fail", 97 | } 98 | 99 | // String returns a string representation of the operation. 100 | func (k TraceOpKind) String() string { 101 | return traceOps[k] 102 | } 103 | --------------------------------------------------------------------------------