Add token generation using go macaroon (#437)

* Add Go macaroon library

Signed-off-by: Anant Prakash <anantprakashjsr@gmail.com>

* Add macaroon generation and serialization, for login token.

Signed-off-by: Anant Prakash <anantprakashjsr@gmail.com>

* Remove copyright, trim empty lines

* Make Serialize functions private

* Fix typos
This commit is contained in:
Anant Prakash 2018-05-22 14:43:58 +05:30 committed by Andrew Morgan
parent 89e0a9e812
commit afeab7b2d4
37 changed files with 6295 additions and 0 deletions

View file

@ -0,0 +1,26 @@
Copyright © 2014, Roger Peppe
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of this project nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,159 @@
# macaroon
--
import "gopkg.in/macaroon.v1"
The macaroon package implements macaroons as described in the paper "Macaroons:
Cookies with Contextual Caveats for Decentralized Authorization in the Cloud"
(http://theory.stanford.edu/~ataly/Papers/macaroons.pdf)
See the macaroon bakery packages at http://godoc.org/gopkg.in/macaroon-bakery.v0
for higher level services and operations that use macaroons.
## Usage
#### type Caveat
```go
type Caveat struct {
Id string
Location string
}
```
#### type Macaroon
```go
type Macaroon struct {
}
```
Macaroon holds a macaroon. See Fig. 7 of
http://theory.stanford.edu/~ataly/Papers/macaroons.pdf for a description of the
data contained within. Macaroons are mutable objects - use Clone as appropriate
to avoid unwanted mutation.
#### func New
```go
func New(rootKey []byte, id, loc string) (*Macaroon, error)
```
New returns a new macaroon with the given root key, identifier and location.
#### func (*Macaroon) AddFirstPartyCaveat
```go
func (m *Macaroon) AddFirstPartyCaveat(caveatId string) error
```
AddFirstPartyCaveat adds a caveat that will be verified by the target service.
#### func (*Macaroon) AddThirdPartyCaveat
```go
func (m *Macaroon) AddThirdPartyCaveat(rootKey []byte, caveatId string, loc string) error
```
AddThirdPartyCaveat adds a third-party caveat to the macaroon, using the given
shared root key, caveat id and location hint. The caveat id should encode the
root key in some way, either by encrypting it with a key known to the third
party or by holding a reference to it stored in the third party's storage.
#### func (*Macaroon) Bind
```go
func (m *Macaroon) Bind(sig []byte)
```
Bind prepares the macaroon for being used to discharge the macaroon with the
given signature sig. This must be used before it is used in the discharges
argument to Verify.
#### func (*Macaroon) Caveats
```go
func (m *Macaroon) Caveats() []Caveat
```
Caveats returns the macaroon's caveats. This method will probably change, and
it's important not to change the returned caveat.
#### func (*Macaroon) Clone
```go
func (m *Macaroon) Clone() *Macaroon
```
Clone returns a copy of the receiving macaroon.
#### func (*Macaroon) Id
```go
func (m *Macaroon) Id() string
```
Id returns the id of the macaroon. This can hold arbitrary information.
#### func (*Macaroon) Location
```go
func (m *Macaroon) Location() string
```
Location returns the macaroon's location hint. This is not verified as part of
the macaroon.
#### func (*Macaroon) MarshalBinary
```go
func (m *Macaroon) MarshalBinary() ([]byte, error)
```
MarshalBinary implements encoding.BinaryMarshaler.
#### func (*Macaroon) MarshalJSON
```go
func (m *Macaroon) MarshalJSON() ([]byte, error)
```
MarshalJSON implements json.Marshaler.
#### func (*Macaroon) Signature
```go
func (m *Macaroon) Signature() []byte
```
Signature returns the macaroon's signature.
#### func (*Macaroon) UnmarshalBinary
```go
func (m *Macaroon) UnmarshalBinary(data []byte) error
```
UnmarshalBinary implements encoding.BinaryUnmarshaler.
#### func (*Macaroon) UnmarshalJSON
```go
func (m *Macaroon) UnmarshalJSON(jsonData []byte) error
```
UnmarshalJSON implements json.Unmarshaler.
#### func (*Macaroon) Verify
```go
func (m *Macaroon) Verify(rootKey []byte, check func(caveat string) error, discharges []*Macaroon) error
```
Verify verifies that the receiving macaroon is valid. The root key must be the
same that the macaroon was originally minted with. The check function is called
to verify each first-party caveat - it should return an error if the condition
is not met.
The discharge macaroons should be provided in discharges.
Verify returns true if the verification succeeds; if returns (false, nil) if the
verification fails, and (false, err) if the verification cannot be asserted (but
may not be false).
TODO(rog) is there a possible DOS attack that can cause this function to
infinitely recurse?
#### type Verifier
```go
type Verifier interface {
Verify(m *Macaroon, rootKey []byte) (bool, error)
}
```

View file

@ -0,0 +1,4 @@
macaroon:
- verify that all signature calculations to correspond exactly
with libmacaroons.

View file

@ -0,0 +1,109 @@
package macaroon_test
import (
"crypto/rand"
"encoding/base64"
"testing"
"gopkg.in/macaroon.v2"
)
func randomBytes(n int) []byte {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return b
}
func BenchmarkNew(b *testing.B) {
rootKey := randomBytes(24)
id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100)))
loc := base64.StdEncoding.EncodeToString(randomBytes(40))
b.ResetTimer()
for i := b.N - 1; i >= 0; i-- {
MustNew(rootKey, id, loc, macaroon.LatestVersion)
}
}
func BenchmarkAddCaveat(b *testing.B) {
rootKey := randomBytes(24)
id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100)))
loc := base64.StdEncoding.EncodeToString(randomBytes(40))
b.ResetTimer()
for i := b.N - 1; i >= 0; i-- {
b.StopTimer()
m := MustNew(rootKey, id, loc, macaroon.LatestVersion)
b.StartTimer()
m.AddFirstPartyCaveat([]byte("some caveat stuff"))
}
}
func benchmarkVerify(b *testing.B, mspecs []macaroonSpec) {
rootKey, macaroons := makeMacaroons(mspecs)
check := func(string) error {
return nil
}
b.ResetTimer()
for i := b.N - 1; i >= 0; i-- {
err := macaroons[0].Verify(rootKey, check, macaroons[1:])
if err != nil {
b.Fatalf("verification failed: %v", err)
}
}
}
func BenchmarkVerifyLarge(b *testing.B) {
benchmarkVerify(b, multilevelThirdPartyCaveatMacaroons)
}
func BenchmarkVerifySmall(b *testing.B) {
benchmarkVerify(b, []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}},
}})
}
func BenchmarkMarshalJSON(b *testing.B) {
rootKey := randomBytes(24)
id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100)))
loc := base64.StdEncoding.EncodeToString(randomBytes(40))
m := MustNew(rootKey, id, loc, macaroon.LatestVersion)
b.ResetTimer()
for i := b.N - 1; i >= 0; i-- {
_, err := m.MarshalJSON()
if err != nil {
b.Fatalf("cannot marshal JSON: %v", err)
}
}
}
func MustNew(rootKey, id []byte, loc string, vers macaroon.Version) *macaroon.Macaroon {
m, err := macaroon.New(rootKey, id, loc, vers)
if err != nil {
panic(err)
}
return m
}
func BenchmarkUnmarshalJSON(b *testing.B) {
rootKey := randomBytes(24)
id := []byte(base64.StdEncoding.EncodeToString(randomBytes(100)))
loc := base64.StdEncoding.EncodeToString(randomBytes(40))
m := MustNew(rootKey, id, loc, macaroon.LatestVersion)
data, err := m.MarshalJSON()
if err != nil {
b.Fatalf("cannot marshal JSON: %v", err)
}
for i := b.N - 1; i >= 0; i-- {
var m macaroon.Macaroon
err := m.UnmarshalJSON(data)
if err != nil {
b.Fatalf("cannot unmarshal JSON: %v", err)
}
}
}

View file

@ -0,0 +1,91 @@
package macaroon
import (
"crypto/hmac"
"crypto/sha256"
"fmt"
"hash"
"io"
"golang.org/x/crypto/nacl/secretbox"
)
func keyedHash(key *[hashLen]byte, text []byte) *[hashLen]byte {
h := keyedHasher(key)
h.Write([]byte(text))
var sum [hashLen]byte
hashSum(h, &sum)
return &sum
}
func keyedHasher(key *[hashLen]byte) hash.Hash {
return hmac.New(sha256.New, key[:])
}
var keyGen = []byte("macaroons-key-generator")
// makeKey derives a fixed length key from a variable
// length key. The keyGen constant is the same
// as that used in libmacaroons.
func makeKey(variableKey []byte) *[keyLen]byte {
h := hmac.New(sha256.New, keyGen)
h.Write(variableKey)
var key [keyLen]byte
hashSum(h, &key)
return &key
}
// hashSum calls h.Sum to put the sum into
// the given destination. It also sanity
// checks that the result really is the expected
// size.
func hashSum(h hash.Hash, dest *[hashLen]byte) {
r := h.Sum(dest[:0])
if len(r) != len(dest) {
panic("hash size inconsistency")
}
}
const (
keyLen = 32
nonceLen = 24
hashLen = sha256.Size
)
func newNonce(r io.Reader) (*[nonceLen]byte, error) {
var nonce [nonceLen]byte
_, err := r.Read(nonce[:])
if err != nil {
return nil, fmt.Errorf("cannot generate random bytes: %v", err)
}
return &nonce, nil
}
func encrypt(key *[keyLen]byte, text *[hashLen]byte, r io.Reader) ([]byte, error) {
nonce, err := newNonce(r)
if err != nil {
return nil, err
}
out := make([]byte, 0, len(nonce)+secretbox.Overhead+len(text))
out = append(out, nonce[:]...)
return secretbox.Seal(out, text[:], nonce, key), nil
}
func decrypt(key *[keyLen]byte, ciphertext []byte) (*[hashLen]byte, error) {
if len(ciphertext) < nonceLen+secretbox.Overhead {
return nil, fmt.Errorf("message too short")
}
var nonce [nonceLen]byte
copy(nonce[:], ciphertext)
ciphertext = ciphertext[nonceLen:]
text, ok := secretbox.Open(nil, ciphertext, &nonce, key)
if !ok {
return nil, fmt.Errorf("decryption failure")
}
if len(text) != hashLen {
return nil, fmt.Errorf("decrypted text is wrong length")
}
var rtext [hashLen]byte
copy(rtext[:], text)
return &rtext, nil
}

View file

@ -0,0 +1,66 @@
package macaroon
import (
"crypto/rand"
"fmt"
"golang.org/x/crypto/nacl/secretbox"
gc "gopkg.in/check.v1"
)
type cryptoSuite struct{}
var _ = gc.Suite(&cryptoSuite{})
var testCryptKey = &[hashLen]byte{'k', 'e', 'y'}
var testCryptText = &[hashLen]byte{'t', 'e', 'x', 't'}
func (*cryptoSuite) TestEncDec(c *gc.C) {
b, err := encrypt(testCryptKey, testCryptText, rand.Reader)
c.Assert(err, gc.IsNil)
t, err := decrypt(testCryptKey, b)
c.Assert(err, gc.IsNil)
c.Assert(string(t[:]), gc.Equals, string(testCryptText[:]))
}
func (*cryptoSuite) TestUniqueNonces(c *gc.C) {
nonces := make(map[string]struct{})
for i := 0; i < 100; i++ {
nonce, err := newNonce(rand.Reader)
c.Assert(err, gc.IsNil)
nonces[string(nonce[:])] = struct{}{}
}
c.Assert(nonces, gc.HasLen, 100, gc.Commentf("duplicate nonce detected"))
}
type ErrorReader struct{}
func (*ErrorReader) Read([]byte) (int, error) {
return 0, fmt.Errorf("fail")
}
func (*cryptoSuite) TestBadRandom(c *gc.C) {
_, err := newNonce(&ErrorReader{})
c.Assert(err, gc.ErrorMatches, "^cannot generate random bytes:.*")
_, err = encrypt(testCryptKey, testCryptText, &ErrorReader{})
c.Assert(err, gc.ErrorMatches, "^cannot generate random bytes:.*")
}
func (*cryptoSuite) TestBadCiphertext(c *gc.C) {
buf := randomBytes(nonceLen + secretbox.Overhead)
for i := range buf {
_, err := decrypt(testCryptKey, buf[0:i])
c.Assert(err, gc.ErrorMatches, "message too short")
}
_, err := decrypt(testCryptKey, buf)
c.Assert(err, gc.ErrorMatches, "decryption failure")
}
func randomBytes(n int) []byte {
buf := make([]byte, n)
if _, err := rand.Reader.Read(buf); err != nil {
panic(err)
}
return buf
}

View file

@ -0,0 +1,14 @@
package macaroon
var (
AddThirdPartyCaveatWithRand = (*Macaroon).addThirdPartyCaveatWithRand
)
type MacaroonJSONV2 macaroonJSONV2
// SetVersion sets the version field of m to v;
// usually so that we can compare it for deep equality with
// another differently unmarshaled macaroon.
func (m *Macaroon) SetVersion(v Version) {
m.version = v
}

View file

@ -0,0 +1,366 @@
// The macaroon package implements macaroons as described in
// the paper "Macaroons: Cookies with Contextual Caveats for
// Decentralized Authorization in the Cloud"
// (http://theory.stanford.edu/~ataly/Papers/macaroons.pdf)
//
// See the macaroon bakery packages at http://godoc.org/gopkg.in/macaroon-bakery.v1
// for higher level services and operations that use macaroons.
package macaroon
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"fmt"
"io"
"unicode/utf8"
)
// Macaroon holds a macaroon.
// See Fig. 7 of http://theory.stanford.edu/~ataly/Papers/macaroons.pdf
// for a description of the data contained within.
// Macaroons are mutable objects - use Clone as appropriate
// to avoid unwanted mutation.
type Macaroon struct {
location string
id []byte
caveats []Caveat
sig [hashLen]byte
version Version
}
// Caveat holds a first person or third party caveat.
type Caveat struct {
// Id holds the id of the caveat. For first
// party caveats this holds the condition;
// for third party caveats this holds the encrypted
// third party caveat.
Id []byte
// VerificationId holds the verification id. If this is
// non-empty, it's a third party caveat.
VerificationId []byte
// For third-party caveats, Location holds the
// ocation hint. Note that this is not signature checked
// as part of the caveat, so should only
// be used as a hint.
Location string
}
// isThirdParty reports whether the caveat must be satisfied
// by some third party (if not, it's a first person caveat).
func (cav *Caveat) isThirdParty() bool {
return len(cav.VerificationId) > 0
}
// New returns a new macaroon with the given root key,
// identifier, location and version.
func New(rootKey, id []byte, loc string, version Version) (*Macaroon, error) {
var m Macaroon
if version < V2 {
if !utf8.Valid(id) {
return nil, fmt.Errorf("invalid id for %v macaroon", id)
}
// TODO check id length too.
}
if version < V1 || version > LatestVersion {
return nil, fmt.Errorf("invalid version %v", version)
}
m.version = version
m.init(append([]byte(nil), id...), loc, version)
derivedKey := makeKey(rootKey)
m.sig = *keyedHash(derivedKey, m.id)
return &m, nil
}
// init initializes the macaroon. It retains a reference to id.
func (m *Macaroon) init(id []byte, loc string, vers Version) {
m.location = loc
m.id = append([]byte(nil), id...)
m.version = vers
}
// SetLocation sets the location associated with the macaroon.
// Note that the location is not included in the macaroon's
// hash chain, so this does not change the signature.
func (m *Macaroon) SetLocation(loc string) {
m.location = loc
}
// Clone returns a copy of the receiving macaroon.
func (m *Macaroon) Clone() *Macaroon {
m1 := *m
// Ensure that if any caveats are appended to the new
// macaroon, it will copy the caveats.
m1.caveats = m1.caveats[0:len(m1.caveats):len(m1.caveats)]
return &m1
}
// Location returns the macaroon's location hint. This is
// not verified as part of the macaroon.
func (m *Macaroon) Location() string {
return m.location
}
// Id returns the id of the macaroon. This can hold
// arbitrary information.
func (m *Macaroon) Id() []byte {
return append([]byte(nil), m.id...)
}
// Signature returns the macaroon's signature.
func (m *Macaroon) Signature() []byte {
// sig := m.sig
// return sig[:]
// Work around https://github.com/golang/go/issues/9537
sig := new([hashLen]byte)
*sig = m.sig
return sig[:]
}
// Caveats returns the macaroon's caveats.
// This method will probably change, and it's important not to change the returned caveat.
func (m *Macaroon) Caveats() []Caveat {
return m.caveats[0:len(m.caveats):len(m.caveats)]
}
// appendCaveat appends a caveat without modifying the macaroon's signature.
func (m *Macaroon) appendCaveat(caveatId, verificationId []byte, loc string) {
m.caveats = append(m.caveats, Caveat{
Id: caveatId,
VerificationId: verificationId,
Location: loc,
})
}
func (m *Macaroon) addCaveat(caveatId, verificationId []byte, loc string) error {
if m.version < V2 {
if !utf8.Valid(caveatId) {
return fmt.Errorf("invalid caveat id for %v macaroon", m.version)
}
// TODO check caveat length too.
}
m.appendCaveat(caveatId, verificationId, loc)
if len(verificationId) == 0 {
m.sig = *keyedHash(&m.sig, caveatId)
} else {
m.sig = *keyedHash2(&m.sig, verificationId, caveatId)
}
return nil
}
func keyedHash2(key *[keyLen]byte, d1, d2 []byte) *[hashLen]byte {
var data [hashLen * 2]byte
copy(data[0:], keyedHash(key, d1)[:])
copy(data[hashLen:], keyedHash(key, d2)[:])
return keyedHash(key, data[:])
}
// Bind prepares the macaroon for being used to discharge the
// macaroon with the given signature sig. This must be
// used before it is used in the discharges argument to Verify.
func (m *Macaroon) Bind(sig []byte) {
m.sig = *bindForRequest(sig, &m.sig)
}
// AddFirstPartyCaveat adds a caveat that will be verified
// by the target service.
func (m *Macaroon) AddFirstPartyCaveat(condition []byte) error {
m.addCaveat(condition, nil, "")
return nil
}
// AddThirdPartyCaveat adds a third-party caveat to the macaroon,
// using the given shared root key, caveat id and location hint.
// The caveat id should encode the root key in some
// way, either by encrypting it with a key known to the third party
// or by holding a reference to it stored in the third party's
// storage.
func (m *Macaroon) AddThirdPartyCaveat(rootKey, caveatId []byte, loc string) error {
return m.addThirdPartyCaveatWithRand(rootKey, caveatId, loc, rand.Reader)
}
// addThirdPartyCaveatWithRand adds a third-party caveat to the macaroon, using
// the given source of randomness for encrypting the caveat id.
func (m *Macaroon) addThirdPartyCaveatWithRand(rootKey, caveatId []byte, loc string, r io.Reader) error {
derivedKey := makeKey(rootKey)
verificationId, err := encrypt(&m.sig, derivedKey, r)
if err != nil {
return err
}
m.addCaveat(caveatId, verificationId, loc)
return nil
}
var zeroKey [hashLen]byte
// bindForRequest binds the given macaroon
// to the given signature of its parent macaroon.
func bindForRequest(rootSig []byte, dischargeSig *[hashLen]byte) *[hashLen]byte {
if bytes.Equal(rootSig, dischargeSig[:]) {
return dischargeSig
}
return keyedHash2(&zeroKey, rootSig, dischargeSig[:])
}
// Verify verifies that the receiving macaroon is valid.
// The root key must be the same that the macaroon was originally
// minted with. The check function is called to verify each
// first-party caveat - it should return an error if the
// condition is not met.
//
// The discharge macaroons should be provided in discharges.
//
// Verify returns nil if the verification succeeds.
func (m *Macaroon) Verify(rootKey []byte, check func(caveat string) error, discharges []*Macaroon) error {
var vctx verificationContext
vctx.init(rootKey, m, discharges, check)
return vctx.verify(m, rootKey)
}
// VerifySignature verifies the signature of the given macaroon with respect
// to the root key, but it does not validate any first-party caveats. Instead
// it returns all the applicable first party caveats on success.
//
// The caller is responsible for checking the returned first party caveat
// conditions.
func (m *Macaroon) VerifySignature(rootKey []byte, discharges []*Macaroon) ([]string, error) {
n := len(m.caveats)
for _, dm := range discharges {
n += len(dm.caveats)
}
conds := make([]string, 0, n)
var vctx verificationContext
vctx.init(rootKey, m, discharges, func(cond string) error {
conds = append(conds, cond)
return nil
})
err := vctx.verify(m, rootKey)
if err != nil {
return nil, err
}
return conds, nil
}
// TraceVerify verifies the signature of the macaroon without checking
// any of the first party caveats, and returns a slice of Traces holding
// the operations used when verifying the macaroons.
//
// Each element in the returned slice corresponds to the
// operation for one of the argument macaroons, with m at index 0,
// and discharges at 1 onwards.
func (m *Macaroon) TraceVerify(rootKey []byte, discharges []*Macaroon) ([]Trace, error) {
var vctx verificationContext
vctx.init(rootKey, m, discharges, func(string) error { return nil })
vctx.traces = make([]Trace, len(discharges)+1)
err := vctx.verify(m, rootKey)
return vctx.traces, err
}
type verificationContext struct {
used []bool
discharges []*Macaroon
rootSig *[hashLen]byte
traces []Trace
check func(caveat string) error
}
func (vctx *verificationContext) init(rootKey []byte, root *Macaroon, discharges []*Macaroon, check func(caveat string) error) {
*vctx = verificationContext{
discharges: discharges,
used: make([]bool, len(discharges)),
rootSig: &root.sig,
check: check,
}
}
func (vctx *verificationContext) verify(root *Macaroon, rootKey []byte) error {
vctx.traceRootKey(0, rootKey)
vctx.trace(0, TraceMakeKey, rootKey, nil)
derivedKey := makeKey(rootKey)
if err := vctx.verify0(root, 0, derivedKey); err != nil {
vctx.trace(0, TraceFail, nil, nil)
return err
}
for i, wasUsed := range vctx.used {
if !wasUsed {
vctx.trace(i+1, TraceFail, nil, nil)
return fmt.Errorf("discharge macaroon %q was not used", vctx.discharges[i].Id())
}
}
return nil
}
func (vctx *verificationContext) verify0(m *Macaroon, index int, rootKey *[hashLen]byte) error {
vctx.trace(index, TraceHash, m.id, nil)
caveatSig := keyedHash(rootKey, m.id)
for i, cav := range m.caveats {
if cav.isThirdParty() {
cavKey, err := decrypt(caveatSig, cav.VerificationId)
if err != nil {
return fmt.Errorf("failed to decrypt caveat %d signature: %v", i, err)
}
dm, di, err := vctx.findDischarge(cav.Id)
if err != nil {
return err
}
vctx.traceRootKey(di+1, cavKey[:])
if err := vctx.verify0(dm, di+1, cavKey); err != nil {
vctx.trace(di+1, TraceFail, nil, nil)
return err
}
vctx.trace(index, TraceHash, cav.VerificationId, cav.Id)
caveatSig = keyedHash2(caveatSig, cav.VerificationId, cav.Id)
} else {
vctx.trace(index, TraceHash, cav.Id, nil)
caveatSig = keyedHash(caveatSig, cav.Id)
if err := vctx.check(string(cav.Id)); err != nil {
return err
}
}
}
if index > 0 {
vctx.trace(index, TraceBind, vctx.rootSig[:], caveatSig[:])
caveatSig = bindForRequest(vctx.rootSig[:], caveatSig)
}
// TODO perhaps we should actually do this check before doing
// all the potentially expensive caveat checks.
if !hmac.Equal(caveatSig[:], m.sig[:]) {
return fmt.Errorf("signature mismatch after caveat verification")
}
return nil
}
func (vctx *verificationContext) findDischarge(id []byte) (dm *Macaroon, index int, err error) {
for di, dm := range vctx.discharges {
if !bytes.Equal(dm.id, id) {
continue
}
// Don't use a discharge macaroon more than once.
// It's important that we do this check here rather than after
// verify as it prevents potentially infinite recursion.
if vctx.used[di] {
return nil, 0, fmt.Errorf("discharge macaroon %q was used more than once", dm.Id())
}
vctx.used[di] = true
return dm, di, nil
}
return nil, 0, fmt.Errorf("cannot find discharge macaroon for caveat %x", id)
}
func (vctx *verificationContext) trace(index int, op TraceOpKind, data1, data2 []byte) {
if vctx.traces != nil {
vctx.traces[index].Ops = append(vctx.traces[index].Ops, TraceOp{
Kind: op,
Data1: data1,
Data2: data2,
})
}
}
func (vctx *verificationContext) traceRootKey(index int, rootKey []byte) {
if vctx.traces != nil {
vctx.traces[index].RootKey = rootKey[:]
}
}

View file

@ -0,0 +1,914 @@
package macaroon_test
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"testing"
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
"gopkg.in/macaroon.v2"
)
func TestPackage(t *testing.T) {
gc.TestingT(t)
}
type macaroonSuite struct{}
var _ = gc.Suite(&macaroonSuite{})
func never(string) error {
return fmt.Errorf("condition is never true")
}
func (*macaroonSuite) TestNoCaveats(c *gc.C) {
rootKey := []byte("secret")
m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion)
c.Assert(m.Location(), gc.Equals, "a location")
c.Assert(m.Id(), gc.DeepEquals, []byte("some id"))
err := m.Verify(rootKey, never, nil)
c.Assert(err, gc.IsNil)
}
func (*macaroonSuite) TestFirstPartyCaveat(c *gc.C) {
rootKey := []byte("secret")
m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion)
caveats := map[string]bool{
"a caveat": true,
"another caveat": true,
}
tested := make(map[string]bool)
for cav := range caveats {
m.AddFirstPartyCaveat([]byte(cav))
}
expectErr := fmt.Errorf("condition not met")
check := func(cav string) error {
tested[cav] = true
if caveats[cav] {
return nil
}
return expectErr
}
err := m.Verify(rootKey, check, nil)
c.Assert(err, gc.IsNil)
c.Assert(tested, gc.DeepEquals, caveats)
m.AddFirstPartyCaveat([]byte("not met"))
err = m.Verify(rootKey, check, nil)
c.Assert(err, gc.Equals, expectErr)
c.Assert(tested["not met"], gc.Equals, true)
}
func (*macaroonSuite) TestThirdPartyCaveat(c *gc.C) {
rootKey := []byte("secret")
m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion)
dischargeRootKey := []byte("shared root key")
thirdPartyCaveatId := []byte("3rd party caveat")
err := m.AddThirdPartyCaveat(dischargeRootKey, thirdPartyCaveatId, "remote.com")
c.Assert(err, gc.IsNil)
dm := MustNew(dischargeRootKey, thirdPartyCaveatId, "remote location", macaroon.LatestVersion)
dm.Bind(m.Signature())
err = m.Verify(rootKey, never, []*macaroon.Macaroon{dm})
c.Assert(err, gc.IsNil)
}
func (*macaroonSuite) TestThirdPartyCaveatBadRandom(c *gc.C) {
rootKey := []byte("secret")
m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion)
dischargeRootKey := []byte("shared root key")
thirdPartyCaveatId := []byte("3rd party caveat")
err := macaroon.AddThirdPartyCaveatWithRand(m, dischargeRootKey, thirdPartyCaveatId, "remote.com", &macaroon.ErrorReader{})
c.Assert(err, gc.ErrorMatches, "cannot generate random bytes: fail")
}
func (*macaroonSuite) TestSetLocation(c *gc.C) {
rootKey := []byte("secret")
m := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion)
c.Assert(m.Location(), gc.Equals, "a location")
m.SetLocation("another location")
c.Assert(m.Location(), gc.Equals, "another location")
}
type conditionTest struct {
conditions map[string]bool
expectErr string
}
var verifyTests = []struct {
about string
macaroons []macaroonSpec
conditions []conditionTest
}{{
about: "single third party caveat without discharge",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}, {
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}},
}},
conditions: []conditionTest{{
conditions: map[string]bool{
"wonderful": true,
},
expectErr: fmt.Sprintf(`cannot find discharge macaroon for caveat %x`, "bob-is-great"),
}},
}, {
about: "single third party caveat with discharge",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}, {
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
}},
conditions: []conditionTest{{
conditions: map[string]bool{
"wonderful": true,
},
}, {
conditions: map[string]bool{
"wonderful": false,
},
expectErr: `condition "wonderful" not met`,
}},
}, {
about: "single third party caveat with discharge with mismatching root key",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}, {
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key-wrong",
id: "bob-is-great",
}},
conditions: []conditionTest{{
conditions: map[string]bool{
"wonderful": true,
},
expectErr: `signature mismatch after caveat verification`,
}},
}, {
about: "single third party caveat with two discharges",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}, {
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
caveats: []caveat{{
condition: "splendid",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
caveats: []caveat{{
condition: "top of the world",
}},
}},
conditions: []conditionTest{{
conditions: map[string]bool{
"wonderful": true,
},
expectErr: `condition "splendid" not met`,
}, {
conditions: map[string]bool{
"wonderful": true,
"splendid": true,
"top of the world": true,
},
expectErr: `discharge macaroon "bob-is-great" was not used`,
}, {
conditions: map[string]bool{
"wonderful": true,
"splendid": false,
"top of the world": true,
},
expectErr: `condition "splendid" not met`,
}, {
conditions: map[string]bool{
"wonderful": true,
"splendid": true,
"top of the world": false,
},
expectErr: `discharge macaroon "bob-is-great" was not used`,
}},
}, {
about: "one discharge used for two macaroons",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "somewhere else",
location: "bob",
rootKey: "bob-caveat-root-key",
}, {
condition: "bob-is-great",
location: "charlie",
rootKey: "bob-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "somewhere else",
caveats: []caveat{{
condition: "bob-is-great",
location: "charlie",
rootKey: "bob-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
}},
conditions: []conditionTest{{
expectErr: `discharge macaroon "bob-is-great" was used more than once`,
}},
}, {
about: "recursive third party caveat",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
caveats: []caveat{{
condition: "bob-is-great",
location: "charlie",
rootKey: "bob-caveat-root-key",
}},
}},
conditions: []conditionTest{{
expectErr: `discharge macaroon "bob-is-great" was used more than once`,
}},
}, {
about: "two third party caveats",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}, {
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}, {
condition: "charlie-is-great",
location: "charlie",
rootKey: "charlie-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
caveats: []caveat{{
condition: "splendid",
}},
}, {
location: "charlie",
rootKey: "charlie-caveat-root-key",
id: "charlie-is-great",
caveats: []caveat{{
condition: "top of the world",
}},
}},
conditions: []conditionTest{{
conditions: map[string]bool{
"wonderful": true,
"splendid": true,
"top of the world": true,
},
}, {
conditions: map[string]bool{
"wonderful": true,
"splendid": false,
"top of the world": true,
},
expectErr: `condition "splendid" not met`,
}, {
conditions: map[string]bool{
"wonderful": true,
"splendid": true,
"top of the world": false,
},
expectErr: `condition "top of the world" not met`,
}},
}, {
about: "third party caveat with undischarged third party caveat",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}, {
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
caveats: []caveat{{
condition: "splendid",
}, {
condition: "barbara-is-great",
location: "barbara",
rootKey: "barbara-caveat-root-key",
}},
}},
conditions: []conditionTest{{
conditions: map[string]bool{
"wonderful": true,
"splendid": true,
},
expectErr: fmt.Sprintf(`cannot find discharge macaroon for caveat %x`, "barbara-is-great"),
}},
}, {
about: "multilevel third party caveats",
macaroons: multilevelThirdPartyCaveatMacaroons,
conditions: []conditionTest{{
conditions: map[string]bool{
"wonderful": true,
"splendid": true,
"high-fiving": true,
"spiffing": true,
},
}, {
conditions: map[string]bool{
"wonderful": true,
"splendid": true,
"high-fiving": false,
"spiffing": true,
},
expectErr: `condition "high-fiving" not met`,
}},
}, {
about: "unused discharge",
macaroons: []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
}, {
rootKey: "other-key",
id: "unused",
}},
conditions: []conditionTest{{
expectErr: `discharge macaroon "unused" was not used`,
}},
}}
var multilevelThirdPartyCaveatMacaroons = []macaroonSpec{{
rootKey: "root-key",
id: "root-id",
caveats: []caveat{{
condition: "wonderful",
}, {
condition: "bob-is-great",
location: "bob",
rootKey: "bob-caveat-root-key",
}, {
condition: "charlie-is-great",
location: "charlie",
rootKey: "charlie-caveat-root-key",
}},
}, {
location: "bob",
rootKey: "bob-caveat-root-key",
id: "bob-is-great",
caveats: []caveat{{
condition: "splendid",
}, {
condition: "barbara-is-great",
location: "barbara",
rootKey: "barbara-caveat-root-key",
}},
}, {
location: "charlie",
rootKey: "charlie-caveat-root-key",
id: "charlie-is-great",
caveats: []caveat{{
condition: "splendid",
}, {
condition: "celine-is-great",
location: "celine",
rootKey: "celine-caveat-root-key",
}},
}, {
location: "barbara",
rootKey: "barbara-caveat-root-key",
id: "barbara-is-great",
caveats: []caveat{{
condition: "spiffing",
}, {
condition: "ben-is-great",
location: "ben",
rootKey: "ben-caveat-root-key",
}},
}, {
location: "ben",
rootKey: "ben-caveat-root-key",
id: "ben-is-great",
}, {
location: "celine",
rootKey: "celine-caveat-root-key",
id: "celine-is-great",
caveats: []caveat{{
condition: "high-fiving",
}},
}}
func (*macaroonSuite) TestVerify(c *gc.C) {
for i, test := range verifyTests {
c.Logf("test %d: %s", i, test.about)
rootKey, macaroons := makeMacaroons(test.macaroons)
for _, cond := range test.conditions {
c.Logf("conditions %#v", cond.conditions)
check := func(cav string) error {
if cond.conditions[cav] {
return nil
}
return fmt.Errorf("condition %q not met", cav)
}
err := macaroons[0].Verify(
rootKey,
check,
macaroons[1:],
)
if cond.expectErr != "" {
c.Assert(err, gc.ErrorMatches, cond.expectErr)
} else {
c.Assert(err, gc.IsNil)
}
// Cloned macaroon should have same verify result.
cloneErr := macaroons[0].Clone().Verify(rootKey, check, macaroons[1:])
c.Assert(cloneErr, gc.DeepEquals, err)
}
}
}
func (*macaroonSuite) TestTraceVerify(c *gc.C) {
rootKey, macaroons := makeMacaroons(multilevelThirdPartyCaveatMacaroons)
traces, err := macaroons[0].TraceVerify(rootKey, macaroons[1:])
c.Assert(err, gc.Equals, nil)
c.Assert(traces, gc.HasLen, len(macaroons))
// Check that we can run through the resulting operations and
// arrive at the same signature.
for i, m := range macaroons {
r := traces[i].Results()
c.Assert(b64str(r[len(r)-1]), gc.Equals, b64str(m.Signature()), gc.Commentf("macaroon %d", i))
}
}
func (*macaroonSuite) TestTraceVerifyFailure(c *gc.C) {
rootKey, macaroons := makeMacaroons([]macaroonSpec{{
rootKey: "xxx",
id: "hello",
caveats: []caveat{{
condition: "cond1",
}, {
condition: "cond2",
}, {
condition: "cond3",
}},
}})
// Marshal the macaroon, corrupt a condition, then unmarshal
// it and check we see the expected trace failure.
data, err := json.Marshal(macaroons[0])
c.Assert(err, gc.Equals, nil)
var jm macaroon.MacaroonJSONV2
err = json.Unmarshal(data, &jm)
c.Assert(err, gc.Equals, nil)
jm.Caveats[1].CID = "cond2 corrupted"
data, err = json.Marshal(jm)
c.Assert(err, gc.Equals, nil)
var corruptm *macaroon.Macaroon
err = json.Unmarshal(data, &corruptm)
c.Assert(err, gc.Equals, nil)
traces, err := corruptm.TraceVerify(rootKey, nil)
c.Assert(err, gc.ErrorMatches, `signature mismatch after caveat verification`)
c.Assert(traces, gc.HasLen, 1)
var kinds []macaroon.TraceOpKind
for _, op := range traces[0].Ops {
kinds = append(kinds, op.Kind)
}
c.Assert(kinds, gc.DeepEquals, []macaroon.TraceOpKind{
macaroon.TraceMakeKey,
macaroon.TraceHash, // id
macaroon.TraceHash, // cond1
macaroon.TraceHash, // cond2
macaroon.TraceHash, // cond3
macaroon.TraceFail, // sig mismatch
})
}
func b64str(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
func (*macaroonSuite) TestVerifySignature(c *gc.C) {
rootKey, macaroons := makeMacaroons([]macaroonSpec{{
rootKey: "xxx",
id: "hello",
caveats: []caveat{{
rootKey: "y",
condition: "something",
location: "somewhere",
}, {
condition: "cond1",
}, {
condition: "cond2",
}},
}, {
rootKey: "y",
id: "something",
caveats: []caveat{{
condition: "cond3",
}, {
condition: "cond4",
}},
}})
conds, err := macaroons[0].VerifySignature(rootKey, macaroons[1:])
c.Assert(err, gc.IsNil)
c.Assert(conds, jc.DeepEquals, []string{"cond3", "cond4", "cond1", "cond2"})
conds, err = macaroons[0].VerifySignature(nil, macaroons[1:])
c.Assert(err, gc.ErrorMatches, `failed to decrypt caveat 0 signature: decryption failure`)
c.Assert(conds, gc.IsNil)
}
// TODO(rog) move the following JSON-marshal tests into marshal_test.go.
// jsonTestVersions holds the various possible ways of marshaling a macaroon
// to JSON.
var jsonTestVersions = []macaroon.Version{
macaroon.V1,
macaroon.V2,
}
func (s *macaroonSuite) TestMarshalJSON(c *gc.C) {
for i, vers := range jsonTestVersions {
c.Logf("test %d: %v", i, vers)
s.testMarshalJSONWithVersion(c, vers)
}
}
func (*macaroonSuite) testMarshalJSONWithVersion(c *gc.C, vers macaroon.Version) {
rootKey := []byte("secret")
m0 := MustNew(rootKey, []byte("some id"), "a location", vers)
m0.AddFirstPartyCaveat([]byte("account = 3735928559"))
m0JSON, err := json.Marshal(m0)
c.Assert(err, gc.IsNil)
var m1 macaroon.Macaroon
err = json.Unmarshal(m0JSON, &m1)
c.Assert(err, gc.IsNil)
c.Assert(m0.Location(), gc.Equals, m1.Location())
c.Assert(string(m0.Id()), gc.Equals, string(m1.Id()))
c.Assert(
hex.EncodeToString(m0.Signature()),
gc.Equals,
hex.EncodeToString(m1.Signature()))
c.Assert(m1.Version(), gc.Equals, vers)
}
var jsonRoundTripTests = []struct {
about string
// data holds the marshaled data. All the data values hold
// different encodings of the same macaroon - the same as produced
// from the second example in libmacaroons
// example README with the following libmacaroons code:
//
// secret = 'this is a different super-secret key; never use the same secret twice'
// public = 'we used our other secret key'
// location = 'http://mybank/'
// M = macaroons.create(location, secret, public)
// M = M.add_first_party_caveat('account = 3735928559')
// caveat_key = '4; guaranteed random by a fair toss of the dice'
// predicate = 'user = Alice'
// identifier = 'this was how we remind auth of key/pred'
// M = M.add_third_party_caveat('http://auth.mybank/', caveat_key, identifier)
// m.serialize_json()
data string
expectExactRoundTrip bool
expectVers macaroon.Version
}{{
about: "exact JSON as produced by libmacaroons",
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"}`,
expectVers: macaroon.V1,
expectExactRoundTrip: true,
}, {
about: "V2 object with std base-64 binary values",
data: `{"c":[{"i64":"YWNjb3VudCA9IDM3MzU5Mjg1NTk="},{"i64":"dGhpcyB3YXMgaG93IHdlIHJlbWluZCBhdXRoIG9mIGtleS9wcmVk","v64":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD/w/dedwv4Jjw7UorCREw5rXbRqIKhr","l":"http://auth.mybank/"}],"l":"http://mybank/","i64":"d2UgdXNlZCBvdXIgb3RoZXIgc2VjcmV0IGtleQ==","s64":"0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Vw="}`,
expectVers: macaroon.V2,
}, {
about: "V2 object with URL base-64 binary values",
data: `{"c":[{"i64":"YWNjb3VudCA9IDM3MzU5Mjg1NTk"},{"i64":"dGhpcyB3YXMgaG93IHdlIHJlbWluZCBhdXRoIG9mIGtleS9wcmVk","v64":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr","l":"http://auth.mybank/"}],"l":"http://mybank/","i64":"d2UgdXNlZCBvdXIgb3RoZXIgc2VjcmV0IGtleQ","s64":"0n2y_R8idg5MPa6BN-LY_B32wHQcGK7UuXJWv3jR9Vw"}`,
expectVers: macaroon.V2,
}, {
about: "V2 object with URL base-64 binary values and strings for ASCII",
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"}`,
expectVers: macaroon.V2,
expectExactRoundTrip: true,
}, {
about: "V2 base64 encoded binary",
data: `"` +
base64.StdEncoding.EncodeToString([]byte(
"\x02"+
"\x01\x0ehttp://mybank/"+
"\x02\x1cwe used our other secret key"+
"\x00"+
"\x02\x14account = 3735928559"+
"\x00"+
"\x01\x13http://auth.mybank/"+
"\x02'this was how we remind auth of key/pred"+
"\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"+
"\x00"+
"\x00"+
"\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",
)) + `"`,
expectVers: macaroon.V2,
}}
func (s *macaroonSuite) TestJSONRoundTrip(c *gc.C) {
for i, test := range jsonRoundTripTests {
c.Logf("test %d (%v) %s", i, test.expectVers, test.about)
s.testJSONRoundTripWithVersion(c, test.data, test.expectVers, test.expectExactRoundTrip)
}
}
func (*macaroonSuite) testJSONRoundTripWithVersion(c *gc.C, jsonData string, vers macaroon.Version, expectExactRoundTrip bool) {
var m macaroon.Macaroon
err := json.Unmarshal([]byte(jsonData), &m)
c.Assert(err, gc.IsNil)
assertLibMacaroonsMacaroon(c, &m)
c.Assert(m.Version(), gc.Equals, vers)
data, err := m.MarshalJSON()
c.Assert(err, gc.IsNil)
if expectExactRoundTrip {
// The data is in canonical form, so we can check that
// the round-tripped data is the same as the original
// data when unmarshalled into an interface{}.
var got interface{}
err = json.Unmarshal(data, &got)
c.Assert(err, gc.IsNil)
var original interface{}
err = json.Unmarshal([]byte(jsonData), &original)
c.Assert(err, gc.IsNil)
c.Assert(got, jc.DeepEquals, original, gc.Commentf("data: %s", data))
}
// Check that we can unmarshal the marshaled data anyway
// and the macaroon still looks the same.
var m1 macaroon.Macaroon
err = m1.UnmarshalJSON(data)
c.Assert(err, gc.IsNil)
assertLibMacaroonsMacaroon(c, &m1)
c.Assert(m.Version(), gc.Equals, vers)
}
// assertLibMacaroonsMacaroon asserts that m looks like the macaroon
// created in the README of the libmacaroons documentation.
// In particular, the signature is the same one reported there.
func assertLibMacaroonsMacaroon(c *gc.C, m *macaroon.Macaroon) {
c.Assert(fmt.Sprintf("%x", m.Signature()), gc.Equals,
"d27db2fd1f22760e4c3dae8137e2d8fc1df6c0741c18aed4b97256bf78d1f55c")
c.Assert(m.Location(), gc.Equals, "http://mybank/")
c.Assert(string(m.Id()), gc.Equals, "we used our other secret key")
c.Assert(m.Caveats(), jc.DeepEquals, []macaroon.Caveat{{
Id: []byte("account = 3735928559"),
}, {
Id: []byte("this was how we remind auth of key/pred"),
VerificationId: decodeB64("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA027FAuBYhtHwJ58FX6UlVNFtFsGxQHS7uD_w_dedwv4Jjw7UorCREw5rXbRqIKhr"),
Location: "http://auth.mybank/",
}})
}
var jsonDecodeErrorTests = []struct {
about string
data string
expectError string
}{{
about: "ambiguous id #1",
data: `{"i": "hello", "i64": "abcd", "s64": "ZDI3ZGIyZmQxZjIyNzYwZTRjM2RhZTgxMzdlMmQ4ZmMK"}`,
expectError: "invalid identifier: ambiguous field encoding",
}, {
about: "ambiguous signature",
data: `{"i": "hello", "s": "345", "s64": "543467"}`,
expectError: "invalid signature: ambiguous field encoding",
}, {
about: "signature too short",
data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Q"}`,
expectError: "signature has unexpected length 31",
}, {
about: "signature too long",
data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9dP1"}`,
expectError: "signature has unexpected length 33",
}, {
about: "invalid caveat id",
data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Vw", "c": [{"i": "hello", "i64": "00"}]}`,
expectError: "invalid cid in caveat: ambiguous field encoding",
}, {
about: "invalid caveat vid",
data: `{"i": "hello", "s64": "0n2y/R8idg5MPa6BN+LY/B32wHQcGK7UuXJWv3jR9Vw", "c": [{"i": "hello", "v": "hello", "v64": "00"}]}`,
expectError: "invalid vid in caveat: ambiguous field encoding",
}}
func (*macaroonSuite) TestJSONDecodeError(c *gc.C) {
for i, test := range jsonDecodeErrorTests {
c.Logf("test %d: %v", i, test.about)
var m macaroon.Macaroon
err := json.Unmarshal([]byte(test.data), &m)
c.Assert(err, gc.ErrorMatches, test.expectError)
}
}
func (*macaroonSuite) TestFirstPartyCaveatWithInvalidUTF8(c *gc.C) {
rootKey := []byte("secret")
badString := "foo\xff"
m0 := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion)
err := m0.AddFirstPartyCaveat([]byte(badString))
c.Assert(err, gc.Equals, nil)
}
func decodeB64(s string) []byte {
data, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
panic(err)
}
return data
}
type caveat struct {
rootKey string
location string
condition string
}
type macaroonSpec struct {
rootKey string
id string
caveats []caveat
location string
}
func makeMacaroons(mspecs []macaroonSpec) (rootKey []byte, macaroons macaroon.Slice) {
for _, mspec := range mspecs {
macaroons = append(macaroons, makeMacaroon(mspec))
}
primary := macaroons[0]
for _, m := range macaroons[1:] {
m.Bind(primary.Signature())
}
return []byte(mspecs[0].rootKey), macaroons
}
func makeMacaroon(mspec macaroonSpec) *macaroon.Macaroon {
m := MustNew([]byte(mspec.rootKey), []byte(mspec.id), mspec.location, macaroon.LatestVersion)
for _, cav := range mspec.caveats {
if cav.location != "" {
err := m.AddThirdPartyCaveat([]byte(cav.rootKey), []byte(cav.condition), cav.location)
if err != nil {
panic(err)
}
} else {
m.AddFirstPartyCaveat([]byte(cav.condition))
}
}
return m
}
func assertEqualMacaroons(c *gc.C, m0, m1 *macaroon.Macaroon) {
m0json, err := m0.MarshalJSON()
c.Assert(err, gc.IsNil)
m1json, err := m1.MarshalJSON()
var m0val, m1val interface{}
err = json.Unmarshal(m0json, &m0val)
c.Assert(err, gc.IsNil)
err = json.Unmarshal(m1json, &m1val)
c.Assert(err, gc.IsNil)
c.Assert(m0val, gc.DeepEquals, m1val)
}
func (*macaroonSuite) TestBinaryRoundTrip(c *gc.C) {
// Test the binary marshalling and unmarshalling of a macaroon with
// first and third party caveats.
rootKey := []byte("secret")
m0 := MustNew(rootKey, []byte("some id"), "a location", macaroon.LatestVersion)
err := m0.AddFirstPartyCaveat([]byte("first caveat"))
c.Assert(err, gc.IsNil)
err = m0.AddFirstPartyCaveat([]byte("second caveat"))
c.Assert(err, gc.IsNil)
err = m0.AddThirdPartyCaveat([]byte("shared root key"), []byte("3rd party caveat"), "remote.com")
c.Assert(err, gc.IsNil)
data, err := m0.MarshalBinary()
c.Assert(err, gc.IsNil)
var m1 macaroon.Macaroon
err = m1.UnmarshalBinary(data)
c.Assert(err, gc.IsNil)
assertEqualMacaroons(c, m0, &m1)
}
func (*macaroonSuite) TestBinaryMarshalingAgainstLibmacaroon(c *gc.C) {
// Test that a libmacaroon marshalled macaroon can be correctly unmarshaled
data, err := base64.RawURLEncoding.DecodeString(
"MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMmNpZGVudGlmaWVyIHdlIHVzZWQgb3VyIG90aGVyIHNlY3JldCBrZXkKMDAxZGNpZCBhY2NvdW50ID0gMzczNTkyODU1OQowMDMwY2lkIHRoaXMgd2FzIGhvdyB3ZSByZW1pbmQgYXV0aCBvZiBrZXkvcHJlZAowMDUxdmlkIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANNuxQLgWIbR8CefBV-lJVTRbRbBsUB0u7g_8P3XncL-CY8O1KKwkRMOa120aiCoawowMDFiY2wgaHR0cDovL2F1dGgubXliYW5rLwowMDJmc2lnbmF0dXJlINJ9sv0fInYOTD2ugTfi2Pwd9sB0HBiu1LlyVr940fVcCg")
c.Assert(err, gc.IsNil)
var m0 macaroon.Macaroon
err = m0.UnmarshalBinary(data)
c.Assert(err, gc.IsNil)
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"}`)
var m1 macaroon.Macaroon
err = m1.UnmarshalJSON(jsonData)
c.Assert(err, gc.IsNil)
assertEqualMacaroons(c, &m0, &m1)
}
var binaryFieldBase64ChoiceTests = []struct {
id string
expectBase64 bool
}{
{"x", false},
{"\x00", true},
{"\x03\x00", true},
{"a longer id with more stuff", false},
{"a longer id with more stuff and one invalid \xff", true},
{"a longer id with more stuff and one encoded \x00", false},
}
func (*macaroonSuite) TestBinaryFieldBase64Choice(c *gc.C) {
for i, test := range binaryFieldBase64ChoiceTests {
c.Logf("test %d: %q", i, test.id)
m := MustNew([]byte{0}, []byte(test.id), "", macaroon.LatestVersion)
data, err := json.Marshal(m)
c.Assert(err, gc.Equals, nil)
var x struct {
Id *string `json:"i"`
Id64 *string `json:"i64"`
}
err = json.Unmarshal(data, &x)
c.Assert(err, gc.Equals, nil)
if test.expectBase64 {
c.Assert(x.Id64, gc.NotNil)
c.Assert(x.Id, gc.IsNil)
idDec, err := base64.RawURLEncoding.DecodeString(*x.Id64)
c.Assert(err, gc.Equals, nil)
c.Assert(string(idDec), gc.Equals, test.id)
} else {
c.Assert(x.Id64, gc.IsNil)
c.Assert(x.Id, gc.NotNil)
c.Assert(*x.Id, gc.Equals, test.id)
}
}
}

View file

@ -0,0 +1,190 @@
package macaroon
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"unicode/utf8"
)
// macaroonJSONV1 defines the V1 JSON format for macaroons.
type macaroonJSONV1 struct {
Caveats []caveatJSONV1 `json:"caveats"`
Location string `json:"location"`
Identifier string `json:"identifier"`
Signature string `json:"signature"` // hex-encoded
}
// caveatJSONV1 defines the V1 JSON format for caveats within a macaroon.
type caveatJSONV1 struct {
CID string `json:"cid"`
VID string `json:"vid,omitempty"`
Location string `json:"cl,omitempty"`
}
// marshalJSONV1 marshals the macaroon to the V1 JSON format.
func (m *Macaroon) marshalJSONV1() ([]byte, error) {
if !utf8.Valid(m.id) {
return nil, fmt.Errorf("macaroon id is not valid UTF-8")
}
mjson := macaroonJSONV1{
Location: m.location,
Identifier: string(m.id),
Signature: hex.EncodeToString(m.sig[:]),
Caveats: make([]caveatJSONV1, len(m.caveats)),
}
for i, cav := range m.caveats {
if !utf8.Valid(cav.Id) {
return nil, fmt.Errorf("caveat id is not valid UTF-8")
}
mjson.Caveats[i] = caveatJSONV1{
Location: cav.Location,
CID: string(cav.Id),
VID: base64.RawURLEncoding.EncodeToString(cav.VerificationId),
}
}
data, err := json.Marshal(mjson)
if err != nil {
return nil, fmt.Errorf("cannot marshal json data: %v", err)
}
return data, nil
}
// initJSONV1 initializes m from the JSON-unmarshaled data
// held in mjson.
func (m *Macaroon) initJSONV1(mjson *macaroonJSONV1) error {
m.init([]byte(mjson.Identifier), mjson.Location, V1)
sig, err := hex.DecodeString(mjson.Signature)
if err != nil {
return fmt.Errorf("cannot decode macaroon signature %q: %v", m.sig, err)
}
if len(sig) != hashLen {
return fmt.Errorf("signature has unexpected length %d", len(sig))
}
copy(m.sig[:], sig)
m.caveats = m.caveats[:0]
for _, cav := range mjson.Caveats {
vid, err := Base64Decode([]byte(cav.VID))
if err != nil {
return fmt.Errorf("cannot decode verification id %q: %v", cav.VID, err)
}
m.appendCaveat([]byte(cav.CID), vid, cav.Location)
}
return nil
}
// The original (v1) binary format of a macaroon is as follows.
// Each identifier represents a v1 packet.
//
// location
// identifier
// (
// caveatId?
// verificationId?
// caveatLocation?
// )*
// signature
// parseBinaryV1 parses the given data in V1 format into the macaroon. The macaroon's
// internal data structures will retain references to the data. It
// returns the data after the end of the macaroon.
func (m *Macaroon) parseBinaryV1(data []byte) ([]byte, error) {
var err error
loc, err := expectPacketV1(data, fieldNameLocation)
if err != nil {
return nil, err
}
data = data[loc.totalLen:]
id, err := expectPacketV1(data, fieldNameIdentifier)
if err != nil {
return nil, err
}
data = data[id.totalLen:]
m.init(id.data, string(loc.data), V1)
var cav Caveat
for {
p, err := parsePacketV1(data)
if err != nil {
return nil, err
}
data = data[p.totalLen:]
switch field := string(p.fieldName); field {
case fieldNameSignature:
// At the end of the caveats we find the signature.
if cav.Id != nil {
m.caveats = append(m.caveats, cav)
}
if len(p.data) != hashLen {
return nil, fmt.Errorf("signature has unexpected length %d", len(p.data))
}
copy(m.sig[:], p.data)
return data, nil
case fieldNameCaveatId:
if cav.Id != nil {
m.caveats = append(m.caveats, cav)
cav = Caveat{}
}
cav.Id = p.data
case fieldNameVerificationId:
if cav.VerificationId != nil {
return nil, fmt.Errorf("repeated field %q in caveat", fieldNameVerificationId)
}
cav.VerificationId = p.data
case fieldNameCaveatLocation:
if cav.Location != "" {
return nil, fmt.Errorf("repeated field %q in caveat", fieldNameLocation)
}
cav.Location = string(p.data)
default:
return nil, fmt.Errorf("unexpected field %q", field)
}
}
}
func expectPacketV1(data []byte, kind string) (packetV1, error) {
p, err := parsePacketV1(data)
if err != nil {
return packetV1{}, err
}
if field := string(p.fieldName); field != kind {
return packetV1{}, fmt.Errorf("unexpected field %q; expected %s", field, kind)
}
return p, nil
}
// appendBinaryV1 appends the binary encoding of m to data.
func (m *Macaroon) appendBinaryV1(data []byte) ([]byte, error) {
var ok bool
data, ok = appendPacketV1(data, fieldNameLocation, []byte(m.location))
if !ok {
return nil, fmt.Errorf("failed to append location to macaroon, packet is too long")
}
data, ok = appendPacketV1(data, fieldNameIdentifier, m.id)
if !ok {
return nil, fmt.Errorf("failed to append identifier to macaroon, packet is too long")
}
for _, cav := range m.caveats {
data, ok = appendPacketV1(data, fieldNameCaveatId, cav.Id)
if !ok {
return nil, fmt.Errorf("failed to append caveat id to macaroon, packet is too long")
}
if cav.VerificationId == nil {
continue
}
data, ok = appendPacketV1(data, fieldNameVerificationId, cav.VerificationId)
if !ok {
return nil, fmt.Errorf("failed to append verification id to macaroon, packet is too long")
}
data, ok = appendPacketV1(data, fieldNameCaveatLocation, []byte(cav.Location))
if !ok {
return nil, fmt.Errorf("failed to append verification id to macaroon, packet is too long")
}
}
data, ok = appendPacketV1(data, fieldNameSignature, m.sig[:])
if !ok {
return nil, fmt.Errorf("failed to append signature to macaroon, packet is too long")
}
return data, nil
}

View file

@ -0,0 +1,253 @@
package macaroon
import (
"encoding/base64"
"encoding/json"
"fmt"
"unicode/utf8"
)
// macaroonJSONV2 defines the V2 JSON format for macaroons.
type macaroonJSONV2 struct {
Caveats []caveatJSONV2 `json:"c,omitempty"`
Location string `json:"l,omitempty"`
Identifier string `json:"i,omitempty"`
Identifier64 string `json:"i64,omitempty"`
Signature string `json:"s,omitempty"`
Signature64 string `json:"s64,omitempty"`
}
// caveatJSONV2 defines the V2 JSON format for caveats within a macaroon.
type caveatJSONV2 struct {
CID string `json:"i,omitempty"`
CID64 string `json:"i64,omitempty"`
VID string `json:"v,omitempty"`
VID64 string `json:"v64,omitempty"`
Location string `json:"l,omitempty"`
}
func (m *Macaroon) marshalJSONV2() ([]byte, error) {
mjson := macaroonJSONV2{
Location: m.location,
Caveats: make([]caveatJSONV2, len(m.caveats)),
}
putJSONBinaryField(m.id, &mjson.Identifier, &mjson.Identifier64)
putJSONBinaryField(m.sig[:], &mjson.Signature, &mjson.Signature64)
for i, cav := range m.caveats {
cavjson := caveatJSONV2{
Location: cav.Location,
}
putJSONBinaryField(cav.Id, &cavjson.CID, &cavjson.CID64)
putJSONBinaryField(cav.VerificationId, &cavjson.VID, &cavjson.VID64)
mjson.Caveats[i] = cavjson
}
data, err := json.Marshal(mjson)
if err != nil {
return nil, fmt.Errorf("cannot marshal json data: %v", err)
}
return data, nil
}
// initJSONV2 initializes m from the JSON-unmarshaled data
// held in mjson.
func (m *Macaroon) initJSONV2(mjson *macaroonJSONV2) error {
id, err := jsonBinaryField(mjson.Identifier, mjson.Identifier64)
if err != nil {
return fmt.Errorf("invalid identifier: %v", err)
}
m.init(id, mjson.Location, V2)
sig, err := jsonBinaryField(mjson.Signature, mjson.Signature64)
if err != nil {
return fmt.Errorf("invalid signature: %v", err)
}
if len(sig) != hashLen {
return fmt.Errorf("signature has unexpected length %d", len(sig))
}
copy(m.sig[:], sig)
m.caveats = make([]Caveat, 0, len(mjson.Caveats))
for _, cav := range mjson.Caveats {
cid, err := jsonBinaryField(cav.CID, cav.CID64)
if err != nil {
return fmt.Errorf("invalid cid in caveat: %v", err)
}
vid, err := jsonBinaryField(cav.VID, cav.VID64)
if err != nil {
return fmt.Errorf("invalid vid in caveat: %v", err)
}
m.appendCaveat(cid, vid, cav.Location)
}
return nil
}
// putJSONBinaryField puts the value of x into one
// of the appropriate fields depending on its value.
func putJSONBinaryField(x []byte, s, sb64 *string) {
if !utf8.Valid(x) {
*sb64 = base64.RawURLEncoding.EncodeToString(x)
return
}
// We could use either string or base64 encoding;
// choose the most compact of the two possibilities.
b64len := base64.RawURLEncoding.EncodedLen(len(x))
sx := string(x)
if jsonEnc, _ := json.Marshal(sx); len(jsonEnc)-2 <= b64len+2 {
// The JSON encoding is smaller than the base 64 encoding.
// NB marshaling a string can never return an error;
// it always includes the two quote characters;
// but using base64 also uses two extra characters for the
// "64" suffix on the field name. If all is equal, prefer string
// encoding because it's more readable.
*s = sx
return
}
*sb64 = base64.RawURLEncoding.EncodeToString(x)
}
// jsonBinaryField returns the value of a JSON field that may
// be string, hex or base64-encoded.
func jsonBinaryField(s, sb64 string) ([]byte, error) {
switch {
case s != "":
if sb64 != "" {
return nil, fmt.Errorf("ambiguous field encoding")
}
return []byte(s), nil
case sb64 != "":
return Base64Decode([]byte(sb64))
}
return []byte{}, nil
}
// The v2 binary format of a macaroon is as follows.
// All entries other than the version are packets as
// parsed by parsePacketV2.
//
// version [1 byte]
// location?
// identifier
// eos
// (
// location?
// identifier
// verificationId?
// eos
// )*
// eos
// signature
//
// See also https://github.com/rescrv/libmacaroons/blob/master/doc/format.txt
// parseBinaryV2 parses the given data in V2 format into the macaroon. The macaroon's
// internal data structures will retain references to the data. It
// returns the data after the end of the macaroon.
func (m *Macaroon) parseBinaryV2(data []byte) ([]byte, error) {
// The version has already been checked, so
// skip it.
data = data[1:]
data, section, err := parseSectionV2(data)
if err != nil {
return nil, err
}
var loc string
if len(section) > 0 && section[0].fieldType == fieldLocation {
loc = string(section[0].data)
section = section[1:]
}
if len(section) != 1 || section[0].fieldType != fieldIdentifier {
return nil, fmt.Errorf("invalid macaroon header")
}
id := section[0].data
m.init(id, loc, V2)
for {
rest, section, err := parseSectionV2(data)
if err != nil {
return nil, err
}
data = rest
if len(section) == 0 {
break
}
var cav Caveat
if len(section) > 0 && section[0].fieldType == fieldLocation {
cav.Location = string(section[0].data)
section = section[1:]
}
if len(section) == 0 || section[0].fieldType != fieldIdentifier {
return nil, fmt.Errorf("no identifier in caveat")
}
cav.Id = section[0].data
section = section[1:]
if len(section) == 0 {
// First party caveat.
if cav.Location != "" {
return nil, fmt.Errorf("location not allowed in first party caveat")
}
m.caveats = append(m.caveats, cav)
continue
}
if len(section) != 1 {
return nil, fmt.Errorf("extra fields found in caveat")
}
if section[0].fieldType != fieldVerificationId {
return nil, fmt.Errorf("invalid field found in caveat")
}
cav.VerificationId = section[0].data
m.caveats = append(m.caveats, cav)
}
data, sig, err := parsePacketV2(data)
if err != nil {
return nil, err
}
if sig.fieldType != fieldSignature {
return nil, fmt.Errorf("unexpected field found instead of signature")
}
if len(sig.data) != hashLen {
return nil, fmt.Errorf("signature has unexpected length")
}
copy(m.sig[:], sig.data)
return data, nil
}
// appendBinaryV2 appends the binary-encoded macaroon
// in v2 format to data.
func (m *Macaroon) appendBinaryV2(data []byte) []byte {
// Version byte.
data = append(data, 2)
if len(m.location) > 0 {
data = appendPacketV2(data, packetV2{
fieldType: fieldLocation,
data: []byte(m.location),
})
}
data = appendPacketV2(data, packetV2{
fieldType: fieldIdentifier,
data: m.id,
})
data = appendEOSV2(data)
for _, cav := range m.caveats {
if len(cav.Location) > 0 {
data = appendPacketV2(data, packetV2{
fieldType: fieldLocation,
data: []byte(cav.Location),
})
}
data = appendPacketV2(data, packetV2{
fieldType: fieldIdentifier,
data: cav.Id,
})
if len(cav.VerificationId) > 0 {
data = appendPacketV2(data, packetV2{
fieldType: fieldVerificationId,
data: []byte(cav.VerificationId),
})
}
data = appendEOSV2(data)
}
data = appendEOSV2(data)
data = appendPacketV2(data, packetV2{
fieldType: fieldSignature,
data: m.sig[:],
})
return data
}

View file

@ -0,0 +1,239 @@
package macaroon
import (
"encoding/base64"
"encoding/json"
"fmt"
)
// Version specifies the version of a macaroon.
// In version 1, the macaroon id and all caveats
// must be UTF-8-compatible strings, and the
// size of any part of the macaroon may not exceed
// approximately 64K. In version 2,
// all field may be arbitrary binary blobs.
type Version uint16
const (
// V1 specifies version 1 macaroons.
V1 Version = 1
// V2 specifies version 2 macaroons.
V2 Version = 2
// LatestVersion holds the latest supported version.
LatestVersion = V2
)
// String returns a string representation of the version;
// for example V1 formats as "v1".
func (v Version) String() string {
return fmt.Sprintf("v%d", v)
}
// Version returns the version of the macaroon.
func (m *Macaroon) Version() Version {
return m.version
}
// MarshalJSON implements json.Marshaler by marshaling the
// macaroon in JSON format. The serialisation format is determined
// by the macaroon's version.
func (m *Macaroon) MarshalJSON() ([]byte, error) {
switch m.version {
case V1:
return m.marshalJSONV1()
case V2:
return m.marshalJSONV2()
default:
return nil, fmt.Errorf("unknown version %v", m.version)
}
}
// UnmarshalJSON implements json.Unmarshaller by unmarshaling
// the given macaroon in JSON format. It accepts both V1 and V2
// forms encoded forms, and also a base64-encoded JSON string
// containing the binary-marshaled macaroon.
//
// After unmarshaling, the macaroon's version will reflect
// the version that it was unmarshaled as.
func (m *Macaroon) UnmarshalJSON(data []byte) error {
if data[0] == '"' {
// It's a string, so it must be a base64-encoded binary form.
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
data, err := Base64Decode([]byte(s))
if err != nil {
return err
}
if err := m.UnmarshalBinary(data); err != nil {
return err
}
return nil
}
// Not a string; try to unmarshal into both kinds of macaroon object.
// This assumes that neither format has any fields in common.
// For subsequent versions we may need to change this approach.
type MacaroonJSONV1 macaroonJSONV1
type MacaroonJSONV2 macaroonJSONV2
var both struct {
*MacaroonJSONV1
*MacaroonJSONV2
}
if err := json.Unmarshal(data, &both); err != nil {
return err
}
switch {
case both.MacaroonJSONV1 != nil && both.MacaroonJSONV2 != nil:
return fmt.Errorf("cannot determine macaroon encoding version")
case both.MacaroonJSONV1 != nil:
if err := m.initJSONV1((*macaroonJSONV1)(both.MacaroonJSONV1)); err != nil {
return err
}
m.version = V1
case both.MacaroonJSONV2 != nil:
if err := m.initJSONV2((*macaroonJSONV2)(both.MacaroonJSONV2)); err != nil {
return err
}
m.version = V2
default:
return fmt.Errorf("invalid JSON macaroon encoding")
}
return nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
// It accepts both V1 and V2 binary encodings.
func (m *Macaroon) UnmarshalBinary(data []byte) error {
// Copy the data to avoid retaining references to it
// in the internal data structures.
data = append([]byte(nil), data...)
_, err := m.parseBinary(data)
return err
}
// parseBinary parses the macaroon in binary format
// from the given data and returns where the parsed data ends.
//
// It retains references to data.
func (m *Macaroon) parseBinary(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty macaroon data")
}
v := data[0]
if v == 2 {
// Version 2 binary format.
data, err := m.parseBinaryV2(data)
if err != nil {
return nil, fmt.Errorf("unmarshal v2: %v", err)
}
m.version = V2
return data, nil
}
if isASCIIHex(v) {
// It's a hex digit - version 1 binary format
data, err := m.parseBinaryV1(data)
if err != nil {
return nil, fmt.Errorf("unmarshal v1: %v", err)
}
m.version = V1
return data, nil
}
return nil, fmt.Errorf("cannot determine data format of binary-encoded macaroon")
}
// MarshalBinary implements encoding.BinaryMarshaler by
// formatting the macaroon according to the version specified
// by MarshalAs.
func (m *Macaroon) MarshalBinary() ([]byte, error) {
return m.appendBinary(nil)
}
// appendBinary appends the binary-formatted macaroon to
// the given data, formatting it according to the macaroon's
// version.
func (m *Macaroon) appendBinary(data []byte) ([]byte, error) {
switch m.version {
case V1:
return m.appendBinaryV1(data)
case V2:
return m.appendBinaryV2(data), nil
default:
return nil, fmt.Errorf("bad macaroon version %v", m.version)
}
}
// Slice defines a collection of macaroons. By convention, the
// first macaroon in the slice is a primary macaroon and the rest
// are discharges for its third party caveats.
type Slice []*Macaroon
// MarshalBinary implements encoding.BinaryMarshaler.
func (s Slice) MarshalBinary() ([]byte, error) {
var data []byte
var err error
for _, m := range s {
data, err = m.appendBinary(data)
if err != nil {
return nil, fmt.Errorf("failed to marshal macaroon %q: %v", m.Id(), err)
}
}
return data, nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
// It accepts all known binary encodings for the data - all the
// embedded macaroons need not be encoded in the same format.
func (s *Slice) UnmarshalBinary(data []byte) error {
// Prevent the internal data structures from holding onto the
// slice by copying it first.
data = append([]byte(nil), data...)
*s = (*s)[:0]
for len(data) > 0 {
var m Macaroon
rest, err := m.parseBinary(data)
if err != nil {
return fmt.Errorf("cannot unmarshal macaroon: %v", err)
}
*s = append(*s, &m)
data = rest
}
return nil
}
const (
padded = 1 << iota
stdEncoding
)
var codecs = [4]*base64.Encoding{
0: base64.RawURLEncoding,
padded: base64.URLEncoding,
stdEncoding: base64.RawStdEncoding,
stdEncoding | padded: base64.StdEncoding,
}
// Base64Decode base64-decodes the given data.
// It accepts both standard and URL encodings, both
// padded and unpadded.
func Base64Decode(data []byte) ([]byte, error) {
encoding := 0
if len(data) > 0 && data[len(data)-1] == '=' {
encoding |= padded
}
for _, b := range data {
if b == '/' || b == '+' {
encoding |= stdEncoding
break
}
}
codec := codecs[encoding]
buf := make([]byte, codec.DecodedLen(len(data)))
n, err := codec.Decode(buf, data)
if err == nil {
return buf[0:n], nil
}
return nil, err
}

View file

@ -0,0 +1,203 @@
package macaroon_test
import (
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
"gopkg.in/macaroon.v2"
)
type marshalSuite struct{}
var _ = gc.Suite(&marshalSuite{})
func (s *marshalSuite) TestMarshalUnmarshalMacaroonV1(c *gc.C) {
s.testMarshalUnmarshalWithVersion(c, macaroon.V1)
}
func (s *marshalSuite) TestMarshalUnmarshalMacaroonV2(c *gc.C) {
s.testMarshalUnmarshalWithVersion(c, macaroon.V2)
}
func (*marshalSuite) testMarshalUnmarshalWithVersion(c *gc.C, vers macaroon.Version) {
rootKey := []byte("secret")
m := MustNew(rootKey, []byte("some id"), "a location", vers)
// Adding the third party caveat before the first party caveat
// tests a former bug where the caveat wasn't zeroed
// before moving to the next caveat.
err := m.AddThirdPartyCaveat([]byte("shared root key"), []byte("3rd party caveat"), "remote.com")
c.Assert(err, gc.IsNil)
err = m.AddFirstPartyCaveat([]byte("a caveat"))
c.Assert(err, gc.IsNil)
b, err := m.MarshalBinary()
c.Assert(err, gc.IsNil)
var um macaroon.Macaroon
err = um.UnmarshalBinary(b)
c.Assert(err, gc.IsNil)
c.Assert(um.Location(), gc.Equals, m.Location())
c.Assert(string(um.Id()), gc.Equals, string(m.Id()))
c.Assert(um.Signature(), jc.DeepEquals, m.Signature())
c.Assert(um.Caveats(), jc.DeepEquals, m.Caveats())
c.Assert(um.Version(), gc.Equals, vers)
um.SetVersion(m.Version())
c.Assert(m, jc.DeepEquals, &um)
}
func (s *marshalSuite) TestMarshalBinaryRoundTrip(c *gc.C) {
// This data holds the V2 binary encoding of
data := []byte(
"\x02" +
"\x01\x0ehttp://mybank/" +
"\x02\x1cwe used our other secret key" +
"\x00" +
"\x02\x14account = 3735928559" +
"\x00" +
"\x01\x13http://auth.mybank/" +
"\x02'this was how we remind auth of key/pred" +
"\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" +
"\x00" +
"\x00" +
"\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",
)
var m macaroon.Macaroon
err := m.UnmarshalBinary(data)
c.Assert(err, gc.Equals, nil)
assertLibMacaroonsMacaroon(c, &m)
c.Assert(m.Version(), gc.Equals, macaroon.V2)
data1, err := m.MarshalBinary()
c.Assert(err, gc.Equals, nil)
c.Assert(data1, jc.DeepEquals, data)
}
func (s *marshalSuite) TestMarshalUnmarshalSliceV1(c *gc.C) {
s.testMarshalUnmarshalSliceWithVersion(c, macaroon.V1)
}
func (s *marshalSuite) TestMarshalUnmarshalSliceV2(c *gc.C) {
s.testMarshalUnmarshalSliceWithVersion(c, macaroon.V2)
}
func (*marshalSuite) testMarshalUnmarshalSliceWithVersion(c *gc.C, vers macaroon.Version) {
rootKey := []byte("secret")
m1 := MustNew(rootKey, []byte("some id"), "a location", vers)
m2 := MustNew(rootKey, []byte("some other id"), "another location", vers)
err := m1.AddFirstPartyCaveat([]byte("a caveat"))
c.Assert(err, gc.IsNil)
err = m2.AddFirstPartyCaveat([]byte("another caveat"))
c.Assert(err, gc.IsNil)
macaroons := macaroon.Slice{m1, m2}
b, err := macaroons.MarshalBinary()
c.Assert(err, gc.IsNil)
var unmarshaledMacs macaroon.Slice
err = unmarshaledMacs.UnmarshalBinary(b)
c.Assert(err, gc.IsNil)
c.Assert(unmarshaledMacs, gc.HasLen, len(macaroons))
for i, m := range macaroons {
um := unmarshaledMacs[i]
c.Assert(um.Location(), gc.Equals, m.Location())
c.Assert(string(um.Id()), gc.Equals, string(m.Id()))
c.Assert(um.Signature(), jc.DeepEquals, m.Signature())
c.Assert(um.Caveats(), jc.DeepEquals, m.Caveats())
c.Assert(um.Version(), gc.Equals, vers)
um.SetVersion(m.Version())
}
c.Assert(macaroons, jc.DeepEquals, unmarshaledMacs)
// Check that appending a caveat to the first does not
// affect the second.
for i := 0; i < 10; i++ {
err = unmarshaledMacs[0].AddFirstPartyCaveat([]byte("caveat"))
c.Assert(err, gc.IsNil)
}
unmarshaledMacs[1].SetVersion(macaroons[1].Version())
c.Assert(unmarshaledMacs[1], jc.DeepEquals, macaroons[1])
c.Assert(err, gc.IsNil)
}
func (s *marshalSuite) TestSliceRoundTripV1(c *gc.C) {
s.testSliceRoundTripWithVersion(c, macaroon.V1)
}
func (s *marshalSuite) TestSliceRoundTripV2(c *gc.C) {
s.testSliceRoundTripWithVersion(c, macaroon.V2)
}
func (*marshalSuite) testSliceRoundTripWithVersion(c *gc.C, vers macaroon.Version) {
rootKey := []byte("secret")
m1 := MustNew(rootKey, []byte("some id"), "a location", vers)
m2 := MustNew(rootKey, []byte("some other id"), "another location", vers)
err := m1.AddFirstPartyCaveat([]byte("a caveat"))
c.Assert(err, gc.IsNil)
err = m2.AddFirstPartyCaveat([]byte("another caveat"))
c.Assert(err, gc.IsNil)
macaroons := macaroon.Slice{m1, m2}
b, err := macaroons.MarshalBinary()
c.Assert(err, gc.IsNil)
var unmarshaledMacs macaroon.Slice
err = unmarshaledMacs.UnmarshalBinary(b)
c.Assert(err, gc.IsNil)
marshaledMacs, err := unmarshaledMacs.MarshalBinary()
c.Assert(err, gc.IsNil)
c.Assert(b, jc.DeepEquals, marshaledMacs)
}
var base64DecodeTests = []struct {
about string
input string
expect string
expectError string
}{{
about: "empty string",
input: "",
expect: "",
}, {
about: "standard encoding, padded",
input: "Z29+IQ==",
expect: "go~!",
}, {
about: "URL encoding, padded",
input: "Z29-IQ==",
expect: "go~!",
}, {
about: "standard encoding, not padded",
input: "Z29+IQ",
expect: "go~!",
}, {
about: "URL encoding, not padded",
input: "Z29-IQ",
expect: "go~!",
}, {
about: "standard encoding, too much padding",
input: "Z29+IQ===",
expectError: `illegal base64 data at input byte 8`,
}}
func (*marshalSuite) TestBase64Decode(c *gc.C) {
for i, test := range base64DecodeTests {
c.Logf("test %d: %s", i, test.about)
out, err := macaroon.Base64Decode([]byte(test.input))
if test.expectError != "" {
c.Assert(err, gc.ErrorMatches, test.expectError)
} else {
c.Assert(err, gc.Equals, nil)
c.Assert(string(out), gc.Equals, test.expect)
}
}
}

View file

@ -0,0 +1,133 @@
package macaroon
import (
"bytes"
"fmt"
)
// field names, as defined in libmacaroons
const (
fieldNameLocation = "location"
fieldNameIdentifier = "identifier"
fieldNameSignature = "signature"
fieldNameCaveatId = "cid"
fieldNameVerificationId = "vid"
fieldNameCaveatLocation = "cl"
)
// maxPacketV1Len is the maximum allowed length of a packet in the v1 macaroon
// serialization format.
const maxPacketV1Len = 0xffff
// The original macaroon binary encoding is made from a sequence
// of "packets", each of which has a field name and some data.
// The encoding is:
//
// - four ascii hex digits holding the entire packet size (including
// the digits themselves).
//
// - the field name, followed by an ascii space.
//
// - the raw data
//
// - a newline (\n) character
//
// The packet struct below holds a reference into Macaroon.data.
type packetV1 struct {
// ftype holds the field name of the packet.
fieldName []byte
// data holds the packet's data.
data []byte
// len holds the total length in bytes
// of the packet, including any header.
totalLen int
}
// parsePacket parses the packet at the start of the
// given data.
func parsePacketV1(data []byte) (packetV1, error) {
if len(data) < 6 {
return packetV1{}, fmt.Errorf("packet too short")
}
plen, ok := parseSizeV1(data)
if !ok {
return packetV1{}, fmt.Errorf("cannot parse size")
}
if plen > len(data) {
return packetV1{}, fmt.Errorf("packet size too big")
}
if plen < 4 {
return packetV1{}, fmt.Errorf("packet size too small")
}
data = data[4:plen]
i := bytes.IndexByte(data, ' ')
if i <= 0 {
return packetV1{}, fmt.Errorf("cannot parse field name")
}
fieldName := data[0:i]
if data[len(data)-1] != '\n' {
return packetV1{}, fmt.Errorf("no terminating newline found")
}
return packetV1{
fieldName: fieldName,
data: data[i+1 : len(data)-1],
totalLen: plen,
}, nil
}
// appendPacketV1 appends a packet with the given field name
// and data to the given buffer. If the field and data were
// too long to be encoded, it returns nil, false; otherwise
// it returns the appended buffer.
func appendPacketV1(buf []byte, field string, data []byte) ([]byte, bool) {
plen := packetV1Size(field, data)
if plen > maxPacketV1Len {
return nil, false
}
buf = appendSizeV1(buf, plen)
buf = append(buf, field...)
buf = append(buf, ' ')
buf = append(buf, data...)
buf = append(buf, '\n')
return buf, true
}
func packetV1Size(field string, data []byte) int {
return 4 + len(field) + 1 + len(data) + 1
}
var hexDigits = []byte("0123456789abcdef")
func appendSizeV1(data []byte, size int) []byte {
return append(data,
hexDigits[size>>12],
hexDigits[(size>>8)&0xf],
hexDigits[(size>>4)&0xf],
hexDigits[size&0xf],
)
}
func parseSizeV1(data []byte) (int, bool) {
d0, ok0 := asciiHex(data[0])
d1, ok1 := asciiHex(data[1])
d2, ok2 := asciiHex(data[2])
d3, ok3 := asciiHex(data[3])
return d0<<12 + d1<<8 + d2<<4 + d3, ok0 && ok1 && ok2 && ok3
}
func asciiHex(b byte) (int, bool) {
switch {
case b >= '0' && b <= '9':
return int(b) - '0', true
case b >= 'a' && b <= 'f':
return int(b) - 'a' + 0xa, true
}
return 0, false
}
func isASCIIHex(b byte) bool {
_, ok := asciiHex(b)
return ok
}

View file

@ -0,0 +1,98 @@
package macaroon
import (
"strconv"
"strings"
"unicode"
gc "gopkg.in/check.v1"
)
type packetV1Suite struct{}
var _ = gc.Suite(&packetV1Suite{})
func (*packetV1Suite) TestAppendPacket(c *gc.C) {
data, ok := appendPacketV1(nil, "field", []byte("some data"))
c.Assert(ok, gc.Equals, true)
c.Assert(string(data), gc.Equals, "0014field some data\n")
data, ok = appendPacketV1(data, "otherfield", []byte("more and more data"))
c.Assert(ok, gc.Equals, true)
c.Assert(string(data), gc.Equals, "0014field some data\n0022otherfield more and more data\n")
}
func (*packetV1Suite) TestAppendPacketTooBig(c *gc.C) {
data, ok := appendPacketV1(nil, "field", make([]byte, 65532))
c.Assert(ok, gc.Equals, false)
c.Assert(data, gc.IsNil)
}
var parsePacketV1Tests = []struct {
data string
expect packetV1
expectErr string
}{{
expectErr: "packet too short",
}, {
data: "0014field some data\n",
expect: packetV1{
fieldName: []byte("field"),
data: []byte("some data"),
totalLen: 20,
},
}, {
data: "0015field some data\n",
expectErr: "packet size too big",
}, {
data: "0003a\n",
expectErr: "packet size too small",
}, {
data: "0014fieldwithoutanyspaceordata\n",
expectErr: "cannot parse field name",
}, {
data: "fedcsomefield " + strings.Repeat("x", 0xfedc-len("0000somefield \n")) + "\n",
expect: packetV1{
fieldName: []byte("somefield"),
data: []byte(strings.Repeat("x", 0xfedc-len("0000somefield \n"))),
totalLen: 0xfedc,
},
}, {
data: "zzzzbadpacketsizenomacaroon",
expectErr: "cannot parse size",
}}
func (*packetV1Suite) TestParsePacketV1(c *gc.C) {
for i, test := range parsePacketV1Tests {
c.Logf("test %d: %q", i, truncate(test.data))
p, err := parsePacketV1([]byte(test.data))
if test.expectErr != "" {
c.Assert(err, gc.ErrorMatches, test.expectErr)
c.Assert(p, gc.DeepEquals, packetV1{})
continue
}
c.Assert(err, gc.IsNil)
c.Assert(p, gc.DeepEquals, test.expect)
}
}
func truncate(d string) string {
if len(d) > 50 {
return d[0:50] + "..."
}
return d
}
func (*packetV1Suite) TestAsciiHex(c *gc.C) {
for b := 0; b < 256; b++ {
n, err := strconv.ParseInt(string(b), 16, 8)
value, ok := asciiHex(byte(b))
if err != nil || unicode.IsUpper(rune(b)) {
c.Assert(ok, gc.Equals, false)
c.Assert(value, gc.Equals, 0)
} else {
c.Assert(ok, gc.Equals, true)
c.Assert(value, gc.Equals, int(n))
}
}
}

View file

@ -0,0 +1,117 @@
package macaroon
import (
"encoding/binary"
"fmt"
)
type fieldType int
// Field constants as used in the binary encoding.
const (
fieldEOS fieldType = 0
fieldLocation fieldType = 1
fieldIdentifier fieldType = 2
fieldVerificationId fieldType = 4
fieldSignature fieldType = 6
)
type packetV2 struct {
// fieldType holds the type of the field.
fieldType fieldType
// data holds the packet's data.
data []byte
}
// parseSectionV2 parses a sequence of packets
// in data. The sequence is terminated by a packet
// with a field type of fieldEOS.
func parseSectionV2(data []byte) ([]byte, []packetV2, error) {
prevFieldType := fieldType(-1)
var packets []packetV2
for {
if len(data) == 0 {
return nil, nil, fmt.Errorf("section extends past end of buffer")
}
rest, p, err := parsePacketV2(data)
if err != nil {
return nil, nil, err
}
if p.fieldType == fieldEOS {
return rest, packets, nil
}
if p.fieldType <= prevFieldType {
return nil, nil, fmt.Errorf("fields out of order")
}
packets = append(packets, p)
prevFieldType = p.fieldType
data = rest
}
}
// parsePacketV2 parses a V2 data package at the start
// of the given data.
// The format of a packet is as follows:
//
// fieldType(varint) payloadLen(varint) data[payloadLen bytes]
//
// apart from fieldEOS which has no payloadLen or data (it's
// a single zero byte).
func parsePacketV2(data []byte) ([]byte, packetV2, error) {
data, ft, err := parseVarint(data)
if err != nil {
return nil, packetV2{}, err
}
p := packetV2{
fieldType: fieldType(ft),
}
if p.fieldType == fieldEOS {
return data, p, nil
}
data, payloadLen, err := parseVarint(data)
if err != nil {
return nil, packetV2{}, err
}
if payloadLen > len(data) {
return nil, packetV2{}, fmt.Errorf("field data extends past end of buffer")
}
p.data = data[0:payloadLen]
return data[payloadLen:], p, nil
}
// parseVarint parses the variable-length integer
// at the start of the given data and returns rest
// of the buffer and the number.
func parseVarint(data []byte) ([]byte, int, error) {
val, n := binary.Uvarint(data)
if n > 0 {
if val > 0x7fffffff {
return nil, 0, fmt.Errorf("varint value out of range")
}
return data[n:], int(val), nil
}
if n == 0 {
return nil, 0, fmt.Errorf("varint value extends past end of buffer")
}
return nil, 0, fmt.Errorf("varint value out of range")
}
func appendPacketV2(data []byte, p packetV2) []byte {
data = appendVarint(data, int(p.fieldType))
if p.fieldType != fieldEOS {
data = appendVarint(data, len(p.data))
data = append(data, p.data...)
}
return data
}
func appendEOSV2(data []byte) []byte {
return append(data, 0)
}
func appendVarint(data []byte, x int) []byte {
var buf [binary.MaxVarintLen32]byte
n := binary.PutUvarint(buf[:], uint64(x))
return append(data, buf[:n]...)
}

View file

@ -0,0 +1,126 @@
package macaroon
import (
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
)
type packetV2Suite struct{}
var _ = gc.Suite(&packetV2Suite{})
var parsePacketV2Tests = []struct {
about string
data string
expectPacket packetV2
expectData string
expectError string
}{{
about: "EOS packet",
data: "\x00",
expectPacket: packetV2{
fieldType: fieldEOS,
},
}, {
about: "simple field",
data: "\x02\x03xyz",
expectPacket: packetV2{
fieldType: 2,
data: []byte("xyz"),
},
}, {
about: "empty buffer",
data: "",
expectError: "varint value extends past end of buffer",
}, {
about: "varint out of range",
data: "\xff\xff\xff\xff\xff\xff\x7f",
expectError: "varint value out of range",
}, {
about: "varint way out of range",
data: "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x7f",
expectError: "varint value out of range",
}, {
about: "unterminated varint",
data: "\x80",
expectError: "varint value extends past end of buffer",
}, {
about: "field data too long",
data: "\x01\x02a",
expectError: "field data extends past end of buffer",
}, {
about: "bad data length varint",
data: "\x01\xff",
expectError: "varint value extends past end of buffer",
}}
func (*packetV2Suite) TestParsePacketV2(c *gc.C) {
for i, test := range parsePacketV2Tests {
c.Logf("test %d: %v", i, test.about)
data, p, err := parsePacketV2([]byte(test.data))
if test.expectError != "" {
c.Assert(err, gc.ErrorMatches, test.expectError)
c.Assert(data, gc.IsNil)
c.Assert(p, gc.DeepEquals, packetV2{})
} else {
c.Assert(err, gc.IsNil)
c.Assert(p, jc.DeepEquals, test.expectPacket)
}
}
}
var parseSectionV2Tests = []struct {
about string
data string
expectData string
expectPackets []packetV2
expectError string
}{{
about: "no packets",
data: "\x00",
}, {
about: "one packet",
data: "\x02\x03xyz\x00",
expectPackets: []packetV2{{
fieldType: 2,
data: []byte("xyz"),
}},
}, {
about: "two packets",
data: "\x02\x03xyz\x07\x05abcde\x00",
expectPackets: []packetV2{{
fieldType: 2,
data: []byte("xyz"),
}, {
fieldType: 7,
data: []byte("abcde"),
}},
}, {
about: "unterminated section",
data: "\x02\x03xyz\x07\x05abcde",
expectError: "section extends past end of buffer",
}, {
about: "out of order fields",
data: "\x07\x05abcde\x02\x03xyz\x00",
expectError: "fields out of order",
}, {
about: "bad packet",
data: "\x07\x05abcde\xff",
expectError: "varint value extends past end of buffer",
}}
func (*packetV2Suite) TestParseSectionV2(c *gc.C) {
for i, test := range parseSectionV2Tests {
c.Logf("test %d: %v", i, test.about)
data, ps, err := parseSectionV2([]byte(test.data))
if test.expectError != "" {
c.Assert(err, gc.ErrorMatches, test.expectError)
c.Assert(data, gc.IsNil)
c.Assert(ps, gc.IsNil)
} else {
c.Assert(err, gc.IsNil)
c.Assert(ps, jc.DeepEquals, test.expectPackets)
}
}
}

View file

@ -0,0 +1,102 @@
package macaroon
import (
"fmt"
)
// Trace holds all toperations involved in verifying a macaroon,
// and the root key used as the initial verification key.
// This can be useful for debugging macaroon implementations.
type Trace struct {
RootKey []byte
Ops []TraceOp
}
// Results returns the output from all operations in the Trace.
// The result from ts.Ops[i] will be in the i'th element of the
// returned slice.
// When a trace has resulted in a failure, the
// last element will be nil.
func (t Trace) Results() [][]byte {
r := make([][]byte, len(t.Ops))
input := t.RootKey
for i, op := range t.Ops {
input = op.Result(input)
r[i] = input
}
return r
}
// TraceOp holds one possible operation when verifying a macaroon.
type TraceOp struct {
Kind TraceOpKind `json:"kind"`
Data1 []byte `json:"data1,omitempty"`
Data2 []byte `json:"data2,omitempty"`
}
// Result returns the result of computing the given
// operation with the given input data.
// If op is TraceFail, it returns nil.
func (op TraceOp) Result(input []byte) []byte {
switch op.Kind {
case TraceMakeKey:
return makeKey(input)[:]
case TraceHash:
if len(op.Data2) == 0 {
return keyedHash(bytesToKey(input), op.Data1)[:]
}
return keyedHash2(bytesToKey(input), op.Data1, op.Data2)[:]
case TraceBind:
return bindForRequest(op.Data1, bytesToKey(input))[:]
case TraceFail:
return nil
default:
panic(fmt.Errorf("unknown trace operation kind %d", op.Kind))
}
}
func bytesToKey(data []byte) *[keyLen]byte {
var key [keyLen]byte
if len(data) != keyLen {
panic(fmt.Errorf("unexpected input key length; got %d want %d", len(data), keyLen))
}
copy(key[:], data)
return &key
}
// TraceOpKind represents the kind of a macaroon verification operation.
type TraceOpKind int
const (
TraceInvalid = TraceOpKind(iota)
// TraceMakeKey represents the operation of calculating a
// fixed length root key from the variable length input key.
TraceMakeKey
// TraceHash represents a keyed hash operation with one
// or two values. If there is only one value, it will be in Data1.
TraceHash
// TraceBind represents the operation of binding a discharge macaroon
// to its primary macaroon. Data1 holds the signature of the primary
// macaroon.
TraceBind
// TraceFail represents a verification failure. If present, this will always
// be the last operation in a trace.
TraceFail
)
var traceOps = []string{
TraceInvalid: "invalid",
TraceMakeKey: "makekey",
TraceHash: "hash",
TraceBind: "bind",
TraceFail: "fail",
}
// String returns a string representation of the operation.
func (k TraceOpKind) String() string {
return traceOps[k]
}