@@ -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
3537const (
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)
4246type 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
4954func NewConfManager () * ConfManager {
@@ -98,8 +103,6 @@ func (cm *ConfManager) UpdateConf(cu *authn.ConfigToUpdate) error {
98103}
99104
100105func (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+
265358func (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
278379func (cm * ConfManager ) createPubJWKS () error {
279380 conf := cm .conf .Load ()
0 commit comments