garm/vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/oven.go
Gabriel Adrian Samfira c61b7fd268
Update go modules
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2023-03-12 16:22:37 +02:00

359 lines
10 KiB
Go

package bakery
import (
"bytes"
"context"
"encoding/base64"
"sort"
"github.com/go-macaroon-bakery/macaroonpb"
"github.com/rogpeppe/fastuuid"
"gopkg.in/errgo.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// MacaroonVerifier verifies macaroons and returns the operations and
// caveats they're associated with.
type MacaroonVerifier interface {
// VerifyMacaroon verifies the signature of the given macaroon and returns
// information on its associated operations, and all the first party
// caveat conditions that need to be checked.
//
// This method should not check first party caveats itself.
//
// It should return a *VerificationError if the error occurred
// because the macaroon signature failed or the root key
// was not found - any other error will be treated as fatal
// by Checker and cause authorization to terminate.
VerifyMacaroon(ctx context.Context, ms macaroon.Slice) ([]Op, []string, error)
}
var uuidGen = fastuuid.MustNewGenerator()
// Oven bakes macaroons. They emerge sweet and delicious
// and ready for use in a Checker.
//
// All macaroons are associated with one or more operations (see
// the Op type) which define the capabilities of the macaroon.
//
// There is one special operation, "login" (defined by LoginOp)
// which grants the capability to speak for a particular user.
// The login capability will never be mixed with other capabilities.
//
// It is up to the caller to decide on semantics for other operations.
type Oven struct {
p OvenParams
}
type OvenParams struct {
// Namespace holds the namespace to use when adding first party caveats.
// If this is nil, checkers.New(nil).Namespace will be used.
Namespace *checkers.Namespace
// RootKeyStoreForEntity returns the macaroon storage to be
// used for root keys associated with macaroons created
// wth NewMacaroon.
//
// If this is nil, NewMemRootKeyStore will be used to create
// a new store to be used for all entities.
RootKeyStoreForOps func(ops []Op) RootKeyStore
// Key holds the private key pair used to encrypt third party caveats.
// If it is nil, no third party caveats can be created.
Key *KeyPair
// Location holds the location that will be associated with new macaroons
// (as returned by Macaroon.Location).
Location string
// Locator is used to find out information on third parties when
// adding third party caveats. If this is nil, no non-local third
// party caveats can be added.
Locator ThirdPartyLocator
// LegacyMacaroonOp holds the operation to associate with old
// macaroons that don't have associated operations.
// If this is empty, legacy macaroons will not be associated
// with any operations.
LegacyMacaroonOp Op
// TODO max macaroon or macaroon id size?
}
// NewOven returns a new oven using the given parameters.
func NewOven(p OvenParams) *Oven {
if p.Locator == nil {
p.Locator = emptyLocator{}
}
if p.RootKeyStoreForOps == nil {
store := NewMemRootKeyStore()
p.RootKeyStoreForOps = func(ops []Op) RootKeyStore {
return store
}
}
if p.Namespace == nil {
p.Namespace = checkers.New(nil).Namespace()
}
return &Oven{
p: p,
}
}
// VerifyMacaroon implements MacaroonVerifier.VerifyMacaroon, making Oven
// an instance of MacaroonVerifier.
//
// For macaroons minted with previous bakery versions, it always
// returns a single LoginOp operation.
func (o *Oven) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []Op, conditions []string, err error) {
if len(ms) == 0 {
return nil, nil, errgo.Newf("no macaroons in slice")
}
storageId, ops, err := o.decodeMacaroonId(ms[0].Id())
if err != nil {
return nil, nil, errgo.Mask(err)
}
rootKey, err := o.p.RootKeyStoreForOps(ops).Get(ctx, storageId)
if err != nil {
if errgo.Cause(err) != ErrNotFound {
return nil, nil, errgo.Notef(err, "cannot get macaroon")
}
// If the macaroon was not found, it is probably
// because it's been removed after time-expiry,
// so return a verification error.
return nil, nil, &VerificationError{
Reason: errgo.Newf("macaroon not found in storage"),
}
}
conditions, err = ms[0].VerifySignature(rootKey, ms[1:])
if err != nil {
return nil, nil, &VerificationError{
Reason: errgo.Mask(err),
}
}
return ops, conditions, nil
}
func (o *Oven) decodeMacaroonId(id []byte) (storageId []byte, ops []Op, err error) {
base64Decoded := false
if id[0] == 'A' {
// The first byte is not a version number and it's 'A', which is the
// base64 encoding of the top 6 bits (all zero) of the version number 2 or 3,
// so we assume that it's the base64 encoding of a new-style
// macaroon id, so we base64 decode it.
//
// Note that old-style ids always start with an ASCII character >= 4
// (> 32 in fact) so this logic won't be triggered for those.
dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(id)))
n, err := base64.RawURLEncoding.Decode(dec, id)
if err == nil {
// Set the id only on success - if it's a bad encoding, we'll get a not-found error
// which is fine because "not found" is a correct description of the issue - we
// can't find the root key for the given id.
id = dec[0:n]
base64Decoded = true
}
}
// Trim any extraneous information from the id before retrieving
// it from storage, including the UUID that's added when
// creating macaroons to make all macaroons unique even if
// they're using the same root key.
switch id[0] {
case byte(Version2):
// Skip the UUID at the start of the id.
storageId = id[1+16:]
case byte(Version3):
var id1 macaroonpb.MacaroonId
if err := id1.UnmarshalBinary(id[1:]); err != nil {
return nil, nil, errgo.Notef(err, "cannot unmarshal macaroon id")
}
if len(id1.Ops) == 0 || len(id1.Ops[0].Actions) == 0 {
return nil, nil, errgo.Newf("no operations found in macaroon")
}
ops = make([]Op, 0, len(id1.Ops))
for _, op := range id1.Ops {
for _, action := range op.Actions {
ops = append(ops, Op{
Entity: op.Entity,
Action: action,
})
}
}
return id1.StorageId, ops, nil
}
if !base64Decoded && isLowerCaseHexChar(id[0]) {
// It's an old-style id, probably with a hyphenated UUID.
// so trim that off.
if i := bytes.LastIndexByte(id, '-'); i >= 0 {
storageId = id[0:i]
}
}
if op := o.p.LegacyMacaroonOp; op != (Op{}) {
ops = []Op{op}
}
return storageId, ops, nil
}
// NewMacaroon takes a macaroon with the given version from the oven, associates it with the given operations
// and attaches the given caveats. There must be at least one operation specified.
func (o *Oven) NewMacaroon(ctx context.Context, version Version, caveats []checkers.Caveat, ops ...Op) (*Macaroon, error) {
if len(ops) == 0 {
return nil, errgo.Newf("cannot mint a macaroon associated with no operations")
}
ops = CanonicalOps(ops)
rootKey, storageId, err := o.p.RootKeyStoreForOps(ops).RootKey(ctx)
if err != nil {
return nil, errgo.Mask(err)
}
id, err := o.newMacaroonId(ctx, ops, storageId)
if err != nil {
return nil, errgo.Mask(err)
}
idBytesNoVersion, err := id.MarshalBinary()
if err != nil {
return nil, errgo.Mask(err)
}
idBytes := make([]byte, len(idBytesNoVersion)+1)
idBytes[0] = byte(LatestVersion)
// TODO We could use a proto.Buffer to avoid this copy.
copy(idBytes[1:], idBytesNoVersion)
if MacaroonVersion(version) < macaroon.V2 {
// The old macaroon format required valid text for the macaroon id,
// so base64-encode it.
b64data := make([]byte, base64.RawURLEncoding.EncodedLen(len(idBytes)))
base64.RawURLEncoding.Encode(b64data, idBytes)
idBytes = b64data
}
m, err := NewMacaroon(rootKey, idBytes, o.p.Location, version, o.p.Namespace)
if err != nil {
return nil, errgo.Notef(err, "cannot create macaroon with version %v", version)
}
if err := o.AddCaveats(ctx, m, caveats); err != nil {
return nil, errgo.Mask(err)
}
return m, nil
}
// AddCaveat adds a caveat to the given macaroon.
func (o *Oven) AddCaveat(ctx context.Context, m *Macaroon, cav checkers.Caveat) error {
return m.AddCaveat(ctx, cav, o.p.Key, o.p.Locator)
}
// AddCaveats adds all the caveats to the given macaroon.
func (o *Oven) AddCaveats(ctx context.Context, m *Macaroon, caveats []checkers.Caveat) error {
return m.AddCaveats(ctx, caveats, o.p.Key, o.p.Locator)
}
// Key returns the oven's private/public key par.
func (o *Oven) Key() *KeyPair {
return o.p.Key
}
// Locator returns the third party locator that the
// oven was created with.
func (o *Oven) Locator() ThirdPartyLocator {
return o.p.Locator
}
// CanonicalOps returns the given operations slice sorted
// with duplicates removed.
func CanonicalOps(ops []Op) []Op {
canonOps := opsByValue(ops)
needNewSlice := false
for i := 1; i < len(ops); i++ {
if !canonOps.Less(i-1, i) {
needNewSlice = true
break
}
}
if !needNewSlice {
return ops
}
canonOps = make([]Op, len(ops))
copy(canonOps, ops)
sort.Sort(canonOps)
// Note we know that there's at least one operation here
// because we'd have returned earlier if the slice was empty.
j := 0
for _, op := range canonOps[1:] {
if op != canonOps[j] {
j++
canonOps[j] = op
}
}
return canonOps[0 : j+1]
}
func (o *Oven) newMacaroonId(ctx context.Context, ops []Op, storageId []byte) (*macaroonpb.MacaroonId, error) {
uuid := uuidGen.Next()
nonce := uuid[0:16]
return &macaroonpb.MacaroonId{
Nonce: nonce,
StorageId: storageId,
Ops: macaroonIdOps(ops),
}, nil
}
// macaroonIdOps returns operations suitable for serializing
// as part of an *macaroonpb.MacaroonId. It assumes that
// ops has been canonicalized and that there's at least
// one operation.
func macaroonIdOps(ops []Op) []*macaroonpb.Op {
idOps := make([]macaroonpb.Op, 0, len(ops))
idOps = append(idOps, macaroonpb.Op{
Entity: ops[0].Entity,
Actions: []string{ops[0].Action},
})
i := 0
idOp := &idOps[0]
for _, op := range ops[1:] {
if op.Entity != idOp.Entity {
idOps = append(idOps, macaroonpb.Op{
Entity: op.Entity,
Actions: []string{op.Action},
})
i++
idOp = &idOps[i]
continue
}
if op.Action != idOp.Actions[len(idOp.Actions)-1] {
idOp.Actions = append(idOp.Actions, op.Action)
}
}
idOpPtrs := make([]*macaroonpb.Op, len(idOps))
for i := range idOps {
idOpPtrs[i] = &idOps[i]
}
return idOpPtrs
}
type opsByValue []Op
func (o opsByValue) Less(i, j int) bool {
o0, o1 := o[i], o[j]
if o0.Entity != o1.Entity {
return o0.Entity < o1.Entity
}
return o0.Action < o1.Action
}
func (o opsByValue) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
func (o opsByValue) Len() int {
return len(o)
}
func isLowerCaseHexChar(c byte) bool {
switch {
case '0' <= c && c <= '9':
return true
case 'a' <= c && c <= 'f':
return true
}
return false
}