Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

INFOPLAT-1562 dynamic expiring auth headers #974

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a2177ac
INFOPLAT-1559 Adds auth header token expiry functionality
hendoxc Dec 4, 2024
c2e1595
INFOPLAT-1559 Removes sleep
hendoxc Dec 6, 2024
1ef77e8
INFOPLAT-1559 Updates test comments
hendoxc Dec 6, 2024
cdb4455
INFOPLAT-1560 Adds grpc.PerRPCCredentials implementation
hendoxc Dec 13, 2024
c2a542e
INFOPLAT-1560 Adds `AuthHeaderProvider` to beholder client config
hendoxc Dec 13, 2024
e40e8fd
INFOPLAT-1560 Allows `AuthHeaderProvider` to be used instead of stati…
hendoxc Dec 13, 2024
a4712db
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 17, 2024
4dfa84f
INFOPLAT-1560 Update `refresh` logic
hendoxc Dec 17, 2024
88e9813
INFOPLAT-1560 Makes `refresh` thread safe
hendoxc Dec 17, 2024
83ef2a4
INFOPLAT-1560 Makes `RequireTransportSecurity` configurable
hendoxc Dec 17, 2024
5acee42
INFOPLAT-1560 Fixes tests
hendoxc Dec 17, 2024
13fd01c
INFOPLAT-1560 Runs formating and linting
hendoxc Dec 17, 2024
e6a4b35
INFOPLAT-1560 Runs formating and linting
hendoxc Dec 17, 2024
bb43e81
Merge branch 'INFOPLAT-1562-dynamic-expiring-auth-headers' of github.…
hendoxc Dec 17, 2024
2c36a16
INFOPLAT 1560 Add tests for NewAuthHeaderProvider and authHeaderPerRP…
hendoxc Dec 17, 2024
cf5dcda
INFOPLAT-1560 SImplifies caller usage
hendoxc Dec 19, 2024
da28e35
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 19, 2024
d895532
INFOPLAT-1562 Handle configuration inconsistency
hendoxc Dec 20, 2024
00b67a4
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 20, 2024
a809c1c
Merge branch 'main' into INFOPLAT-1562-dynamic-expiring-auth-headers
hendoxc Dec 23, 2024
5803b51
Merge branch 'INFOPLAT-1562-dynamic-expiring-auth-headers' of github.…
hendoxc Dec 23, 2024
aae9c45
INFOPLAT-1562 Reconfigure loop configs to use AuthHeaderProvider
hendoxc Dec 23, 2024
2bb5c5c
Revert "INFOPLAT-1562 Reconfigure loop configs to use AuthHeaderProvi…
hendoxc Dec 23, 2024
963f8c3
INFOPLAT1-1562 Reconfigures loops to use AuthHeaderProvider
hendoxc Dec 23, 2024
0190cd3
INFOPLAT-1562 Fixes test
hendoxc Dec 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 151 additions & 8 deletions pkg/beholder/auth.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,170 @@
package beholder

import (
"context"
"crypto/ed25519"
"encoding/binary"
"fmt"
"sync"
"time"

"google.golang.org/grpc/credentials"
)

const (
// authHeaderKey is the name of the header that the node authenticator will use to send the auth token
authHeaderKey = "X-Beholder-Node-Auth-Token"
// authHeaderVersion is the version of the auth header format
authHeaderVersion1 = "1"
authHeaderVersion2 = "2"
// defaultAuthHeaderTTL is the default time before the auth header is refreshed
defaultAuthHeaderTTL = 1 * time.Minute
)

// authHeaderKey is the name of the header that the node authenticator will use to send the auth token
var authHeaderKey = "X-Beholder-Node-Auth-Token"
type AuthHeaderProvider interface {
// Credentials returns the PerRPCCredentials implementation
Credentials() credentials.PerRPCCredentials
// SetRequireTransportSecurity sets the value of requireTransportSecurity
SetRequireTransportSecurity(bool)
}

// AuthHeaderProviderConfig configures AuthHeaderProvider
type AuthHeaderProviderConfig struct {
HeaderTTL time.Duration
Version string
RequireTransportSecurity bool
}

// authHeaderPerRPCredentials is a PerRPCCredentials implementation that provides the auth headers
type authHeaderPerRPCCredentials struct {
privKey ed25519.PrivateKey
lastUpdated time.Time
headerTTL time.Duration
requireTransportSecurity bool
headers map[string]string
version string
mu sync.Mutex
}

func (config AuthHeaderProviderConfig) New(privKey ed25519.PrivateKey) AuthHeaderProvider {
if config.HeaderTTL <= 0 {
config.HeaderTTL = defaultAuthHeaderTTL
}
if config.Version == "" {
config.Version = authHeaderVersion2
}

creds := &authHeaderPerRPCCredentials{
privKey: privKey,
headerTTL: config.HeaderTTL,
version: config.Version,
requireTransportSecurity: config.RequireTransportSecurity,
}
// Initialize the headers ~ lastUpdated is 0 so the headers are generated on the first call
creds.refresh()
return creds
}

func NewAuthHeaderProvider(privKey ed25519.PrivateKey) AuthHeaderProvider {
return AuthHeaderProviderConfig{}.New(privKey)
}

func (a *authHeaderPerRPCCredentials) Credentials() credentials.PerRPCCredentials {
return a
}

func (a *authHeaderPerRPCCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
return a.getHeaders(), nil
}

func (a *authHeaderPerRPCCredentials) RequireTransportSecurity() bool {
return a.requireTransportSecurity
}

// SetRequireTransportSecurity sets the value of requireTransportSecurity
// This is to safeguard against inconsistent values between the PerRPCCredentials and the AuthHeaderProvider
func (a *authHeaderPerRPCCredentials) SetRequireTransportSecurity(newValue bool) {
a.mu.Lock()
defer a.mu.Unlock()
a.requireTransportSecurity = newValue
}

// getHeaders returns the auth headers, refreshing them if they are expired
func (a *authHeaderPerRPCCredentials) getHeaders() map[string]string {
if time.Since(a.lastUpdated) > a.headerTTL {
a.refresh()
}
hendoxc marked this conversation as resolved.
Show resolved Hide resolved
return a.headers
}

// refresh creates a new signed auth header token and sets the lastUpdated time to now
func (a *authHeaderPerRPCCredentials) refresh() {
a.mu.Lock()
defer a.mu.Unlock()

// authHeaderVersion is the version of the auth header format
var authHeaderVersion = "1"
timeNow := time.Now()

// BuildAuthHeaders creates the auth header value to be included on requests.
// The current format for the header is:
switch a.version {
// refresh doesn't actually do anything for version 1 since we are only signing the public key
// this for backwards compatibility and smooth transition to version 2
case authHeaderVersion1:
a.headers = BuildAuthHeaders(a.privKey)
case authHeaderVersion2:
a.headers = buildAuthHeadersV2(a.privKey, &AuthHeaderConfig{timestamp: timeNow.UnixMilli()})
default:
a.headers = buildAuthHeadersV2(a.privKey, &AuthHeaderConfig{timestamp: timeNow.UnixMilli()})
}
// Set the lastUpdated time to now
a.lastUpdated = timeNow
}

// AuthHeaderConfig configures buildAuthHeadersV2
type AuthHeaderConfig struct {
timestamp int64
version string
}

// BuildAuthHeaders creates the auth headers to be included on requests.
// There are two formats for the header. Version `1` is:
//
// <version>:<public_key_hex>:<signature_hex>
//
// where the byte value of <public_key_hex> is what's being signed
// and <signature_hex> is the signature of the public key.
func BuildAuthHeaders(privKey ed25519.PrivateKey) map[string]string {
pubKey := privKey.Public().(ed25519.PublicKey)
messageBytes := pubKey
signature := ed25519.Sign(privKey, messageBytes)
headerValue := fmt.Sprintf("%s:%x:%x", authHeaderVersion, messageBytes, signature)

return map[string]string{authHeaderKey: headerValue}
return map[string]string{authHeaderKey: fmt.Sprintf("%s:%x:%x", authHeaderVersion1, messageBytes, signature)}
}

// buildAuthHeadersV2 creates the auth headers to be included on requests.
// Version `2` is:
//
// <version>:<public_key_hex>:<timestamp>:<signature_hex>
//
// where the concatenated byte value of <public_key_hex> & <timestamp> is what's being signed
func buildAuthHeadersV2(privKey ed25519.PrivateKey, config *AuthHeaderConfig) map[string]string {
hendoxc marked this conversation as resolved.
Show resolved Hide resolved
if config == nil {
config = &AuthHeaderConfig{}
}
if config.version == "" {
config.version = authHeaderVersion2
}
// If timestamp is negative or 0, set it to current timestamp.
// negative values cause overflow on conversion to uint64
if config.timestamp <= 0 {
config.timestamp = time.Now().UnixMilli()
}

pubKey := privKey.Public().(ed25519.PublicKey)

timestampUnixMsBytes := make([]byte, 8)
binary.BigEndian.PutUint64(timestampUnixMsBytes, uint64(config.timestamp))

messageBytes := append(pubKey, timestampUnixMsBytes...)
signature := ed25519.Sign(privKey, messageBytes)

return map[string]string{authHeaderKey: fmt.Sprintf("%s:%x:%d:%x", config.version, pubKey, config.timestamp, signature)}
}
Loading
Loading