diff --git a/pkg/macaroon/macaroon.go b/pkg/macaroon/macaroon.go new file mode 100644 index 000000000..f90846cce --- /dev/null +++ b/pkg/macaroon/macaroon.go @@ -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(), + } +} diff --git a/pkg/macaroon/macaroon_test.go b/pkg/macaroon/macaroon_test.go new file mode 100644 index 000000000..89c676a91 --- /dev/null +++ b/pkg/macaroon/macaroon_test.go @@ -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) + }) +} diff --git a/pkg/macaroon/serialize.go b/pkg/macaroon/serialize.go new file mode 100644 index 000000000..5ba22df57 --- /dev/null +++ b/pkg/macaroon/serialize.go @@ -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 +}