safing-portmaster/spn/access/token/pblind.go
Daniel Hååvi 80664d1a27
Restructure modules (#1572)
* Move portbase into monorepo

* Add new simple module mgr

* [WIP] Switch to new simple module mgr

* Add StateMgr and more worker variants

* [WIP] Switch more modules

* [WIP] Switch more modules

* [WIP] swtich more modules

* [WIP] switch all SPN modules

* [WIP] switch all service modules

* [WIP] Convert all workers to the new module system

* [WIP] add new task system to module manager

* [WIP] Add second take for scheduling workers

* [WIP] Add FIXME for bugs in new scheduler

* [WIP] Add minor improvements to scheduler

* [WIP] Add new worker scheduler

* [WIP] Fix more bug related to new module system

* [WIP] Fix start handing of the new module system

* [WIP] Improve startup process

* [WIP] Fix minor issues

* [WIP] Fix missing subsystem in settings

* [WIP] Initialize managers in constructor

* [WIP] Move module event initialization to constrictors

* [WIP] Fix setting for enabling and disabling the SPN module

* [WIP] Move API registeration into module construction

* [WIP] Update states mgr for all modules

* [WIP] Add CmdLine operation support

* Add state helper methods to module group and instance

* Add notification and module status handling to status package

* Fix starting issues

* Remove pilot widget and update security lock to new status data

* Remove debug logs

* Improve http server shutdown

* Add workaround for cleanly shutting down firewall+netquery

* Improve logging

* Add syncing states with notifications for new module system

* Improve starting, stopping, shutdown; resolve FIXMEs/TODOs

* [WIP] Fix most unit tests

* Review new module system and fix minor issues

* Push shutdown and restart events again via API

* Set sleep mode via interface

* Update example/template module

* [WIP] Fix spn/cabin unit test

* Remove deprecated UI elements

* Make log output more similar for the logging transition phase

* Switch spn hub and observer cmds to new module system

* Fix log sources

* Make worker mgr less error prone

* Fix tests and minor issues

* Fix observation hub

* Improve shutdown and restart handling

* Split up big connection.go source file

* Move varint and dsd packages to structures repo

* Improve expansion test

* Fix linter warnings

* Fix interception module on windows

* Fix linter errors

---------

Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
2024-08-09 18:15:48 +03:00

552 lines
14 KiB
Go

package token
import (
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"math"
"math/big"
mrand "math/rand"
"sync"
"github.com/mr-tron/base58"
"github.com/rot256/pblind"
"github.com/safing/structures/container"
"github.com/safing/structures/dsd"
)
const pblindSecretSize = 32
// PBlindToken is token based on the pblind library.
type PBlindToken struct {
Serial int `json:"N,omitempty"`
Token []byte `json:"T,omitempty"`
Signature *pblind.Signature `json:"S,omitempty"`
}
// Pack packs the token.
func (pbt *PBlindToken) Pack() ([]byte, error) {
return dsd.Dump(pbt, dsd.CBOR)
}
// UnpackPBlindToken unpacks the token.
func UnpackPBlindToken(token []byte) (*PBlindToken, error) {
t := &PBlindToken{}
_, err := dsd.Load(token, t)
if err != nil {
return nil, err
}
return t, nil
}
// PBlindHandler is a handler for the pblind tokens.
type PBlindHandler struct {
sync.Mutex
opts *PBlindOptions
publicKey *pblind.PublicKey
privateKey *pblind.SecretKey
storageLock sync.Mutex
Storage []*PBlindToken
// Client request state.
requestStateLock sync.Mutex
requestState []RequestState
}
// PBlindOptions are options for the PBlindHandler.
type PBlindOptions struct {
Zone string
CurveName string
Curve elliptic.Curve
PublicKey string
PrivateKey string
BatchSize int
UseSerials bool
RandomizeOrder bool
Fallback bool
SignalShouldRequest func(Handler)
DoubleSpendProtection func([]byte) error
}
// PBlindSignerState is a signer state.
type PBlindSignerState struct {
signers []*pblind.StateSigner
}
// PBlindSetupResponse is a setup response.
type PBlindSetupResponse struct {
Msgs []*pblind.Message1
}
// PBlindTokenRequest is a token request.
type PBlindTokenRequest struct {
Msgs []*pblind.Message2
}
// IssuedPBlindTokens are issued pblind tokens.
type IssuedPBlindTokens struct {
Msgs []*pblind.Message3
}
// RequestState is a request state.
type RequestState struct {
Token []byte
State *pblind.StateRequester
}
// NewPBlindHandler creates a new pblind handler.
func NewPBlindHandler(opts PBlindOptions) (*PBlindHandler, error) {
pbh := &PBlindHandler{
opts: &opts,
}
// Check curve, get from name.
if opts.Curve == nil {
switch opts.CurveName {
case "P-256":
opts.Curve = elliptic.P256()
case "P-384":
opts.Curve = elliptic.P384()
case "P-521":
opts.Curve = elliptic.P521()
default:
return nil, errors.New("no curve supplied")
}
} else if opts.CurveName != "" {
return nil, errors.New("both curve and curve name supplied")
}
// Load keys.
switch {
case pbh.opts.PrivateKey != "":
keyData, err := base58.Decode(pbh.opts.PrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to decode private key: %w", err)
}
pivateKey := pblind.SecretKeyFromBytes(pbh.opts.Curve, keyData)
pbh.privateKey = &pivateKey
publicKey := pbh.privateKey.GetPublicKey()
pbh.publicKey = &publicKey
// Check public key if also provided.
if pbh.opts.PublicKey != "" {
if pbh.opts.PublicKey != base58.Encode(pbh.publicKey.Bytes()) {
return nil, errors.New("private and public mismatch")
}
}
case pbh.opts.PublicKey != "":
keyData, err := base58.Decode(pbh.opts.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decode public key: %w", err)
}
publicKey, err := pblind.PublicKeyFromBytes(pbh.opts.Curve, keyData)
if err != nil {
return nil, fmt.Errorf("failed to decode public key: %w", err)
}
pbh.publicKey = &publicKey
default:
return nil, errors.New("no key supplied")
}
return pbh, nil
}
func (pbh *PBlindHandler) makeInfo(serial int) (*pblind.Info, error) {
// Gather data for info.
infoData := container.New()
infoData.AppendAsBlock([]byte(pbh.opts.Zone))
if pbh.opts.UseSerials {
infoData.AppendInt(serial)
}
// Compress to point.
info, err := pblind.CompressInfo(pbh.opts.Curve, infoData.CompileData())
if err != nil {
return nil, fmt.Errorf("failed to compress info: %w", err)
}
return &info, nil
}
// Zone returns the zone name.
func (pbh *PBlindHandler) Zone() string {
return pbh.opts.Zone
}
// ShouldRequest returns whether the new tokens should be requested.
func (pbh *PBlindHandler) ShouldRequest() bool {
pbh.storageLock.Lock()
defer pbh.storageLock.Unlock()
return pbh.shouldRequest()
}
func (pbh *PBlindHandler) shouldRequest() bool {
// Return true if storage is at or below 10%.
return len(pbh.Storage) == 0 || pbh.opts.BatchSize/len(pbh.Storage) > 10
}
// Amount returns the current amount of tokens in this handler.
func (pbh *PBlindHandler) Amount() int {
pbh.storageLock.Lock()
defer pbh.storageLock.Unlock()
return len(pbh.Storage)
}
// IsFallback returns whether this handler should only be used as a fallback.
func (pbh *PBlindHandler) IsFallback() bool {
return pbh.opts.Fallback
}
// CreateSetup sets up signers for a request.
func (pbh *PBlindHandler) CreateSetup() (state *PBlindSignerState, setupResponse *PBlindSetupResponse, err error) {
state = &PBlindSignerState{
signers: make([]*pblind.StateSigner, pbh.opts.BatchSize),
}
setupResponse = &PBlindSetupResponse{
Msgs: make([]*pblind.Message1, pbh.opts.BatchSize),
}
// Go through the batch.
for i := range pbh.opts.BatchSize {
info, err := pbh.makeInfo(i + 1)
if err != nil {
return nil, nil, fmt.Errorf("failed to create info #%d: %w", i, err)
}
// Create signer.
signer, err := pblind.CreateSigner(*pbh.privateKey, *info)
if err != nil {
return nil, nil, fmt.Errorf("failed to create signer #%d: %w", i, err)
}
state.signers[i] = signer
// Create request setup.
setupMsg, err := signer.CreateMessage1()
if err != nil {
return nil, nil, fmt.Errorf("failed to create setup msg #%d: %w", i, err)
}
setupResponse.Msgs[i] = &setupMsg
}
return state, setupResponse, nil
}
// CreateTokenRequest creates a token request to be sent to the token server.
func (pbh *PBlindHandler) CreateTokenRequest(requestSetup *PBlindSetupResponse) (request *PBlindTokenRequest, err error) {
// Check request setup data.
if len(requestSetup.Msgs) != pbh.opts.BatchSize {
return nil, fmt.Errorf("invalid request setup msg count of %d", len(requestSetup.Msgs))
}
// Lock and reset the request state.
pbh.requestStateLock.Lock()
defer pbh.requestStateLock.Unlock()
pbh.requestState = make([]RequestState, pbh.opts.BatchSize)
request = &PBlindTokenRequest{
Msgs: make([]*pblind.Message2, pbh.opts.BatchSize),
}
// Go through the batch.
for i := range pbh.opts.BatchSize {
// Check if we have setup data.
if requestSetup.Msgs[i] == nil {
return nil, fmt.Errorf("missing setup data #%d", i)
}
// Generate secret token.
token := make([]byte, pblindSecretSize)
n, err := rand.Read(token) //nolint:gosec // False positive - check the imports.
if err != nil {
return nil, fmt.Errorf("failed to get random token #%d: %w", i, err)
}
if n != pblindSecretSize {
return nil, fmt.Errorf("failed to get full random token #%d: only got %d bytes", i, n)
}
pbh.requestState[i].Token = token
// Create public metadata.
info, err := pbh.makeInfo(i + 1)
if err != nil {
return nil, fmt.Errorf("failed to make token info #%d: %w", i, err)
}
// Create request and request state.
requester, err := pblind.CreateRequester(*pbh.publicKey, *info, token)
if err != nil {
return nil, fmt.Errorf("failed to create request state #%d: %w", i, err)
}
pbh.requestState[i].State = requester
err = requester.ProcessMessage1(*requestSetup.Msgs[i])
if err != nil {
return nil, fmt.Errorf("failed to process setup message #%d: %w", i, err)
}
// Create request message.
requestMsg, err := requester.CreateMessage2()
if err != nil {
return nil, fmt.Errorf("failed to create request message #%d: %w", i, err)
}
request.Msgs[i] = &requestMsg
}
return request, nil
}
// IssueTokens sign the requested tokens.
func (pbh *PBlindHandler) IssueTokens(state *PBlindSignerState, request *PBlindTokenRequest) (response *IssuedPBlindTokens, err error) {
// Check request data.
if len(request.Msgs) != pbh.opts.BatchSize {
return nil, fmt.Errorf("invalid request msg count of %d", len(request.Msgs))
}
if len(state.signers) != pbh.opts.BatchSize {
return nil, fmt.Errorf("invalid request state count of %d", len(request.Msgs))
}
// Create response.
response = &IssuedPBlindTokens{
Msgs: make([]*pblind.Message3, pbh.opts.BatchSize),
}
// Go through the batch.
for i := range pbh.opts.BatchSize {
// Check if we have request data.
if request.Msgs[i] == nil {
return nil, fmt.Errorf("missing request data #%d", i)
}
// Process request msg.
err = state.signers[i].ProcessMessage2(*request.Msgs[i])
if err != nil {
return nil, fmt.Errorf("failed to process request msg #%d: %w", i, err)
}
// Issue token.
responseMsg, err := state.signers[i].CreateMessage3()
if err != nil {
return nil, fmt.Errorf("failed to issue token #%d: %w", i, err)
}
response.Msgs[i] = &responseMsg
}
return response, nil
}
// ProcessIssuedTokens processes the issued token from the server.
func (pbh *PBlindHandler) ProcessIssuedTokens(issuedTokens *IssuedPBlindTokens) error {
// Check data.
if len(issuedTokens.Msgs) != pbh.opts.BatchSize {
return fmt.Errorf("invalid issued token count of %d", len(issuedTokens.Msgs))
}
// Step 1: Process issued tokens.
// Lock and reset the request state.
pbh.requestStateLock.Lock()
defer pbh.requestStateLock.Unlock()
defer func() {
pbh.requestState = make([]RequestState, pbh.opts.BatchSize)
}()
finalizedTokens := make([]*PBlindToken, pbh.opts.BatchSize)
// Go through the batch.
for i := range pbh.opts.BatchSize {
// Finalize token.
err := pbh.requestState[i].State.ProcessMessage3(*issuedTokens.Msgs[i])
if err != nil {
return fmt.Errorf("failed to create final signature #%d: %w", i, err)
}
// Get and check final signature.
signature, err := pbh.requestState[i].State.Signature()
if err != nil {
return fmt.Errorf("failed to create final signature #%d: %w", i, err)
}
info, err := pbh.makeInfo(i + 1)
if err != nil {
return fmt.Errorf("failed to make token info #%d: %w", i, err)
}
if !pbh.publicKey.Check(signature, *info, pbh.requestState[i].Token) {
return fmt.Errorf("invalid signature on #%d", i)
}
// Save to temporary slice.
newToken := &PBlindToken{
Token: pbh.requestState[i].Token,
Signature: &signature,
}
if pbh.opts.UseSerials {
newToken.Serial = i + 1
}
finalizedTokens[i] = newToken
}
// Step 2: Randomize received tokens
if pbh.opts.RandomizeOrder {
rInt, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return fmt.Errorf("failed to get seed for shuffle: %w", err)
}
mr := mrand.New(mrand.NewSource(rInt.Int64())) //nolint:gosec
mr.Shuffle(len(finalizedTokens), func(i, j int) {
finalizedTokens[i], finalizedTokens[j] = finalizedTokens[j], finalizedTokens[i]
})
}
// Step 3: Add tokens to storage.
// Wait for all processing to be complete, as using tokens from a faulty
// batch can be dangerous, as the server could be doing this purposely to
// create conditions that may benefit an attacker.
pbh.storageLock.Lock()
defer pbh.storageLock.Unlock()
// Add finalized tokens to storage.
pbh.Storage = append(pbh.Storage, finalizedTokens...)
return nil
}
// GetToken returns a token.
func (pbh *PBlindHandler) GetToken() (token *Token, err error) {
pbh.storageLock.Lock()
defer pbh.storageLock.Unlock()
// Check if we have supply.
if len(pbh.Storage) == 0 {
return nil, ErrEmpty
}
// Pack token.
data, err := pbh.Storage[0].Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack token: %w", err)
}
// Shift to next token.
pbh.Storage = pbh.Storage[1:]
// Check if we should signal that we should request tokens.
if pbh.opts.SignalShouldRequest != nil && pbh.shouldRequest() {
pbh.opts.SignalShouldRequest(pbh)
}
return &Token{
Zone: pbh.opts.Zone,
Data: data,
}, nil
}
// Verify verifies the given token.
func (pbh *PBlindHandler) Verify(token *Token) error {
// Check if zone matches.
if token.Zone != pbh.opts.Zone {
return ErrZoneMismatch
}
// Unpack token.
t, err := UnpackPBlindToken(token.Data)
if err != nil {
return fmt.Errorf("%w: %w", ErrTokenMalformed, err)
}
// Check if serial is valid.
switch {
case pbh.opts.UseSerials && t.Serial > 0 && t.Serial <= pbh.opts.BatchSize:
// Using serials in accepted range.
case !pbh.opts.UseSerials && t.Serial == 0:
// Not using serials and serial is zero.
default:
return fmt.Errorf("%w: invalid serial", ErrTokenMalformed)
}
// Build info for checking signature.
info, err := pbh.makeInfo(t.Serial)
if err != nil {
return fmt.Errorf("%w: %w", ErrTokenMalformed, err)
}
// Check signature.
if !pbh.publicKey.Check(*t.Signature, *info, t.Token) {
return ErrTokenInvalid
}
// Check for double spending.
if pbh.opts.DoubleSpendProtection != nil {
if err := pbh.opts.DoubleSpendProtection(t.Token); err != nil {
return fmt.Errorf("%w: %w", ErrTokenUsed, err)
}
}
return nil
}
// PBlindStorage is a storage for pblind tokens.
type PBlindStorage struct {
Storage []*PBlindToken
}
// Save serializes and returns the current tokens.
func (pbh *PBlindHandler) Save() ([]byte, error) {
pbh.storageLock.Lock()
defer pbh.storageLock.Unlock()
if len(pbh.Storage) == 0 {
return nil, ErrEmpty
}
s := &PBlindStorage{
Storage: pbh.Storage,
}
return dsd.Dump(s, dsd.CBOR)
}
// Load loads the given tokens into the handler.
func (pbh *PBlindHandler) Load(data []byte) error {
pbh.storageLock.Lock()
defer pbh.storageLock.Unlock()
s := &PBlindStorage{}
_, err := dsd.Load(data, s)
if err != nil {
return err
}
// Check signatures on load.
for _, t := range s.Storage {
// Build info for checking signature.
info, err := pbh.makeInfo(t.Serial)
if err != nil {
return err
}
// Check signature.
if !pbh.publicKey.Check(*t.Signature, *info, t.Token) {
return ErrTokenInvalid
}
}
pbh.Storage = s.Storage
return nil
}
// Clear clears all the tokens in the handler.
func (pbh *PBlindHandler) Clear() {
pbh.storageLock.Lock()
defer pbh.storageLock.Unlock()
pbh.Storage = nil
}