Files
aperture/mint/mint.go
Wilmer Paulino 9f291ddbf9 mint: introduce proper LSAT creation and verification
This package adheres to the agreed upon internal design document of the
macaroon portion of an LSAT. It is able to mint LSATs for a set of
services at any tier, each containing their desired set of constraints.

LSAT verification so far only ensures the that token was minted by us
and that the target service attempted to be accessed is authorized
according to the white-listed services contained in the token.
2019-11-25 17:07:08 -08:00

257 lines
7.6 KiB
Go

package mint
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"github.com/lightninglabs/loop/lsat"
"github.com/lightningnetwork/lnd/lntypes"
"gopkg.in/macaroon.v2"
)
var (
// ErrSecretNotFound is an error returned when we attempt to retrieve a
// secret by its key but it is not found.
ErrSecretNotFound = errors.New("secret not found")
)
// Challenger is an interface used to present requesters of LSATs with a
// challenge that must be satisfied before an LSAT can be validated. This
// challenge takes the form of a Lightning payment request.
type Challenger interface {
// NewChallenge returns a new challenge in the form of a Lightning
// payment request. The payment hash is also returned as a convenience
// to avoid having to decode the payment request in order to retrieve
// its payment hash.
NewChallenge() (string, lntypes.Hash, error)
}
// SecretStore is the store responsible for storing LSAT secrets. These secrets
// are required for proper verification of each minted LSAT.
type SecretStore interface {
// NewSecret creates a new cryptographically random secret which is
// keyed by the given hash.
NewSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, error)
// GetSecret returns the cryptographically random secret that
// corresponds to the given hash. If there is no secret, then
// ErrSecretNotFound is returned.
GetSecret(context.Context, [sha256.Size]byte) ([lsat.SecretSize]byte, error)
// RevokeSecret removes the cryptographically random secret that
// corresponds to the given hash. This acts as a NOP if the secret does
// not exist.
RevokeSecret(context.Context, [sha256.Size]byte) error
}
// ServiceLimiter abstracts the source of caveats that should be applied to an
// LSAT for a particular service.
type ServiceLimiter interface {
// ServiceCapabilities returns the capabilities caveats for each
// service. This determines which capabilities of each service can be
// accessed.
ServiceCapabilities(context.Context, ...lsat.Service) ([]lsat.Caveat, error)
// ServiceConstraints returns the constraints for each service. This
// enforces additional constraints on a particular service/service
// capability.
ServiceConstraints(context.Context, ...lsat.Service) ([]lsat.Caveat, error)
}
// Config packages all of the required dependencies to instantiate a new LSAT
// mint.
type Config struct {
// Secrets is our source for LSAT secrets which will be used for
// verification purposes.
Secrets SecretStore
// Challenger is our source of new challenges to present requesters of
// an LSAT with.
Challenger Challenger
// ServiceLimiter provides us with how we should limit a new LSAT based
// on its target services.
ServiceLimiter ServiceLimiter
}
// Mint is an entity that is able to mint and verify LSATs for a set of
// services.
type Mint struct {
cfg Config
}
// New creates a new LSAT mint backed by its given dependencies.
func New(cfg *Config) *Mint {
return &Mint{cfg: *cfg}
}
// MintLSAT mints a new LSAT for the target services.
func (m *Mint) MintLSAT(ctx context.Context,
services ...lsat.Service) (*macaroon.Macaroon, string, error) {
// We'll start by retrieving a new challenge in the form of a Lightning
// payment request to present the requester of the LSAT with.
paymentRequest, paymentHash, err := m.cfg.Challenger.NewChallenge()
if err != nil {
return nil, "", err
}
// TODO(wilmer): remove invoice if any of the operations below fail?
// We can then proceed to mint the LSAT with a unique identifier that is
// mapped to a unique secret.
id, err := createUniqueIdentifier(paymentHash)
if err != nil {
return nil, "", err
}
idHash := sha256.Sum256(id)
secret, err := m.cfg.Secrets.NewSecret(ctx, idHash)
if err != nil {
return nil, "", err
}
macaroon, err := macaroon.New(
secret[:], id, "lsat", macaroon.LatestVersion,
)
if err != nil {
// Attempt to revoke the secret to save space.
_ = m.cfg.Secrets.RevokeSecret(ctx, idHash)
return nil, "", err
}
// Include any restrictions that should be immediately applied to the
// LSAT.
var caveats []lsat.Caveat
if len(services) > 0 {
var err error
caveats, err = m.caveatsForServices(ctx, services...)
if err != nil {
// Attempt to revoke the secret to save space.
_ = m.cfg.Secrets.RevokeSecret(ctx, idHash)
return nil, "", err
}
}
if err := lsat.AddFirstPartyCaveats(macaroon, caveats...); err != nil {
// Attempt to revoke the secret to save space.
_ = m.cfg.Secrets.RevokeSecret(ctx, idHash)
return nil, "", err
}
return macaroon, paymentRequest, nil
}
// createUniqueIdentifier creates a new LSAT identifier bound to a payment hash
// and a randomly generated ID.
func createUniqueIdentifier(paymentHash lntypes.Hash) ([]byte, error) {
tokenID, err := generateTokenID()
if err != nil {
return nil, err
}
id := &lsat.Identifier{
Version: lsat.LatestVersion,
PaymentHash: paymentHash,
TokenID: tokenID,
}
var buf bytes.Buffer
if err := lsat.EncodeIdentifier(&buf, id); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// generateTokenID generates a new random LSAT ID.
func generateTokenID() ([lsat.TokenIDSize]byte, error) {
var tokenID [lsat.TokenIDSize]byte
_, err := rand.Read(tokenID[:])
return tokenID, err
}
// caveatsForServices returns all of the caveats that should be applied to an
// LSAT for the target services.
func (m *Mint) caveatsForServices(ctx context.Context,
services ...lsat.Service) ([]lsat.Caveat, error) {
servicesCaveat, err := lsat.NewServicesCaveat(services...)
if err != nil {
return nil, err
}
capabilities, err := m.cfg.ServiceLimiter.ServiceCapabilities(
ctx, services...,
)
if err != nil {
return nil, err
}
constraints, err := m.cfg.ServiceLimiter.ServiceConstraints(
ctx, services...,
)
if err != nil {
return nil, err
}
caveats := []lsat.Caveat{servicesCaveat}
caveats = append(caveats, capabilities...)
caveats = append(caveats, constraints...)
return caveats, nil
}
// VerificationParams holds all of the requirements to properly verify an LSAT.
type VerificationParams struct {
// Macaroon is the macaroon as part of the LSAT we'll attempt to verify.
Macaroon *macaroon.Macaroon
// Preimage is the preimage that should correspond to the LSAT's payment
// hash.
Preimage lntypes.Preimage
// TargetService is the target service a user of an LSAT is attempting
// to access.
TargetService string
}
// VerifyLSAT attempts to verify an LSAT with the given parameters.
func (m *Mint) VerifyLSAT(ctx context.Context, params *VerificationParams) error {
// We'll first perform a quick check to determine if a valid preimage
// was provided.
id, err := lsat.DecodeIdentifier(bytes.NewReader(params.Macaroon.Id()))
if err != nil {
return err
}
if params.Preimage.Hash() != id.PaymentHash {
return fmt.Errorf("invalid preimage %v for %v", params.Preimage,
id.PaymentHash)
}
// If there was, then we'll ensure the LSAT was minted by us.
secret, err := m.cfg.Secrets.GetSecret(
ctx, sha256.Sum256(params.Macaroon.Id()),
)
if err != nil {
return err
}
rawCaveats, err := params.Macaroon.VerifySignature(secret[:], nil)
if err != nil {
return err
}
// With the LSAT verified, we'll now inspect its caveats to ensure the
// target service is authorized.
var caveats []lsat.Caveat
for _, rawCaveat := range rawCaveats {
// LSATs can contain third-party caveats that we're not aware
// of, so just skip those.
caveat, err := lsat.DecodeCaveat(rawCaveat)
if err != nil {
continue
}
caveats = append(caveats, caveat)
}
return lsat.VerifyCaveats(
caveats, lsat.NewServicesSatisfier(params.TargetService),
)
}