🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Commit 4253d7b

Browse files
committed
authN: persistent RSA key pair
Signed-off-by: Aaron Wilson <aawilson@nvidia.com>
1 parent 5eaf263 commit 4253d7b

4 files changed

Lines changed: 135 additions & 30 deletions

File tree

‎api/authn/config.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ type (
4545
// Also used to determine max-age for client caches of JWKS
4646
Expire cos.Duration `json:"expiration_time"`
4747
PubKey *string `json:"public_key"`
48+
// Size of RSA private key to generate
49+
RSAKeyBits int `json:"rsa_key_bits"`
4850
}
4951
TimeoutConf struct {
5052
Default cos.Duration `json:"default_timeout"`

‎api/env/authn.go‎

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@ package env
99

1010
//nolint:gosec // false positive G101
1111
const (
12-
AisAuthEnabled = "AIS_AUTHN_ENABLED"
13-
AisAuthURL = "AIS_AUTHN_URL"
14-
AisAuthTokenFile = "AIS_AUTHN_TOKEN_FILE" // fully qualified
15-
AisAuthToken = "AIS_AUTHN_TOKEN" // Only the JWT token itself (excluding the file and JSON)
16-
AisAuthConfDir = "AIS_AUTHN_CONF_DIR" // contains AuthN config and tokens DB
17-
AisAuthLogDir = "AIS_AUTHN_LOG_DIR"
18-
AisAuthLogLevel = "AIS_AUTHN_LOG_LEVEL"
19-
AisAuthExternalURL = "AIS_AUTHN_EXTERNAL_URL"
20-
AisAuthPort = "AIS_AUTHN_PORT"
21-
AisAuthTTL = "AIS_AUTHN_TTL"
22-
AisAuthUseHTTPS = "AIS_AUTHN_USE_HTTPS"
23-
AisAuthServerCrt = "AIS_SERVER_CRT"
24-
AisAuthServerKey = "AIS_SERVER_KEY"
25-
AisAuthSecretKey = "AIS_AUTHN_SECRET_KEY"
26-
AisAuthPublicKey = "AIS_AUTHN_PUBLIC_KEY" // for asymmetric tokens
27-
AisAuthAdminUsername = "AIS_AUTHN_SU_NAME"
28-
AisAuthAdminPassword = "AIS_AUTHN_SU_PASS"
12+
AisAuthEnabled = "AIS_AUTHN_ENABLED"
13+
AisAuthURL = "AIS_AUTHN_URL"
14+
AisAuthTokenFile = "AIS_AUTHN_TOKEN_FILE" // fully qualified
15+
AisAuthToken = "AIS_AUTHN_TOKEN" // Only the JWT token itself (excluding the file and JSON)
16+
AisAuthConfDir = "AIS_AUTHN_CONF_DIR" // contains AuthN config and tokens DB
17+
AisAuthLogDir = "AIS_AUTHN_LOG_DIR"
18+
AisAuthLogLevel = "AIS_AUTHN_LOG_LEVEL"
19+
AisAuthExternalURL = "AIS_AUTHN_EXTERNAL_URL"
20+
AisAuthPort = "AIS_AUTHN_PORT"
21+
AisAuthTTL = "AIS_AUTHN_TTL"
22+
AisAuthUseHTTPS = "AIS_AUTHN_USE_HTTPS"
23+
AisAuthServerCrt = "AIS_SERVER_CRT"
24+
AisAuthServerKey = "AIS_SERVER_KEY"
25+
AisAuthSecretKey = "AIS_AUTHN_SECRET_KEY"
26+
AisAuthPrivateKeyFile = "AIS_AUTHN_PRIVATE_KEY_FILE"
27+
AisAuthPublicKey = "AIS_AUTHN_PUBLIC_KEY" // for asymmetric tokens
28+
AisAuthAdminUsername = "AIS_AUTHN_SU_NAME"
29+
AisAuthAdminPassword = "AIS_AUTHN_SU_PASS"
2930
)

‎cmd/authn/config/cmgr.go‎

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"fmt"
1414
"net/url"
1515
"os"
16+
"path/filepath"
1617
"sync"
1718
"sync/atomic"
1819
"time"
@@ -21,6 +22,7 @@ import (
2122
"github.com/NVIDIA/aistore/api/env"
2223
"github.com/NVIDIA/aistore/cmn"
2324
"github.com/NVIDIA/aistore/cmn/cos"
25+
"github.com/NVIDIA/aistore/cmn/fname"
2426
"github.com/NVIDIA/aistore/cmn/jsp"
2527
"github.com/NVIDIA/aistore/cmn/nlog"
2628

@@ -33,17 +35,20 @@ const (
3335

3436
// Constants for creating RSA keys and key set
3537
const (
36-
SigningKeyUsage = "sig"
37-
PublicKeyPEMType = "PUBLIC KEY"
38+
SigningKeyUsage = "sig"
39+
PublicKeyPEMType = "PUBLIC KEY"
40+
PrivateKeyPEMType = "PRIVATE KEY"
41+
MinRSAKeyBits = 2048
3842
)
3943

4044
// ConfManager Used by AuthN to interface with the shared authN config used on both API and server-side
4145
// Provides utilities used only by the authN service (not API)
4246
type ConfManager struct {
43-
filePath string
44-
conf atomic.Pointer[authn.Config]
45-
jwks jwk.Set
46-
mu sync.Mutex
47+
filePath string
48+
keyFilePath string
49+
conf atomic.Pointer[authn.Config]
50+
jwks jwk.Set
51+
mu sync.Mutex
4752
}
4853

4954
func NewConfManager() *ConfManager {
@@ -98,8 +103,6 @@ func (cm *ConfManager) UpdateConf(cu *authn.ConfigToUpdate) error {
98103
}
99104

100105
func (cm *ConfManager) Init(configPath string) {
101-
cm.mu.Lock()
102-
defer cm.mu.Unlock()
103106
conf, err := cm.loadFromDisk(configPath)
104107
if err != nil {
105108
cos.ExitLogf("%s service failed to start: %v", ServiceName, err)
@@ -113,7 +116,8 @@ func (cm *ConfManager) Init(configPath string) {
113116
return
114117
}
115118
nlog.Infof("No HMAC secret provided via config or %q, initializing with RSA", env.AisAuthSecretKey)
116-
err = cm.createRSA()
119+
cm.initKeyFile(configPath)
120+
err = cm.initRSA()
117121
if err != nil {
118122
cos.ExitLogf("Failed to initialize RSA key: %v", err)
119123
}
@@ -231,13 +235,79 @@ func (cm *ConfManager) GetSecretChecksum() string {
231235
// RSA and OIDC //
232236
//////////////////
233237

234-
// Updates both the service config and the jwks, must be called under lock
235-
func (cm *ConfManager) createRSA() error {
236-
// Generate RSA256 key pair
237-
key, err := rsa.GenerateKey(rand.Reader, 2048)
238+
// Determine location for private key persistence
239+
func (cm *ConfManager) initKeyFile(configPath string) {
240+
if pFile := os.Getenv(env.AisAuthPrivateKeyFile); pFile != "" {
241+
cm.keyFilePath = pFile
242+
} else {
243+
configDir := filepath.Dir(configPath)
244+
cm.keyFilePath = filepath.Join(configDir, fname.AuthNRSAKey)
245+
}
246+
}
247+
248+
// Initialize an RSA key pair, using one from disk if provided
249+
// Must only be called at init time -- key rotation not yet implemented
250+
func (cm *ConfManager) initRSA() error {
251+
// Try to load existing key
252+
err := cm.loadRSAFromDisk()
253+
if err == nil {
254+
nlog.Infof("Loaded existing RSA private key from %s", cm.keyFilePath)
255+
return nil
256+
}
257+
if errors.Is(err, os.ErrNotExist) {
258+
nlog.Infof("No RSA key found on disk at %q", cm.keyFilePath)
259+
} else {
260+
nlog.Infof("Failed to load RSA key from file %q [err: %v]", cm.keyFilePath, err)
261+
}
262+
// Generate RSA key pair based on length from config
263+
key, err := rsa.GenerateKey(rand.Reader, cm.GetKeySize())
238264
if err != nil {
239265
return err
240266
}
267+
nlog.Infof("Generated new RSA key pair with modulus size %d bytes", key.Size())
268+
err = cm.setRSAFromPrivate(key)
269+
if err != nil {
270+
return err
271+
}
272+
return cm.saveRSAToDisk()
273+
}
274+
275+
func (cm *ConfManager) loadRSAFromDisk() error {
276+
fi, err := os.Stat(cm.keyFilePath)
277+
if err != nil {
278+
return fmt.Errorf("stat key file: %w", err)
279+
}
280+
if fi.IsDir() {
281+
return errors.New("key file is a directory")
282+
}
283+
if fi.Size() == 0 {
284+
return errors.New("key file is empty")
285+
}
286+
keyBytes, err := os.ReadFile(cm.keyFilePath)
287+
if err != nil {
288+
return fmt.Errorf("read key file: %w", err)
289+
}
290+
block, _ := pem.Decode(keyBytes)
291+
if block == nil {
292+
return errors.New("decoding private key PEM block failed")
293+
}
294+
keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
295+
if err != nil {
296+
return fmt.Errorf("parse PKCS8 private key: %w", err)
297+
}
298+
key, ok := keyAny.(*rsa.PrivateKey)
299+
if !ok {
300+
return errors.New("key type invalid")
301+
}
302+
if keyErr := key.Validate(); keyErr != nil {
303+
return fmt.Errorf("key validation failed: %w", keyErr)
304+
}
305+
return cm.setRSAFromPrivate(key)
306+
}
307+
308+
// Updates both the service config and the jwks
309+
// If called from outside init, must be called under lock
310+
func (cm *ConfManager) setRSAFromPrivate(key *rsa.PrivateKey) error {
241311
// Convert to PKIX bytes
242312
pubBytes, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
243313
if err != nil {
@@ -262,6 +332,29 @@ func (cm *ConfManager) setRSAKey(pKey *rsa.PrivateKey, pubStr string) {
262332
cm.conf.Store(&next)
263333
}
264334

335+
func (cm *ConfManager) saveRSAToDisk() error {
336+
privateKey := cm.GetPrivateKey()
337+
if privateKey == nil {
338+
return errors.New("no private key to save")
339+
}
340+
keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
341+
if err != nil {
342+
return fmt.Errorf("failed to marshal private key: %w", err)
343+
}
344+
keyPEM := pem.EncodeToMemory(&pem.Block{
345+
Type: PrivateKeyPEMType,
346+
Bytes: keyBytes,
347+
})
348+
349+
// Write to file with restricted permissions (0600 - owner read/write only)
350+
if err := os.WriteFile(cm.keyFilePath, keyPEM, 0o600); err != nil {
351+
return fmt.Errorf("failed to write RSA key to %s: %w", cm.keyFilePath, err)
352+
}
353+
354+
nlog.Infof("Saved RSA private key to %s", cm.keyFilePath)
355+
return nil
356+
}
357+
265358
func (cm *ConfManager) GetPrivateKey() *rsa.PrivateKey {
266359
return cm.conf.Load().GetPrivateKey()
267360
}
@@ -274,6 +367,14 @@ func (cm *ConfManager) GetKeySet() jwk.Set {
274367
return cm.jwks
275368
}
276369

370+
func (cm *ConfManager) GetKeySize() int {
371+
bits := cm.conf.Load().Server.RSAKeyBits
372+
if bits < MinRSAKeyBits {
373+
return MinRSAKeyBits
374+
}
375+
return bits
376+
}
377+
277378
// Sets jwks for the manager, must be called under lock when creating RSA
278379
func (cm *ConfManager) createPubJWKS() error {
279380
conf := cm.conf.Load()

‎cmn/fname/fname.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
// AuthN: config and DB
4040
AuthNConfig = "authn.json"
4141
AuthNDB = "authn.db"
42+
AuthNRSAKey = "authn_rsa.key"
4243

4344
// Token
4445
Token = "auth.token"

0 commit comments

Comments
 (0)