-
Notifications
You must be signed in to change notification settings - Fork 16
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
base: main
Are you sure you want to change the base?
Changes from 24 commits
a2177ac
c2e1595
1ef77e8
cdb4455
c2a542e
e40e8fd
a4712db
4dfa84f
88e9813
83ef2a4
5acee42
13fd01c
e6a4b35
bb43e81
2c36a16
cf5dcda
da28e35
d895532
00b67a4
a809c1c
5803b51
aae9c45
2bb5c5c
6cb806e
fff5b5f
d567ef5
6f59cef
d5529df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given we want backwards compatibility, is a better default authHeadersVersion1? What's the migration period for the gateway server? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change shouldn't effect current users, but v2 should be our default going forward. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a malicious actor steals a v2 token or v1 token, changes the version to 1, 2, or 3 and then submits it to the gateway, how would the gateway currently respond? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If version of the token is not matching token scheme we expect, we consider the token e.g we get a token that is version |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are there production use cases for setting this timestamp? If not, a comment may be helpful to call out the foot gun with setting the timestamp outside of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No production use-cases, any invalid values are corrected for
|
||
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)} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing a private key around will make eventual isolation of the keystore difficult. If possible, I'd like to see us use the keystore directly here