wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
15
spn/access/token/errors.go
Normal file
15
spn/access/token/errors.go
Normal 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")
|
||||
)
|
||||
13
spn/access/token/module_test.go
Normal file
13
spn/access/token/module_test.go
Normal 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
552
spn/access/token/pblind.go
Normal 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
|
||||
}
|
||||
39
spn/access/token/pblind_gen_test.go
Normal file
39
spn/access/token/pblind_gen_test.go
Normal 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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
260
spn/access/token/pblind_test.go
Normal file
260
spn/access/token/pblind_test.go
Normal 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
|
||||
}
|
||||
116
spn/access/token/registry.go
Normal file
116
spn/access/token/registry.go
Normal 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
244
spn/access/token/request.go
Normal 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
|
||||
}
|
||||
125
spn/access/token/request_test.go
Normal file
125
spn/access/token/request_test.go
Normal 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))
|
||||
}
|
||||
240
spn/access/token/scramble.go
Normal file
240
spn/access/token/scramble.go
Normal 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
|
||||
}
|
||||
48
spn/access/token/scramble_gen_test.go
Normal file
48
spn/access/token/scramble_gen_test.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
84
spn/access/token/scramble_test.go
Normal file
84
spn/access/token/scramble_test.go
Normal 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
83
spn/access/token/token.go
Normal 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
|
||||
}
|
||||
33
spn/access/token/token_test.go
Normal file
33
spn/access/token/token_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user