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:
parent
7d33a2042d
commit
ecb81144a1
127
pkg/macaroon/macaroon.go
Normal file
127
pkg/macaroon/macaroon.go
Normal 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(),
|
||||
}
|
||||
}
|
83
pkg/macaroon/macaroon_test.go
Normal file
83
pkg/macaroon/macaroon_test.go
Normal 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
205
pkg/macaroon/serialize.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user