Macaroon Library (#1537)

* Added initial implementation of Macaroon Library.

* Implemented Unit tests.

* Added header.

* Serialization refactoring.

* Added description for Macaroon and Caveat struct.

* Removed NewNonce. Use NewSecret instead.

* change macaroon library to just use bytes directly

Change-Id: I0411203cb09244605d2ee49f9d9b9b1e2bf46c76

* linting

Change-Id: I0363c0e30b610966eb18ff8b3905d75c69541610
This commit is contained in:
Bogdan Artemenko 2019-05-01 15:40:56 +03:00 committed by Brandon Iglesias
parent 7d33a2042d
commit ecb81144a1
3 changed files with 415 additions and 0 deletions

127
pkg/macaroon/macaroon.go Normal file
View File

@ -0,0 +1,127 @@
// Copyright (C) 2019 Storj Labs, Inc. // See LICENSE for copying information.
package macaroon
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
)
// Macaroon is a struct that determine contextual caveats and authorization
type Macaroon struct {
head []byte
caveats [][]byte
tail []byte
}
// NewUnrestricted creates Macaroon with random Head and generated Tail
func NewUnrestricted(secret []byte) (*Macaroon, error) {
head, err := NewSecret()
if err != nil {
return nil, err
}
return &Macaroon{
head: head,
tail: sign(secret, head),
}, nil
}
func sign(secret []byte, data []byte) []byte {
signer := hmac.New(sha256.New, secret)
_, err := signer.Write(data)
if err != nil {
// Error skipped because sha256 does not return error
panic(err)
}
return signer.Sum(nil)
}
// NewSecret generates cryptographically random 32 bytes
func NewSecret() (secret []byte, err error) {
secret = make([]byte, 32)
_, err = rand.Read(secret)
if err != nil {
return nil, err
}
return secret, nil
}
// AddFirstPartyCaveat creates signed macaroon with appended caveat
func (m *Macaroon) AddFirstPartyCaveat(c []byte) (macaroon *Macaroon, err error) {
macaroon = m.Copy()
macaroon.caveats = append(macaroon.caveats, c)
macaroon.tail = sign(macaroon.tail, c)
return macaroon, nil
}
// Validate reconstructs with all caveats from the secret and compares tails,
// returning true if the tails match
func (m *Macaroon) Validate(secret []byte) (ok bool) {
tail := sign(secret, m.head)
for _, cav := range m.caveats {
tail = sign(tail, cav)
}
return subtle.ConstantTimeCompare(tail, m.tail) == 1
}
// Tails returns all ancestor tails up to and including the current tail
func (m *Macaroon) Tails(secret []byte) [][]byte {
tails := make([][]byte, 0, len(m.caveats)+1)
tail := sign(secret, m.head)
tails = append(tails, tail)
for _, cav := range m.caveats {
tail = sign(tail, cav)
tails = append(tails, tail)
}
return tails
}
// Head returns copy of macaroon head
func (m *Macaroon) Head() (head []byte) {
if len(m.head) == 0 {
return nil
}
return append([]byte(nil), m.head...)
}
// CaveatLen returns the number of caveats this macaroon has
func (m *Macaroon) CaveatLen() int {
return len(m.caveats)
}
// Caveats returns copy of macaroon caveats
func (m *Macaroon) Caveats() (caveats [][]byte) {
if len(m.caveats) == 0 {
return nil
}
caveats = make([][]byte, 0, len(m.caveats))
for _, cav := range m.caveats {
caveats = append(caveats, append([]byte(nil), cav...))
}
return caveats
}
// Tail returns copy of macaroon tail
func (m *Macaroon) Tail() (tail []byte) {
if len(m.tail) == 0 {
return nil
}
return append([]byte(nil), m.tail...)
}
// Copy return copy of macaroon
func (m *Macaroon) Copy() *Macaroon {
return &Macaroon{
head: m.Head(),
caveats: m.Caveats(),
tail: m.Tail(),
}
}

View File

@ -0,0 +1,83 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package macaroon_test
import (
"testing"
assert "github.com/stretchr/testify/require"
"storj.io/storj/pkg/macaroon"
)
func TestNilMacaroon(t *testing.T) {
mac, err := macaroon.NewUnrestricted(nil)
assert.NoError(t, err)
assert.NotNil(t, mac)
data := mac.Serialize()
assert.NotNil(t, data)
assert.NotEmpty(t, data)
mac2, err := macaroon.ParseMacaroon(data)
assert.NoError(t, err)
assert.NotNil(t, mac2)
assert.Equal(t, mac, mac2)
t.Run("Successful add Caveat", func(t *testing.T) {
mac, err = mac.AddFirstPartyCaveat([]byte("cav1"))
assert.NotNil(t, mac)
assert.NoError(t, err)
assert.Equal(t, len(mac.Caveats()), 1)
})
t.Run("Successful serialization", func(t *testing.T) {
data := mac.Serialize()
assert.NotNil(t, data)
assert.NotEmpty(t, data)
mac2, err := macaroon.ParseMacaroon(data)
assert.NotNil(t, mac2)
assert.NoError(t, err)
assert.Equal(t, mac, mac2)
})
}
func TestMacaroon(t *testing.T) {
secret, err := macaroon.NewSecret()
assert.NoError(t, err)
assert.NotNil(t, secret)
assert.Equal(t, len(secret), 32)
mac, err := macaroon.NewUnrestricted(secret)
assert.NoError(t, err)
assert.NotNil(t, mac)
nonce := mac.Head()
assert.NotNil(t, nonce)
assert.Equal(t, len(nonce), 32)
t.Run("Successful add Caveat", func(t *testing.T) {
mac, err = mac.AddFirstPartyCaveat([]byte("cav1"))
assert.NotNil(t, mac)
assert.NoError(t, err)
assert.Equal(t, len(mac.Caveats()), 1)
})
t.Run("Successful serialization", func(t *testing.T) {
data := mac.Serialize()
assert.NotNil(t, data)
assert.NotEmpty(t, data)
mac2, err := macaroon.ParseMacaroon(data)
assert.NotNil(t, mac2)
assert.NoError(t, err)
assert.Equal(t, mac, mac2)
})
t.Run("Successful Unpack", func(t *testing.T) {
ok := mac.Validate(secret)
assert.True(t, ok)
c := mac.Caveats()
assert.NotNil(t, c)
assert.NotEmpty(t, c)
})
}

205
pkg/macaroon/serialize.go Normal file
View File

@ -0,0 +1,205 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package macaroon
import (
"encoding/binary"
"errors"
)
type fieldType int
const (
fieldEOS fieldType = 0
fieldLocation fieldType = 1
fieldIdentifier fieldType = 2
fieldVerificationID fieldType = 4
fieldSignature fieldType = 6
)
type packet struct {
fieldType fieldType
data []byte
}
// Serialize converts macaroon to binary format
func (m *Macaroon) Serialize() (data []byte) {
// Start data from version int
data = append(data, 2)
// Serilize Identity
data = serializePacket(data, packet{
fieldType: fieldIdentifier,
data: m.head,
})
data = append(data, 0)
// Serialize caveats
for _, cav := range m.caveats {
data = serializePacket(data, packet{
fieldType: fieldIdentifier,
data: cav,
})
data = append(data, 0)
}
data = append(data, 0)
// Serialize tail
data = serializePacket(data, packet{
fieldType: fieldSignature,
data: m.tail,
})
return data
}
// serializePacket converts packet to binary
func serializePacket(data []byte, p packet) []byte {
data = appendVarint(data, int(p.fieldType))
data = appendVarint(data, len(p.data))
data = append(data, p.data...)
return data
}
func appendVarint(data []byte, x int) []byte {
var buf [binary.MaxVarintLen32]byte
n := binary.PutUvarint(buf[:], uint64(x))
return append(data, buf[:n]...)
}
// ParseMacaroon converts binary to macaroon
func ParseMacaroon(data []byte) (*Macaroon, error) {
// skip version
data = data[1:]
// Parse Location
data, section, err := parseSection(data)
if err != nil {
return nil, err
}
if len(section) > 0 && section[0].fieldType == fieldLocation {
section = section[1:]
}
if len(section) != 1 || section[0].fieldType != fieldIdentifier {
return nil, errors.New("invalid macaroon header")
}
mac := Macaroon{}
mac.head = section[0].data
for {
rest, section, err := parseSection(data)
if err != nil {
return nil, err
}
data = rest
if len(section) == 0 {
break
}
if len(section) > 0 && section[0].fieldType == fieldLocation {
section = section[1:]
}
if len(section) == 0 || section[0].fieldType != fieldIdentifier {
return nil, errors.New("no Identifier in caveat")
}
cav := append([]byte(nil), section[0].data...)
section = section[1:]
if len(section) == 0 {
// First party caveat.
//if cav.Location != "" {
// return nil, errors.New("location not allowed in first party caveat")
//}
mac.caveats = append(mac.caveats, cav)
continue
}
if len(section) != 1 {
return nil, errors.New("extra fields found in caveat")
}
if section[0].fieldType != fieldVerificationID {
return nil, errors.New("invalid field found in caveat")
}
//cav.VerificationId = section[0].data
mac.caveats = append(mac.caveats, cav)
}
data, sig, err := parsePacket(data)
if err != nil {
return nil, err
}
if sig.fieldType != fieldSignature {
return nil, errors.New("unexpected field found instead of signature")
}
if len(sig.data) != 32 {
return nil, errors.New("signature has unexpected length")
}
mac.tail = make([]byte, 32)
copy(mac.tail[:], sig.data)
//return data, nil
// Parse Identity
// Parse caveats
// Parse tail
return &mac, nil
}
// parseSection returns data leftover and packet array
func parseSection(data []byte) ([]byte, []packet, error) {
prevFieldType := fieldType(-1)
var packets []packet
for {
if len(data) == 0 {
return nil, nil, errors.New("section extends past end of buffer")
}
rest, p, err := parsePacket(data)
if err != nil {
return nil, nil, err
}
if p.fieldType == fieldEOS {
return rest, packets, nil
}
if p.fieldType <= prevFieldType {
return nil, nil, errors.New("fields out of order")
}
packets = append(packets, p)
prevFieldType = p.fieldType
data = rest
}
}
// parsePacket returns data leftover and packet
func parsePacket(data []byte) ([]byte, packet, error) {
data, ft, err := parseVarint(data)
if err != nil {
return nil, packet{}, err
}
p := packet{fieldType: fieldType(ft)}
if p.fieldType == fieldEOS {
return data, p, nil
}
data, packLen, err := parseVarint(data)
if err != nil {
return nil, packet{}, err
}
if packLen > len(data) {
return nil, packet{}, errors.New("out of bounds")
}
if packLen == 0 {
p.data = nil
return data, p, nil
}
p.data = data[0:packLen]
return data[packLen:], p, nil
}
func parseVarint(data []byte) ([]byte, int, error) {
value, n := binary.Uvarint(data)
if n <= 0 || value > 0x7fffffff {
return nil, 0, errors.New("varint error")
}
return data[n:], int(value), nil
}