wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

View File

@@ -0,0 +1,15 @@
package token
import "errors"
// Errors.
var (
ErrEmpty = errors.New("token storage is empty")
ErrNoZone = errors.New("no zone specified")
ErrTokenInvalid = errors.New("token is invalid")
ErrTokenMalformed = errors.New("token malformed")
ErrTokenUsed = errors.New("token already used")
ErrZoneMismatch = errors.New("zone mismatch")
ErrZoneTaken = errors.New("zone taken")
ErrZoneUnknown = errors.New("zone unknown")
)

View File

@@ -0,0 +1,13 @@
package token
import (
"testing"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/service/core/pmtesting"
)
func TestMain(m *testing.M) {
module := modules.Register("token", nil, nil, nil, "rng")
pmtesting.TestMain(m, module)
}

552
spn/access/token/pblind.go Normal file
View File

@@ -0,0 +1,552 @@
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/portbase/container"
"github.com/safing/portbase/formats/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 := 0; i < pbh.opts.BatchSize; i++ {
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 := 0; i < pbh.opts.BatchSize; i++ {
// 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 := 0; i < pbh.opts.BatchSize; i++ {
// 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 := 0; i < pbh.opts.BatchSize; i++ {
// 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
}

View File

@@ -0,0 +1,39 @@
package token
import (
"crypto/elliptic"
"fmt"
"testing"
"github.com/mr-tron/base58"
"github.com/rot256/pblind"
)
func TestGeneratePBlindKeys(t *testing.T) {
t.Parallel()
for _, curve := range []elliptic.Curve{
elliptic.P256(),
elliptic.P384(),
elliptic.P521(),
} {
privateKey, err := pblind.NewSecretKey(curve)
if err != nil {
t.Fatal(err)
}
publicKey := privateKey.GetPublicKey()
fmt.Printf(
"%s (%dbit) private key: %s\n",
curve.Params().Name,
curve.Params().BitSize,
base58.Encode(privateKey.Bytes()),
)
fmt.Printf(
"%s (%dbit) public key: %s\n",
curve.Params().Name,
curve.Params().BitSize,
base58.Encode(publicKey.Bytes()),
)
}
}

View File

@@ -0,0 +1,260 @@
package token
import (
"crypto/elliptic"
"encoding/asn1"
"testing"
"time"
"github.com/rot256/pblind"
)
const PBlindTestZone = "test-pblind"
func init() {
// Combined testing config.
h, err := NewPBlindHandler(PBlindOptions{
Zone: PBlindTestZone,
Curve: elliptic.P256(),
PrivateKey: "HbwGtLsqek1Fdwuz1MhNQfiY7tj9EpWHeMWHPZ9c6KYY",
UseSerials: true,
BatchSize: 1000,
RandomizeOrder: true,
})
if err != nil {
panic(err)
}
err = RegisterPBlindHandler(h)
if err != nil {
panic(err)
}
}
func TestPBlind(t *testing.T) {
t.Parallel()
opts := &PBlindOptions{
Zone: PBlindTestZone,
Curve: elliptic.P256(),
UseSerials: true,
BatchSize: 1000,
RandomizeOrder: true,
}
// Issuer
opts.PrivateKey = "HbwGtLsqek1Fdwuz1MhNQfiY7tj9EpWHeMWHPZ9c6KYY"
issuer, err := NewPBlindHandler(*opts)
if err != nil {
t.Fatal(err)
}
// Client
opts.PrivateKey = ""
opts.PublicKey = "285oMDh3w5mxyFgpmmURifKfhkcqwwsdnePpPZ6Nqm8cc"
client, err := NewPBlindHandler(*opts)
if err != nil {
t.Fatal(err)
}
// Verifier
verifier, err := NewPBlindHandler(*opts)
if err != nil {
t.Fatal(err)
}
// Play through the whole use case.
signerState, setupResponse, err := issuer.CreateSetup()
if err != nil {
t.Fatal(err)
}
request, err := client.CreateTokenRequest(setupResponse)
if err != nil {
t.Fatal(err)
}
issuedTokens, err := issuer.IssueTokens(signerState, request)
if err != nil {
t.Fatal(err)
}
err = client.ProcessIssuedTokens(issuedTokens)
if err != nil {
t.Fatal(err)
}
token, err := client.GetToken()
if err != nil {
t.Fatal(err)
}
err = verifier.Verify(token)
if err != nil {
t.Fatal(err)
}
}
func TestPBlindLibrary(t *testing.T) {
t.Parallel()
// generate a key-pair
curve := elliptic.P256()
sk, _ := pblind.NewSecretKey(curve)
pk := sk.GetPublicKey()
msgStr := []byte("128b_accesstoken")
infoStr := []byte("v=1 serial=12345")
info, err := pblind.CompressInfo(curve, infoStr)
if err != nil {
t.Fatal(err)
}
totalStart := time.Now()
batchSize := 1000
signers := make([]*pblind.StateSigner, batchSize)
requesters := make([]*pblind.StateRequester, batchSize)
toServer := make([][]byte, batchSize)
toClient := make([][]byte, batchSize)
// Create signers and prep requests.
start := time.Now()
for i := 0; i < batchSize; i++ {
signer, err := pblind.CreateSigner(sk, info)
if err != nil {
t.Fatal(err)
}
signers[i] = signer
msg1S, err := signer.CreateMessage1()
if err != nil {
t.Fatal(err)
}
ser1S, err := asn1.Marshal(msg1S)
if err != nil {
t.Fatal(err)
}
toClient[i] = ser1S
}
t.Logf("created %d signers and request preps in %s", batchSize, time.Since(start))
t.Logf("sending %d bytes to client", lenOfByteSlices(toClient))
// Create requesters and create requests.
start = time.Now()
for i := 0; i < batchSize; i++ {
requester, err := pblind.CreateRequester(pk, info, msgStr)
if err != nil {
t.Fatal(err)
}
requesters[i] = requester
var msg1R pblind.Message1
_, err = asn1.Unmarshal(toClient[i], &msg1R)
if err != nil {
t.Fatal(err)
}
err = requester.ProcessMessage1(msg1R)
if err != nil {
t.Fatal(err)
}
msg2R, err := requester.CreateMessage2()
if err != nil {
t.Fatal(err)
}
ser2R, err := asn1.Marshal(msg2R)
if err != nil {
t.Fatal(err)
}
toServer[i] = ser2R
}
t.Logf("created %d requesters and requests in %s", batchSize, time.Since(start))
t.Logf("sending %d bytes to server", lenOfByteSlices(toServer))
// Sign requests
start = time.Now()
for i := 0; i < batchSize; i++ {
var msg2S pblind.Message2
_, err = asn1.Unmarshal(toServer[i], &msg2S)
if err != nil {
t.Fatal(err)
}
err = signers[i].ProcessMessage2(msg2S)
if err != nil {
t.Fatal(err)
}
msg3S, err := signers[i].CreateMessage3()
if err != nil {
t.Fatal(err)
}
ser3S, err := asn1.Marshal(msg3S)
if err != nil {
t.Fatal(err)
}
toClient[i] = ser3S
}
t.Logf("signed %d requests in %s", batchSize, time.Since(start))
t.Logf("sending %d bytes to client", lenOfByteSlices(toClient))
// Verify signed requests
start = time.Now()
for i := 0; i < batchSize; i++ {
var msg3R pblind.Message3
_, err := asn1.Unmarshal(toClient[i], &msg3R)
if err != nil {
t.Fatal(err)
}
err = requesters[i].ProcessMessage3(msg3R)
if err != nil {
t.Fatal(err)
}
signature, err := requesters[i].Signature()
if err != nil {
t.Fatal(err)
}
sig, err := asn1.Marshal(signature)
if err != nil {
t.Fatal(err)
}
toServer[i] = sig
// check signature
if !pk.Check(signature, info, msgStr) {
t.Fatal("signature invalid")
}
}
t.Logf("finalized and verified %d signed tokens in %s", batchSize, time.Since(start))
t.Logf("stored %d signed tokens in %d bytes", batchSize, lenOfByteSlices(toServer))
// Verify on server
start = time.Now()
for i := 0; i < batchSize; i++ {
var sig pblind.Signature
_, err := asn1.Unmarshal(toServer[i], &sig)
if err != nil {
t.Fatal(err)
}
// check signature
if !pk.Check(sig, info, msgStr) {
t.Fatal("signature invalid")
}
}
t.Logf("verified %d signed tokens in %s", batchSize, time.Since(start))
t.Logf("process complete")
t.Logf("simulated the whole process for %d tokens in %s", batchSize, time.Since(totalStart))
}
func lenOfByteSlices(v [][]byte) (length int) {
for _, s := range v {
length += len(s)
}
return
}

View File

@@ -0,0 +1,116 @@
package token
import "sync"
// Handler represents a token handling system.
type Handler interface {
// Zone returns the zone name.
Zone() string
// ShouldRequest returns whether the new tokens should be requested.
ShouldRequest() bool
// Amount returns the current amount of tokens in this handler.
Amount() int
// IsFallback returns whether this handler should only be used as a fallback.
IsFallback() bool
// GetToken returns a token.
GetToken() (token *Token, err error)
// Verify verifies the given token.
Verify(token *Token) error
// Save serializes and returns the current tokens.
Save() ([]byte, error)
// Load loads the given tokens into the handler.
Load(data []byte) error
// Clear clears all the tokens in the handler.
Clear()
}
var (
registry map[string]Handler
pblindRegistry []*PBlindHandler
scrambleRegistry []*ScrambleHandler
registryLock sync.RWMutex
)
func init() {
initRegistry()
}
func initRegistry() {
registry = make(map[string]Handler)
pblindRegistry = make([]*PBlindHandler, 0, 1)
scrambleRegistry = make([]*ScrambleHandler, 0, 1)
}
// RegisterPBlindHandler registers a pblind handler with the registry.
func RegisterPBlindHandler(h *PBlindHandler) error {
registryLock.Lock()
defer registryLock.Unlock()
if err := registerHandler(h, h.opts.Zone); err != nil {
return err
}
pblindRegistry = append(pblindRegistry, h)
return nil
}
// RegisterScrambleHandler registers a scramble handler with the registry.
func RegisterScrambleHandler(h *ScrambleHandler) error {
registryLock.Lock()
defer registryLock.Unlock()
if err := registerHandler(h, h.opts.Zone); err != nil {
return err
}
scrambleRegistry = append(scrambleRegistry, h)
return nil
}
func registerHandler(h Handler, zone string) error {
if zone == "" {
return ErrNoZone
}
_, ok := registry[zone]
if ok {
return ErrZoneTaken
}
registry[zone] = h
return nil
}
// GetHandler returns the handler of the given zone.
func GetHandler(zone string) (handler Handler, ok bool) {
registryLock.RLock()
defer registryLock.RUnlock()
handler, ok = registry[zone]
return
}
// ResetRegistry resets the token handler registry.
func ResetRegistry() {
registryLock.Lock()
defer registryLock.Unlock()
initRegistry()
}
// RegistrySize returns the amount of handler registered.
func RegistrySize() int {
registryLock.Lock()
defer registryLock.Unlock()
return len(registry)
}

244
spn/access/token/request.go Normal file
View File

@@ -0,0 +1,244 @@
package token
import (
"crypto/rand"
"errors"
"fmt"
"github.com/mr-tron/base58"
)
const sessionIDSize = 32
// RequestHandlingState is a request handling state.
type RequestHandlingState struct {
SessionID string
PBlind map[string]*PBlindSignerState
}
// SetupRequest is a setup request.
type SetupRequest struct {
PBlind map[string]struct{} `json:"PB,omitempty"`
}
// SetupResponse is a setup response.
type SetupResponse struct {
SessionID string `json:"ID,omitempty"`
PBlind map[string]*PBlindSetupResponse `json:"PB,omitempty"`
}
// TokenRequest is a token request.
type TokenRequest struct { //nolint:golint // Be explicit.
SessionID string `json:"ID,omitempty"`
PBlind map[string]*PBlindTokenRequest `json:"PB,omitempty"`
Scramble map[string]*ScrambleTokenRequest `json:"S,omitempty"`
}
// IssuedTokens are issued tokens.
type IssuedTokens struct {
PBlind map[string]*IssuedPBlindTokens `json:"PB,omitempty"`
Scramble map[string]*IssuedScrambleTokens `json:"SC,omitempty"`
}
// CreateSetupRequest creates a combined setup request for all registered tokens, if needed.
func CreateSetupRequest() (request *SetupRequest, setupRequired bool) {
registryLock.RLock()
defer registryLock.RUnlock()
request = &SetupRequest{
PBlind: make(map[string]struct{}, len(pblindRegistry)),
}
// Go through handlers and create request setups.
for _, pblindHandler := range pblindRegistry {
// Check if we need to request with this handler.
if pblindHandler.ShouldRequest() {
request.PBlind[pblindHandler.Zone()] = struct{}{}
setupRequired = true
}
}
return
}
// HandleSetupRequest handles a setup request for all registered tokens.
func HandleSetupRequest(request *SetupRequest) (*RequestHandlingState, *SetupResponse, error) {
registryLock.RLock()
defer registryLock.RUnlock()
// Generate session token.
randomID := make([]byte, sessionIDSize)
n, err := rand.Read(randomID)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate session ID: %w", err)
}
if n != sessionIDSize {
return nil, nil, fmt.Errorf("failed to get full session ID: only got %d bytes", n)
}
sessionID := base58.Encode(randomID)
// Create state and response.
state := &RequestHandlingState{
SessionID: sessionID,
PBlind: make(map[string]*PBlindSignerState, len(pblindRegistry)),
}
setup := &SetupResponse{
SessionID: sessionID,
PBlind: make(map[string]*PBlindSetupResponse, len(pblindRegistry)),
}
// Go through handlers and create setups.
for _, pblindHandler := range pblindRegistry {
// Check if we have a request for this handler.
_, ok := request.PBlind[pblindHandler.Zone()]
if !ok {
continue
}
plindState, pblindSetup, err := pblindHandler.CreateSetup()
if err != nil {
return nil, nil, fmt.Errorf("failed to create setup for %s: %w", pblindHandler.Zone(), err)
}
state.PBlind[pblindHandler.Zone()] = plindState
setup.PBlind[pblindHandler.Zone()] = pblindSetup
}
return state, setup, nil
}
// CreateTokenRequest creates a token request for all registered tokens.
func CreateTokenRequest(setup *SetupResponse) (request *TokenRequest, requestRequired bool, err error) {
registryLock.RLock()
defer registryLock.RUnlock()
// Check setup data.
if setup != nil && setup.SessionID == "" {
return nil, false, errors.New("setup data is missing a session ID")
}
// Create token request.
request = &TokenRequest{
PBlind: make(map[string]*PBlindTokenRequest, len(pblindRegistry)),
Scramble: make(map[string]*ScrambleTokenRequest, len(scrambleRegistry)),
}
if setup != nil {
request.SessionID = setup.SessionID
}
// Go through handlers and create requests.
if setup != nil {
for _, pblindHandler := range pblindRegistry {
// Check if we have setup data for this handler.
pblindSetup, ok := setup.PBlind[pblindHandler.Zone()]
if !ok {
// TODO: Abort if we should have received request data.
continue
}
// Create request.
pblindRequest, err := pblindHandler.CreateTokenRequest(pblindSetup)
if err != nil {
return nil, false, fmt.Errorf("failed to create token request for %s: %w", pblindHandler.Zone(), err)
}
requestRequired = true
request.PBlind[pblindHandler.Zone()] = pblindRequest
}
}
for _, scrambleHandler := range scrambleRegistry {
// Check if we need to request with this handler.
if scrambleHandler.ShouldRequest() {
requestRequired = true
request.Scramble[scrambleHandler.Zone()] = scrambleHandler.CreateTokenRequest()
}
}
return request, requestRequired, nil
}
// IssueTokens issues tokens for all registered tokens.
func IssueTokens(state *RequestHandlingState, request *TokenRequest) (response *IssuedTokens, err error) {
registryLock.RLock()
defer registryLock.RUnlock()
// Create token response.
response = &IssuedTokens{
PBlind: make(map[string]*IssuedPBlindTokens, len(pblindRegistry)),
Scramble: make(map[string]*IssuedScrambleTokens, len(scrambleRegistry)),
}
// Go through handlers and create requests.
for _, pblindHandler := range pblindRegistry {
// Check if we have all the data for issuing.
pblindState, ok := state.PBlind[pblindHandler.Zone()]
if !ok {
continue
}
pblindRequest, ok := request.PBlind[pblindHandler.Zone()]
if !ok {
continue
}
// Issue tokens.
pblindTokens, err := pblindHandler.IssueTokens(pblindState, pblindRequest)
if err != nil {
return nil, fmt.Errorf("failed to issue tokens for %s: %w", pblindHandler.Zone(), err)
}
response.PBlind[pblindHandler.Zone()] = pblindTokens
}
for _, scrambleHandler := range scrambleRegistry {
// Check if we have all the data for issuing.
scrambleRequest, ok := request.Scramble[scrambleHandler.Zone()]
if !ok {
continue
}
// Issue tokens.
scrambleTokens, err := scrambleHandler.IssueTokens(scrambleRequest)
if err != nil {
return nil, fmt.Errorf("failed to issue tokens for %s: %w", scrambleHandler.Zone(), err)
}
response.Scramble[scrambleHandler.Zone()] = scrambleTokens
}
return response, nil
}
// ProcessIssuedTokens processes issued tokens for all registered tokens.
func ProcessIssuedTokens(response *IssuedTokens) error {
registryLock.RLock()
defer registryLock.RUnlock()
// Go through handlers and create requests.
for _, pblindHandler := range pblindRegistry {
// Check if we received tokens.
pblindResponse, ok := response.PBlind[pblindHandler.Zone()]
if !ok {
continue
}
// Process issued tokens.
err := pblindHandler.ProcessIssuedTokens(pblindResponse)
if err != nil {
return fmt.Errorf("failed to process issued tokens for %s: %w", pblindHandler.Zone(), err)
}
}
for _, scrambleHandler := range scrambleRegistry {
// Check if we received tokens.
scrambleResponse, ok := response.Scramble[scrambleHandler.Zone()]
if !ok {
continue
}
// Process issued tokens.
err := scrambleHandler.ProcessIssuedTokens(scrambleResponse)
if err != nil {
return fmt.Errorf("failed to process issued tokens for %s: %w", scrambleHandler.Zone(), err)
}
}
return nil
}

View File

@@ -0,0 +1,125 @@
package token
import (
"testing"
"time"
"github.com/safing/portbase/formats/dsd"
)
func TestFull(t *testing.T) {
t.Parallel()
testStart := time.Now()
// Roundtrip 1
start := time.Now()
setupRequest, setupRequired := CreateSetupRequest()
if !setupRequired {
t.Fatal("setup should be required")
}
setupRequestData, err := dsd.Dump(setupRequest, dsd.CBOR)
if err != nil {
t.Fatal(err)
}
setupRequest = nil // nolint:ineffassign,wastedassign // Just to be sure.
t.Logf("setupRequest: %s, %d bytes", time.Since(start), len(setupRequestData))
start = time.Now()
loadedSetupRequest := &SetupRequest{}
_, err = dsd.Load(setupRequestData, loadedSetupRequest)
if err != nil {
t.Fatal(err)
}
serverState, setupResponse, err := HandleSetupRequest(loadedSetupRequest)
if err != nil {
t.Fatal(err)
}
setupResponseData, err := dsd.Dump(setupResponse, dsd.CBOR)
if err != nil {
t.Fatal(err)
}
setupResponse = nil // nolint:ineffassign,wastedassign // Just to be sure.
t.Logf("setupResponse: %s, %d bytes", time.Since(start), len(setupResponseData))
// Roundtrip 2
start = time.Now()
loadedSetupResponse := &SetupResponse{}
_, err = dsd.Load(setupResponseData, loadedSetupResponse)
if err != nil {
t.Fatal(err)
}
request, requestRequired, err := CreateTokenRequest(loadedSetupResponse)
if err != nil {
t.Fatal(err)
}
if !requestRequired {
t.Fatal("request should be required")
}
requestData, err := dsd.Dump(request, dsd.CBOR)
if err != nil {
t.Fatal(err)
}
request = nil // nolint:ineffassign,wastedassign // Just to be sure.
t.Logf("request: %s, %d bytes", time.Since(start), len(requestData))
start = time.Now()
loadedRequest := &TokenRequest{}
_, err = dsd.Load(requestData, loadedRequest)
if err != nil {
t.Fatal(err)
}
response, err := IssueTokens(serverState, loadedRequest)
if err != nil {
t.Fatal(err)
}
responseData, err := dsd.Dump(response, dsd.CBOR)
if err != nil {
t.Fatal(err)
}
response = nil // nolint:ineffassign,wastedassign // Just to be sure.
t.Logf("response: %s, %d bytes", time.Since(start), len(responseData))
start = time.Now()
loadedResponse := &IssuedTokens{}
_, err = dsd.Load(responseData, loadedResponse)
if err != nil {
t.Fatal(err)
}
err = ProcessIssuedTokens(loadedResponse)
if err != nil {
t.Fatal(err)
}
t.Logf("processing: %s", time.Since(start))
// Token Usage
for _, testZone := range []string{
PBlindTestZone,
ScrambleTestZone,
} {
start = time.Now()
token, err := GetToken(testZone)
if err != nil {
t.Fatal(err)
}
tokenData := token.Raw()
token = nil // nolint:wastedassign // Just to be sure.
loadedToken, err := ParseRawToken(tokenData)
if err != nil {
t.Fatal(err)
}
err = VerifyToken(loadedToken)
if err != nil {
t.Fatal(err)
}
t.Logf("using %s token: %s", testZone, time.Since(start))
}
t.Logf("full simulation took %s", time.Since(testStart))
}

View File

@@ -0,0 +1,240 @@
package token
import (
"fmt"
"sync"
"github.com/mr-tron/base58"
"github.com/safing/jess/lhash"
"github.com/safing/portbase/formats/dsd"
)
const (
scrambleSecretSize = 32
)
// ScrambleToken is token based on hashing.
type ScrambleToken struct {
Token []byte
}
// Pack packs the token.
func (pbt *ScrambleToken) Pack() ([]byte, error) {
return pbt.Token, nil
}
// UnpackScrambleToken unpacks the token.
func UnpackScrambleToken(token []byte) (*ScrambleToken, error) {
return &ScrambleToken{Token: token}, nil
}
// ScrambleHandler is a handler for the scramble tokens.
type ScrambleHandler struct {
sync.Mutex
opts *ScrambleOptions
storageLock sync.Mutex
Storage []*ScrambleToken
verifiersLock sync.RWMutex
verifiers map[string]*ScrambleToken
}
// ScrambleOptions are options for the ScrambleHandler.
type ScrambleOptions struct {
Zone string
Algorithm lhash.Algorithm
InitialTokens []string
InitialVerifiers []string
Fallback bool
}
// ScrambleTokenRequest is a token request.
type ScrambleTokenRequest struct{}
// IssuedScrambleTokens are issued scrambled tokens.
type IssuedScrambleTokens struct {
Tokens []*ScrambleToken
}
// NewScrambleHandler creates a new scramble handler.
func NewScrambleHandler(opts ScrambleOptions) (*ScrambleHandler, error) {
sh := &ScrambleHandler{
opts: &opts,
verifiers: make(map[string]*ScrambleToken, len(opts.InitialTokens)+len(opts.InitialVerifiers)),
}
// Add initial tokens.
sh.Storage = make([]*ScrambleToken, len(opts.InitialTokens))
for i, token := range opts.InitialTokens {
// Add to storage.
tokenData, err := base58.Decode(token)
if err != nil {
return nil, fmt.Errorf("failed to decode initial token %q: %w", token, err)
}
sh.Storage[i] = &ScrambleToken{
Token: tokenData,
}
// Add to verifiers.
scrambledToken := lhash.Digest(sh.opts.Algorithm, tokenData).Bytes()
sh.verifiers[string(scrambledToken)] = sh.Storage[i]
}
// Add initial verifiers.
for _, verifier := range opts.InitialVerifiers {
verifierData, err := base58.Decode(verifier)
if err != nil {
return nil, fmt.Errorf("failed to decode verifier %q: %w", verifier, err)
}
sh.verifiers[string(verifierData)] = &ScrambleToken{}
}
return sh, nil
}
// Zone returns the zone name.
func (sh *ScrambleHandler) Zone() string {
return sh.opts.Zone
}
// ShouldRequest returns whether the new tokens should be requested.
func (sh *ScrambleHandler) ShouldRequest() bool {
sh.storageLock.Lock()
defer sh.storageLock.Unlock()
return len(sh.Storage) == 0
}
// Amount returns the current amount of tokens in this handler.
func (sh *ScrambleHandler) Amount() int {
sh.storageLock.Lock()
defer sh.storageLock.Unlock()
return len(sh.Storage)
}
// IsFallback returns whether this handler should only be used as a fallback.
func (sh *ScrambleHandler) IsFallback() bool {
return sh.opts.Fallback
}
// CreateTokenRequest creates a token request to be sent to the token server.
func (sh *ScrambleHandler) CreateTokenRequest() (request *ScrambleTokenRequest) {
return &ScrambleTokenRequest{}
}
// IssueTokens sign the requested tokens.
func (sh *ScrambleHandler) IssueTokens(request *ScrambleTokenRequest) (response *IssuedScrambleTokens, err error) {
// Copy the storage.
tokens := make([]*ScrambleToken, len(sh.Storage))
copy(tokens, sh.Storage)
return &IssuedScrambleTokens{
Tokens: tokens,
}, nil
}
// ProcessIssuedTokens processes the issued token from the server.
func (sh *ScrambleHandler) ProcessIssuedTokens(issuedTokens *IssuedScrambleTokens) error {
sh.verifiersLock.RLock()
defer sh.verifiersLock.RUnlock()
// Validate tokens.
for i, newToken := range issuedTokens.Tokens {
// Scramle token.
scrambledToken := lhash.Digest(sh.opts.Algorithm, newToken.Token).Bytes()
// Check if token is valid.
_, ok := sh.verifiers[string(scrambledToken)]
if !ok {
return fmt.Errorf("invalid token on #%d", i)
}
}
// Copy to storage.
sh.Storage = issuedTokens.Tokens
return nil
}
// Verify verifies the given token.
func (sh *ScrambleHandler) Verify(token *Token) error {
if token.Zone != sh.opts.Zone {
return ErrZoneMismatch
}
// Hash the data.
scrambledToken := lhash.Digest(sh.opts.Algorithm, token.Data).Bytes()
sh.verifiersLock.RLock()
defer sh.verifiersLock.RUnlock()
// Check if token is valid.
_, ok := sh.verifiers[string(scrambledToken)]
if !ok {
return ErrTokenInvalid
}
return nil
}
// GetToken returns a token.
func (sh *ScrambleHandler) GetToken() (*Token, error) {
sh.storageLock.Lock()
defer sh.storageLock.Unlock()
if len(sh.Storage) == 0 {
return nil, ErrEmpty
}
return &Token{
Zone: sh.opts.Zone,
Data: sh.Storage[0].Token,
}, nil
}
// ScrambleStorage is a storage for scramble tokens.
type ScrambleStorage struct {
Storage []*ScrambleToken
}
// Save serializes and returns the current tokens.
func (sh *ScrambleHandler) Save() ([]byte, error) {
sh.storageLock.Lock()
defer sh.storageLock.Unlock()
if len(sh.Storage) == 0 {
return nil, ErrEmpty
}
s := &ScrambleStorage{
Storage: sh.Storage,
}
return dsd.Dump(s, dsd.CBOR)
}
// Load loads the given tokens into the handler.
func (sh *ScrambleHandler) Load(data []byte) error {
sh.storageLock.Lock()
defer sh.storageLock.Unlock()
s := &ScrambleStorage{}
_, err := dsd.Load(data, s)
if err != nil {
return err
}
sh.Storage = s.Storage
return nil
}
// Clear clears all the tokens in the handler.
func (sh *ScrambleHandler) Clear() {
sh.storageLock.Lock()
defer sh.storageLock.Unlock()
sh.Storage = nil
}

View File

@@ -0,0 +1,48 @@
package token
import (
"crypto/rand"
"fmt"
"testing"
"github.com/mr-tron/base58"
"github.com/safing/jess/lhash"
)
type genAlgs struct {
alg lhash.Algorithm
name string
}
func TestGenerateScrambleKeys(t *testing.T) {
t.Parallel()
for _, alg := range []genAlgs{
{alg: lhash.SHA2_256, name: "SHA2_256"},
{alg: lhash.SHA3_256, name: "SHA3_256"},
{alg: lhash.SHA3_512, name: "SHA3_512"},
{alg: lhash.BLAKE2b_256, name: "BLAKE2b_256"},
} {
token := make([]byte, scrambleSecretSize)
n, err := rand.Read(token)
if err != nil {
t.Fatal(err)
}
if n != scrambleSecretSize {
t.Fatalf("only got %d bytes", n)
}
scrambledToken := lhash.Digest(alg.alg, token).Bytes()
fmt.Printf(
"%s secret token: %s\n",
alg.name,
base58.Encode(token),
)
fmt.Printf(
"%s scrambled (public) token: %s\n",
alg.name,
base58.Encode(scrambledToken),
)
}
}

View File

@@ -0,0 +1,84 @@
package token
import (
"testing"
"github.com/safing/jess/lhash"
)
const ScrambleTestZone = "test-scramble"
func init() {
// Combined testing config.
h, err := NewScrambleHandler(ScrambleOptions{
Zone: ScrambleTestZone,
Algorithm: lhash.SHA2_256,
InitialTokens: []string{"2VqJ8BvDew1tUpytZhR7tuvq7ToPpW3tQtHvu3veE3iW"},
})
if err != nil {
panic(err)
}
err = RegisterScrambleHandler(h)
if err != nil {
panic(err)
}
}
func TestScramble(t *testing.T) {
t.Parallel()
opts := &ScrambleOptions{
Zone: ScrambleTestZone,
Algorithm: lhash.SHA2_256,
}
// Issuer
opts.InitialTokens = []string{"2VqJ8BvDew1tUpytZhR7tuvq7ToPpW3tQtHvu3veE3iW"}
issuer, err := NewScrambleHandler(*opts)
if err != nil {
t.Fatal(err)
}
// Client
opts.InitialTokens = nil
opts.InitialVerifiers = []string{"Cy9tz37Xq9NiXGDRU9yicjGU62GjXskE9KqUmuoddSxaE3"}
client, err := NewScrambleHandler(*opts)
if err != nil {
t.Fatal(err)
}
// Verifier
verifier, err := NewScrambleHandler(*opts)
if err != nil {
t.Fatal(err)
}
// Play through the whole use case.
request := client.CreateTokenRequest()
if err != nil {
t.Fatal(err)
}
issuedTokens, err := issuer.IssueTokens(request)
if err != nil {
t.Fatal(err)
}
err = client.ProcessIssuedTokens(issuedTokens)
if err != nil {
t.Fatal(err)
}
token, err := client.GetToken()
if err != nil {
t.Fatal(err)
}
err = verifier.Verify(token)
if err != nil {
t.Fatal(err)
}
}

83
spn/access/token/token.go Normal file
View File

@@ -0,0 +1,83 @@
package token
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/mr-tron/base58"
"github.com/safing/portbase/container"
)
// Token represents a token, consisting of a zone (name) and some data.
type Token struct {
Zone string
Data []byte
}
// GetToken returns a token of the given zone.
func GetToken(zone string) (*Token, error) {
handler, ok := GetHandler(zone)
if !ok {
return nil, ErrZoneUnknown
}
return handler.GetToken()
}
// VerifyToken verifies the given token.
func VerifyToken(token *Token) error {
handler, ok := GetHandler(token.Zone)
if !ok {
return ErrZoneUnknown
}
return handler.Verify(token)
}
// Raw returns the raw format of the token.
func (c *Token) Raw() []byte {
cont := container.New()
cont.Append([]byte(c.Zone))
cont.Append([]byte(":"))
cont.Append(c.Data)
return cont.CompileData()
}
// String returns the stringified format of the token.
func (c *Token) String() string {
return c.Zone + ":" + base58.Encode(c.Data)
}
// ParseRawToken parses a raw token.
func ParseRawToken(code []byte) (*Token, error) {
splitted := bytes.SplitN(code, []byte(":"), 2)
if len(splitted) < 2 {
return nil, errors.New("invalid code format: zone/data separator missing")
}
return &Token{
Zone: string(splitted[0]),
Data: splitted[1],
}, nil
}
// ParseToken parses a stringified token.
func ParseToken(code string) (*Token, error) {
splitted := strings.SplitN(code, ":", 2)
if len(splitted) < 2 {
return nil, errors.New("invalid code format: zone/data separator missing")
}
data, err := base58.Decode(splitted[1])
if err != nil {
return nil, fmt.Errorf("invalid code format: %w", err)
}
return &Token{
Zone: splitted[0],
Data: data,
}, nil
}

View File

@@ -0,0 +1,33 @@
package token
import (
"testing"
"github.com/safing/portbase/rng"
)
func TestToken(t *testing.T) {
t.Parallel()
randomData, err := rng.Bytes(32)
if err != nil {
t.Fatal(err)
}
c := &Token{
Zone: "test",
Data: randomData,
}
s := c.String()
_, err = ParseToken(s)
if err != nil {
t.Fatal(err)
}
r := c.Raw()
_, err = ParseRawToken(r)
if err != nil {
t.Fatal(err)
}
}