mirror of
https://github.com/lightninglabs/aperture.git
synced 2025-12-17 09:04:19 +01:00
auth: LsatAuthenticator -> L402Authenticator sed -i 's/LsatAuthenticator/L402Authenticator/g' aperture.go auth/authenticator.go auth/authenticator_test.go rename package lsat to l402 git mv lsat/ l402 sed 's@aperture/lsat@aperture/l402@g' -i `git grep -l aperture/lsat` sed -i 's@package lsat@package l402@' `git grep -l 'package lsat'` sed -i 's@lsat\.@l402.@g' -i `git grep -l 'lsat\.'` sed 's@l402.Id@lsat.Id@' -i mint/mint_test.go replace lsat with l402 in the code sed 's@lsat@l402@' -i mint/mint_test.go sed 's@Lsat@L402@' -i l402/client_interceptor.go sed 's@lsatstore@l402store@' -i l402/store_test.go replace LSAT to L402 in comments sed '/\/\//s@LSAT@L402@g' -i `git grep -l '//.*LSAT'` replace LSAT -> L402 in the code, skip when a string starts with it sed 's@\([^"/]\)LSAT@\1L402@g' -i `git grep -l LSAT`
300 lines
8.5 KiB
Go
300 lines
8.5 KiB
Go
package mint
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/lightninglabs/aperture/l402"
|
|
"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 L402s with a
|
|
// challenge that must be satisfied before an L402 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(price int64) (string, lntypes.Hash, error)
|
|
|
|
// Stop shuts down the challenger.
|
|
Stop()
|
|
}
|
|
|
|
// SecretStore is the store responsible for storing L402 secrets. These secrets
|
|
// are required for proper verification of each minted L402.
|
|
type SecretStore interface {
|
|
// NewSecret creates a new cryptographically random secret which is
|
|
// keyed by the given hash.
|
|
NewSecret(context.Context, [sha256.Size]byte) ([l402.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) ([l402.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
|
|
// L402 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, ...l402.Service) ([]l402.Caveat,
|
|
error)
|
|
|
|
// ServiceConstraints returns the constraints for each service. This
|
|
// enforces additional constraints on a particular service/service
|
|
// capability.
|
|
ServiceConstraints(context.Context, ...l402.Service) ([]l402.Caveat,
|
|
error)
|
|
|
|
// ServiceTimeouts returns the timeout caveat for each service. This
|
|
// will determine if and when service access can expire.
|
|
ServiceTimeouts(context.Context, ...l402.Service) ([]l402.Caveat,
|
|
error)
|
|
}
|
|
|
|
// Config packages all of the required dependencies to instantiate a new L402
|
|
// mint.
|
|
type Config struct {
|
|
// Secrets is our source for L402 secrets which will be used for
|
|
// verification purposes.
|
|
Secrets SecretStore
|
|
|
|
// Challenger is our source of new challenges to present requesters of
|
|
// an L402 with.
|
|
Challenger Challenger
|
|
|
|
// ServiceLimiter provides us with how we should limit a new L402 based
|
|
// on its target services.
|
|
ServiceLimiter ServiceLimiter
|
|
|
|
// Now returns the current time.
|
|
Now func() time.Time
|
|
}
|
|
|
|
// Mint is an entity that is able to mint and verify L402s for a set of
|
|
// services.
|
|
type Mint struct {
|
|
cfg Config
|
|
}
|
|
|
|
// New creates a new L402 mint backed by its given dependencies.
|
|
func New(cfg *Config) *Mint {
|
|
return &Mint{cfg: *cfg}
|
|
}
|
|
|
|
// MintL402 mints a new L402 for the target services.
|
|
func (m *Mint) MintL402(ctx context.Context,
|
|
services ...l402.Service) (*macaroon.Macaroon, string, error) {
|
|
|
|
// Let the L402 value as the price of the most expensive of the
|
|
// services.
|
|
price := maximumPrice(services)
|
|
|
|
// We'll start by retrieving a new challenge in the form of a Lightning
|
|
// payment request to present the requester of the L402 with.
|
|
paymentRequest, paymentHash, err := m.cfg.Challenger.NewChallenge(price)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// TODO(wilmer): remove invoice if any of the operations below fail?
|
|
|
|
// We can then proceed to mint the L402 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
|
|
}
|
|
mac, 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
|
|
// L402.
|
|
var caveats []l402.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 := l402.AddFirstPartyCaveats(mac, caveats...); err != nil {
|
|
// Attempt to revoke the secret to save space.
|
|
_ = m.cfg.Secrets.RevokeSecret(ctx, idHash)
|
|
return nil, "", err
|
|
}
|
|
|
|
return mac, paymentRequest, nil
|
|
}
|
|
|
|
// maximumPrice determines the necessary price to use for a collection
|
|
// of services.
|
|
func maximumPrice(services []l402.Service) int64 {
|
|
var max int64
|
|
|
|
for _, service := range services {
|
|
if service.Price > max {
|
|
max = service.Price
|
|
}
|
|
}
|
|
|
|
return max
|
|
}
|
|
|
|
// createUniqueIdentifier creates a new L402 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 := &l402.Identifier{
|
|
Version: l402.LatestVersion,
|
|
PaymentHash: paymentHash,
|
|
TokenID: tokenID,
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := l402.EncodeIdentifier(&buf, id); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// generateTokenID generates a new random L402 ID.
|
|
func generateTokenID() ([l402.TokenIDSize]byte, error) {
|
|
var tokenID [l402.TokenIDSize]byte
|
|
_, err := rand.Read(tokenID[:])
|
|
return tokenID, err
|
|
}
|
|
|
|
// caveatsForServices returns all of the caveats that should be applied to an
|
|
// L402 for the target services.
|
|
func (m *Mint) caveatsForServices(ctx context.Context,
|
|
services ...l402.Service) ([]l402.Caveat, error) {
|
|
|
|
servicesCaveat, err := l402.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
|
|
}
|
|
timeouts, err := m.cfg.ServiceLimiter.ServiceTimeouts(ctx, services...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
caveats := []l402.Caveat{servicesCaveat}
|
|
caveats = append(caveats, capabilities...)
|
|
caveats = append(caveats, constraints...)
|
|
caveats = append(caveats, timeouts...)
|
|
return caveats, nil
|
|
}
|
|
|
|
// VerificationParams holds all of the requirements to properly verify an L402.
|
|
type VerificationParams struct {
|
|
// Macaroon is the macaroon as part of the L402 we'll attempt to verify.
|
|
Macaroon *macaroon.Macaroon
|
|
|
|
// Preimage is the preimage that should correspond to the L402's payment
|
|
// hash.
|
|
Preimage lntypes.Preimage
|
|
|
|
// TargetService is the target service a user of an L402 is attempting
|
|
// to access.
|
|
TargetService string
|
|
}
|
|
|
|
// VerifyL402 attempts to verify an L402 with the given parameters.
|
|
func (m *Mint) VerifyL402(ctx context.Context,
|
|
params *VerificationParams) error {
|
|
|
|
// We'll first perform a quick check to determine if a valid preimage
|
|
// was provided.
|
|
id, err := l402.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 L402 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 L402 verified, we'll now inspect its caveats to ensure the
|
|
// target service is authorized.
|
|
caveats := make([]l402.Caveat, 0, len(rawCaveats))
|
|
for _, rawCaveat := range rawCaveats {
|
|
// L402s can contain third-party caveats that we're not aware
|
|
// of, so just skip those.
|
|
caveat, err := l402.DecodeCaveat(rawCaveat)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
caveats = append(caveats, caveat)
|
|
}
|
|
return l402.VerifyCaveats(
|
|
caveats,
|
|
l402.NewServicesSatisfier(params.TargetService),
|
|
l402.NewTimeoutSatisfier(params.TargetService, m.cfg.Now),
|
|
)
|
|
}
|