-
Notifications
You must be signed in to change notification settings - Fork 359
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
feat: Allow master configuration for ssh key crypto system #10072
Changes from 15 commits
5cbd0d2
2a05eb1
b46f97f
ae88b02
e444bc4
5061abe
1a169d4
078d145
305b16e
e32501d
3c6475f
76227ad
6442f9a
6d53f1a
c97313e
bf78880
1690bb7
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 |
---|---|---|
@@ -0,0 +1,8 @@ | ||
:orphan: | ||
|
||
**Improvements** | ||
|
||
- Master Configuration: Add support for crypto system configuration for ssh connection. | ||
``security.key_type`` now accepts ``RSA``, ``ECDSA`` or ``ED25519``. Default key type is changed | ||
from ``1024-bit RSA`` to ``ED25519``, since ``ED25519`` keys are faster and more secure than the | ||
old default, and ``ED25519`` is also the default key type for ``ssh-keygen``. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,15 @@ const ( | |
preemptionScheduler = "preemption" | ||
) | ||
|
||
const ( | ||
// RSAKeyType uses RSA. | ||
RSAKeyType = "RSA" | ||
// ECDSAKeyType uses ECDSA. | ||
ECDSAKeyType = "ECDSA" | ||
// ED25519KeyType uses ED25519. | ||
ED25519KeyType = "ED25519" | ||
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. Idiomatically, these names should be |
||
) | ||
|
||
type ( | ||
// ExperimentConfigPatch is the updatedble fields for patching an experiment. | ||
ExperimentConfigPatch struct { | ||
|
@@ -108,7 +117,7 @@ func DefaultConfig() *Config { | |
Group: "root", | ||
}, | ||
SSH: SSHConfig{ | ||
RsaKeySize: 1024, | ||
KeyType: ED25519KeyType, | ||
}, | ||
AuthZ: *DefaultAuthZConfig(), | ||
}, | ||
|
@@ -452,7 +461,8 @@ type SecurityConfig struct { | |
|
||
// SSHConfig is the configuration setting for SSH. | ||
type SSHConfig struct { | ||
RsaKeySize int `json:"rsa_key_size"` | ||
RsaKeySize int `json:"rsa_key_size"` | ||
KeyType string `json:"key_type"` | ||
} | ||
|
||
// TLSConfig is the configuration for setting up serving over TLS. | ||
|
@@ -475,6 +485,12 @@ func (t *TLSConfig) Validate() []error { | |
// Validate implements the check.Validatable interface. | ||
func (t *SSHConfig) Validate() []error { | ||
var errs []error | ||
if t.KeyType != RSAKeyType && t.KeyType != ECDSAKeyType && t.KeyType != ED25519KeyType { | ||
errs = append(errs, errors.New("Crypto system must be one of 'RSA', 'ECDSA' or 'ED25519'")) | ||
} | ||
if t.KeyType != RSAKeyType { | ||
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. can we flip this and put this rsa specific stuff in an 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. +1. |
||
return errs | ||
} | ||
if t.RsaKeySize < 1 { | ||
rb-determined-ai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
errs = append(errs, errors.New("RSA Key size must be greater than 0")) | ||
} else if t.RsaKeySize > 16384 { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,17 +1,23 @@ | ||||||||||||||||||||||
package ssh | ||||||||||||||||||||||
|
||||||||||||||||||||||
import ( | ||||||||||||||||||||||
"crypto/ecdsa" | ||||||||||||||||||||||
"crypto/ed25519" | ||||||||||||||||||||||
"crypto/elliptic" | ||||||||||||||||||||||
"crypto/rand" | ||||||||||||||||||||||
"crypto/rsa" | ||||||||||||||||||||||
"crypto/x509" | ||||||||||||||||||||||
"encoding/pem" | ||||||||||||||||||||||
|
||||||||||||||||||||||
"github.com/pkg/errors" | ||||||||||||||||||||||
sshlib "golang.org/x/crypto/ssh" | ||||||||||||||||||||||
|
||||||||||||||||||||||
"github.com/determined-ai/determined/master/internal/config" | ||||||||||||||||||||||
) | ||||||||||||||||||||||
|
||||||||||||||||||||||
const ( | ||||||||||||||||||||||
trialPEMBlockType = "RSA PRIVATE KEY" | ||||||||||||||||||||||
rsaPEMBlockType = "RSA PRIVATE KEY" | ||||||||||||||||||||||
ecdsaPEMBlockType = "EC PRIVATE KEY" | ||||||||||||||||||||||
) | ||||||||||||||||||||||
|
||||||||||||||||||||||
// PrivateAndPublicKeys contains a private and public key. | ||||||||||||||||||||||
|
@@ -21,34 +27,97 @@ type PrivateAndPublicKeys struct { | |||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// GenerateKey returns a private and public SSH key. | ||||||||||||||||||||||
func GenerateKey(rsaKeySize int, passphrase *string) (PrivateAndPublicKeys, error) { | ||||||||||||||||||||||
func GenerateKey(conf config.SSHConfig) (PrivateAndPublicKeys, error) { | ||||||||||||||||||||||
var generatedKeys PrivateAndPublicKeys | ||||||||||||||||||||||
switch conf.KeyType { | ||||||||||||||||||||||
case config.RSAKeyType: | ||||||||||||||||||||||
return generateRSAKey(conf.RsaKeySize) | ||||||||||||||||||||||
case config.ECDSAKeyType: | ||||||||||||||||||||||
return generateECDSAKey() | ||||||||||||||||||||||
case config.ED25519KeyType: | ||||||||||||||||||||||
return generateED25519Key() | ||||||||||||||||||||||
default: | ||||||||||||||||||||||
return generatedKeys, errors.New("Invalid crypto system") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
func generateRSAKey(rsaKeySize int) (PrivateAndPublicKeys, error) { | ||||||||||||||||||||||
var generatedKeys PrivateAndPublicKeys | ||||||||||||||||||||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate private key") | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate RSA private key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
if err = privateKey.Validate(); err != nil { | ||||||||||||||||||||||
return generatedKeys, err | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
block := &pem.Block{ | ||||||||||||||||||||||
Type: trialPEMBlockType, | ||||||||||||||||||||||
Type: rsaPEMBlockType, | ||||||||||||||||||||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey), | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
if passphrase != nil { | ||||||||||||||||||||||
// TODO: Replace usage of deprecated x509.EncryptPEMBlock. | ||||||||||||||||||||||
block, err = x509.EncryptPEMBlock( //nolint: staticcheck | ||||||||||||||||||||||
rand.Reader, block.Type, block.Bytes, []byte(*passphrase), x509.PEMCipherAES256) | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to encrypt private key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
publicKey, err := sshlib.NewPublicKey(&privateKey.PublicKey) | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate RSA public key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
generatedKeys = PrivateAndPublicKeys{ | ||||||||||||||||||||||
PrivateKey: pem.EncodeToMemory(block), | ||||||||||||||||||||||
PublicKey: sshlib.MarshalAuthorizedKey(publicKey), | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
return generatedKeys, nil | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
func generateECDSAKey() (PrivateAndPublicKeys, error) { | ||||||||||||||||||||||
var generatedKeys PrivateAndPublicKeys | ||||||||||||||||||||||
// Curve size currently not configurable, using the NIST recommendation. | ||||||||||||||||||||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||||||||||||||||||||||
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. Just as a note: I don't think there were any requests to make the curve size configurable, and P256 is the NIST recommendation, so this is fine. But one day we may have to update it or make configurable. |
||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate ECDSA private key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
privateKeyBytes, err := x509.MarshalECPrivateKey(privateKey) | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to marshal ECDSA private key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
block := &pem.Block{ | ||||||||||||||||||||||
Type: ecdsaPEMBlockType, | ||||||||||||||||||||||
Bytes: privateKeyBytes, | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
publicKey, err := sshlib.NewPublicKey(&privateKey.PublicKey) | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate public key") | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate ECDSA public key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
generatedKeys = PrivateAndPublicKeys{ | ||||||||||||||||||||||
PrivateKey: pem.EncodeToMemory(block), | ||||||||||||||||||||||
PublicKey: sshlib.MarshalAuthorizedKey(publicKey), | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
return generatedKeys, nil | ||||||||||||||||||||||
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. nit
Suggested change
|
||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
func generateED25519Key() (PrivateAndPublicKeys, error) { | ||||||||||||||||||||||
var generatedKeys PrivateAndPublicKeys | ||||||||||||||||||||||
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. same nit here 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. The nit being that this could skip being declared at all? If so, I agree. I'd rather see return PrivateAndPublicKeys{}, errors.Wrap(err, "unable to generate ED25519 private key") and then no 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. agree |
||||||||||||||||||||||
|
||||||||||||||||||||||
ed25519PublicKey, privateKey, err := ed25519.GenerateKey(nil) | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate ED25519 private key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Before OpenSSH 9.6, for ED25519 keys, only the OpenSSH private key format was supported. | ||||||||||||||||||||||
block, err := sshlib.MarshalPrivateKey(privateKey, "") | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to marshal ED25519 private key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
publicKey, err := sshlib.NewPublicKey(ed25519PublicKey) | ||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return generatedKeys, errors.Wrap(err, "unable to generate ED25519 public key") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
generatedKeys = PrivateAndPublicKeys{ | ||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package ssh | ||
|
||
import ( | ||
"testing" | ||
|
||
"golang.org/x/crypto/ssh" | ||
"gotest.tools/assert" | ||
|
||
"github.com/determined-ai/determined/master/internal/config" | ||
) | ||
|
||
func verifyKeys(t *testing.T, keys PrivateAndPublicKeys) { | ||
privateKey, err := ssh.ParsePrivateKey(keys.PrivateKey) | ||
assert.NilError(t, err) | ||
|
||
publickKey, _, _, _, err := ssh.ParseAuthorizedKey(keys.PublicKey) //nolint:dogsled | ||
assert.NilError(t, err) | ||
assert.Equal(t, string(publickKey.Marshal()), string(privateKey.PublicKey().Marshal())) | ||
} | ||
|
||
func TestSSHKeyGenerate(t *testing.T) { | ||
keys, err := GenerateKey(config.SSHConfig{KeyType: config.RSAKeyType, RsaKeySize: 512}) | ||
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 test is so short it doesn't matter, but usually i'd recommend splitting a test like/using subtests with 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. Big +1 to table-driven tests and subtests. |
||
assert.NilError(t, err) | ||
verifyKeys(t, keys) | ||
|
||
keys, err = GenerateKey(config.SSHConfig{KeyType: config.ECDSAKeyType}) | ||
assert.NilError(t, err) | ||
verifyKeys(t, keys) | ||
|
||
keys, err = GenerateKey(config.SSHConfig{KeyType: config.ED25519KeyType}) | ||
assert.NilError(t, err) | ||
verifyKeys(t, keys) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
as a follow up: can/should we default to ed25519?