diff --git a/cmd/keymasterd/certgen.go b/cmd/keymasterd/certgen.go index 60fd4c4c..1ca16c7f 100644 --- a/cmd/keymasterd/certgen.go +++ b/cmd/keymasterd/certgen.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "os" "regexp" "strings" "time" @@ -206,6 +207,24 @@ func getValidSSHPublicKey(userPubKey string) (ssh.PublicKey, error, error) { return userSSH, nil, nil } +func (state *RuntimeState) expandSSHExtensions(username string) (map[string]string, error) { + mapper := func(placeholderName string) string { + switch placeholderName { + case "USERNAME": + return username + } + return "" + } + userExtensions := make(map[string]string) + for _, extension := range state.Config.Base.SSHCertConfig.Extensions { + key := os.Expand(extension.Key, mapper) + value := os.Expand(extension.Value, mapper) + userExtensions[key] = value + } + + return userExtensions, nil +} + func (state *RuntimeState) postAuthSSHCertHandler( w http.ResponseWriter, r *http.Request, targetUser string, duration time.Duration) { @@ -257,8 +276,13 @@ func (state *RuntimeState) postAuthSSHCertHandler( logger.Printf("Signer failed to load") return } - - certString, cert, err = certgen.GenSSHCertFileString(targetUser, userPubKey, signer, state.HostIdentity, duration) + extensions, err := state.expandSSHExtensions(targetUser) + if err != nil { + state.writeFailureResponse(w, r, http.StatusInternalServerError, "") + logger.Printf("Extensions Failed to expand") + return + } + certString, cert, err = certgen.GenSSHCertFileString(targetUser, userPubKey, signer, state.HostIdentity, duration, extensions) if err != nil { state.writeFailureResponse(w, r, http.StatusInternalServerError, "") logger.Printf("signUserPubkey Err") diff --git a/cmd/keymasterd/certgen_test.go b/cmd/keymasterd/certgen_test.go index e8bd4b87..9bfdd8a5 100644 --- a/cmd/keymasterd/certgen_test.go +++ b/cmd/keymasterd/certgen_test.go @@ -52,7 +52,7 @@ const invalidSSHFileBadKeyData = `ssh-rsa AAAAB3NzaC1kc3dddMAAACBALd5BLQoXxeJHHM const testDuration = time.Duration(120 * time.Second) -/// X509section (this is from certgen TODO: make public) +// X509section (this is from certgen TODO: make public) func getPubKeyFromPem(pubkey string) (pub interface{}, err error) { block, rest := pem.Decode([]byte(pubkey)) if block == nil || block.Type != "PUBLIC KEY" { @@ -226,3 +226,44 @@ func TestGenSSHEd25519(t *testing.T) { } } + +func TestExpandSSHExtensions(t *testing.T) { + state, passwdFile, err := setupValidRuntimeStateSigner(t) + if err != nil { + t.Fatal(err) + } + defer os.Remove(passwdFile.Name()) // clean up + state.Config.Base.SSHCertConfig.Extensions = []sshExtension{ + sshExtension{ + Key: "user:username", + Value: "$USERNAME", + }, + sshExtension{ + Key: "key:$USERNAME", + Value: "value:userkey", + }, + } + extensions, err := state.expandSSHExtensions("username") + if err != nil { + t.Fatal(err) + } + if extensions == nil { + t.Fatal("nil extension") + } + compareMap := map[string]string{ + "user:username": "username", + "key:username": "value:userkey", + } + if len(state.Config.Base.SSHCertConfig.Extensions) != len(extensions) { + t.Fatal("incomplete expansion") + } + for key, value := range extensions { + cValue, ok := compareMap[key] + if !ok { + t.Fatal("key not found") + } + if value != cValue { + t.Fatal("value does not match") + } + } +} diff --git a/cmd/keymasterd/config.go b/cmd/keymasterd/config.go index ba340aa7..6252ca3b 100644 --- a/cmd/keymasterd/config.go +++ b/cmd/keymasterd/config.go @@ -54,6 +54,15 @@ type autoUnseal struct { AwsSecretKey string `yaml:"aws_secret_key"` } +type sshExtension struct { + Key string `yaml:"key"` + Value string `yaml:"value"` +} + +type sshCertConfig struct { + Extensions []sshExtension `yaml:"extensions"` +} + type baseConfig struct { HttpAddress string `yaml:"http_address"` AdminAddress string `yaml:"admin_address"` @@ -87,6 +96,7 @@ type baseConfig struct { WebauthTokenForCliLifetime time.Duration `yaml:"webauth_token_for_cli_lifetime"` PasswordAttemptGlobalBurstLimit uint `yaml:"password_attempt_global_burst_limit"` PasswordAttemptGlobalRateLimit rate.Limit `yaml:"password_attempt_global_rate_limit"` + SSHCertConfig sshCertConfig `yaml:"ssh_cert_config"` } type awsCertsConfig struct { diff --git a/keymaster.spec b/keymaster.spec index 3f308be9..1d9dc829 100644 --- a/keymaster.spec +++ b/keymaster.spec @@ -1,5 +1,5 @@ Name: keymaster -Version: 1.11.2 +Version: 1.12.0 Release: 1%{?dist} Summary: Short term access certificate generator and client diff --git a/lib/certgen/certgen.go b/lib/certgen/certgen.go index 599b2121..8c910df7 100644 --- a/lib/certgen/certgen.go +++ b/lib/certgen/certgen.go @@ -1,5 +1,5 @@ /* - Package certgen id set of utilities used to generate ssh certificates +Package certgen contains a set of utilities used to generate ssh certificates. */ package certgen @@ -45,7 +45,7 @@ func goCertToFileString(c ssh.Certificate, username string) (string, error) { } // gen_user_cert a username and key, returns a short lived cert for that user -func GenSSHCertFileString(username string, userPubKey string, signer ssh.Signer, host_identity string, duration time.Duration) (certString string, cert ssh.Certificate, err error) { +func GenSSHCertFileString(username string, userPubKey string, signer ssh.Signer, host_identity string, duration time.Duration, customExtensions map[string]string) (certString string, cert ssh.Certificate, err error) { userKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(userPubKey)) if err != nil { return "", cert, err @@ -60,7 +60,23 @@ func GenSSHCertFileString(username string, userPubKey string, signer ssh.Signer, return "", cert, err } serial := (currentEpoch << 32) | nBig.Uint64() - + // Here we add standard extensions + extensions := map[string]string{ + "permit-X11-forwarding": "", + "permit-agent-forwarding": "", + "permit-port-forwarding": "", + "permit-pty": "", + "permit-user-rc": "", + } + if customExtensions != nil { + for key, value := range customExtensions { + //safeguard for invalid definition + if key == "" { + continue + } + extensions[key] = value + } + } // The values of the permissions are taken from the default values used // by ssh-keygen cert = ssh.Certificate{ @@ -72,12 +88,8 @@ func GenSSHCertFileString(username string, userPubKey string, signer ssh.Signer, ValidAfter: currentEpoch, ValidBefore: expireEpoch, Serial: serial, - Permissions: ssh.Permissions{Extensions: map[string]string{ - "permit-X11-forwarding": "", - "permit-agent-forwarding": "", - "permit-port-forwarding": "", - "permit-pty": "", - "permit-user-rc": ""}}} + Permissions: ssh.Permissions{Extensions: extensions}, + } err = cert.SignCert(bytes.NewReader(cert.Marshal()), signer) if err != nil { @@ -96,10 +108,10 @@ func GenSSHCertFileStringFromSSSDPublicKey(userName string, signer ssh.Signer, h if err != nil { return "", cert, err } - return GenSSHCertFileString(userName, userPubKey, signer, hostIdentity, duration) + return GenSSHCertFileString(userName, userPubKey, signer, hostIdentity, duration, nil) } -/// X509 section +// X509 section func getPubKeyFromPem(pubkey string) (pub interface{}, err error) { block, rest := pem.Decode([]byte(pubkey)) if block == nil || block.Type != "PUBLIC KEY" { @@ -159,7 +171,7 @@ func GetSignerFromPEMBytes(privateKey []byte) (crypto.Signer, error) { } } -//copied from https://golang.org/src/crypto/tls/generate_cert.go +// copied from https://golang.org/src/crypto/tls/generate_cert.go func publicKey(priv interface{}) interface{} { switch k := priv.(type) { case *rsa.PrivateKey: diff --git a/lib/certgen/certgen_test.go b/lib/certgen/certgen_test.go index 9a25c09c..f37d0b56 100644 --- a/lib/certgen/certgen_test.go +++ b/lib/certgen/certgen_test.go @@ -146,7 +146,7 @@ DhV+rrj+h1k9EaIv+VSQ98XGm97NK3PEkolWk5UngF3Qwt5qPDeGjpf4zyhej0lF KwIBAw== -----END PUBLIC KEY-----` -//now other valid sshKeys : ssh-keygen -t ecdsa +// now other valid sshKeys : ssh-keygen -t ecdsa const ecdsaPublicSSH = `ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBD+IdwZ/LsQhxE3soSMoCNOtqftjUgMoy7nqAukSL9MuULIbspoWRvF/bxDaaJf9dcz+mK/ILC5NXxNs36oYNOs= cviecco@cviecco--MacBookPro15` const ed25519PublicSSH = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDdNbfR67CJ0/iB5a5lQfZowi3VTrkDu7/rpMNKfHFPs cviecco@cviecco--MacBookPro15` @@ -200,7 +200,7 @@ RBm1g0vfLOjV1tPs5/0QMy7ANExMLGtzIJidWWWzIzw2rx4WC7xcIkJ+iWFIIFNy S9RSPfwJS7+Zr8LP4H6APpstQWZEXOo= -----END EC PRIVATE KEY-----` -//openssl genpkey -algorithm ED25519 -out key.pem +// openssl genpkey -algorithm ED25519 -out key.pem const pkcs8Ed25519PrivateKey = `-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIHoHbl2RwHwmyWtXVLroUZEI+d/SqL3RKmECM5P7o7D5 -----END PRIVATE KEY-----` @@ -251,7 +251,7 @@ func TestGenSSHCertFileStringGenerateSuccess(t *testing.T) { if err != nil { t.Fatal(err) } - certString, cert, err := GenSSHCertFileString(username, testUserPublicKey, goodSigner, hostIdentity, testDuration) + certString, cert, err := GenSSHCertFileString(username, testUserPublicKey, goodSigner, hostIdentity, testDuration, nil) if err != nil { t.Fatal(err) } @@ -267,7 +267,7 @@ func TestGenSSHCertFileStringGenerateSuccess(t *testing.T) { if err != nil { t.Fatal(err) } - certString, cert, err = GenSSHCertFileString(username, ed25519PublicSSH, goodEd25519Signer, hostIdentity, testDuration) + certString, cert, err = GenSSHCertFileString(username, ed25519PublicSSH, goodEd25519Signer, hostIdentity, testDuration, nil) if err != nil { t.Fatal(err) } @@ -278,6 +278,32 @@ func TestGenSSHCertFileStringGenerateSuccess(t *testing.T) { if len(cert.ValidPrincipals) != 1 || cert.ValidPrincipals[0] != username { t.Fatal("invalid cert content, bad username") } + // test with non nil custom extensions: + extensionTest1 := map[string]string{"hello": "world"} + _, cert, err = GenSSHCertFileString(username, ed25519PublicSSH, goodEd25519Signer, hostIdentity, testDuration, extensionTest1) + if err != nil { + t.Fatal(err) + } + found := false + for key, value := range cert.Permissions.Extensions { + if key == "hello" { + found = true + if value != "world" { + t.Fatal("extension value is invalid") + } + break + } + } + if !found { + t.Fatal("custom extension not found") + } + // invalid extension blank name.. should NOT fail + invalidExtensionTest := map[string]string{"": "world"} + _, _, err = GenSSHCertFileString(username, ed25519PublicSSH, goodEd25519Signer, hostIdentity, testDuration, invalidExtensionTest) + if err != nil { + t.Fatal(err) + } + } func TestGenSSHCertFileStringGenerateFailBadPublicKey(t *testing.T) { @@ -287,7 +313,7 @@ func TestGenSSHCertFileStringGenerateFailBadPublicKey(t *testing.T) { if err != nil { t.Fatal(err) } - _, _, err = GenSSHCertFileString(username, "ThisIsNOTAPublicKey", goodSigner, hostIdentity, testDuration) + _, _, err = GenSSHCertFileString(username, "ThisIsNOTAPublicKey", goodSigner, hostIdentity, testDuration, nil) if err == nil { t.Fatal(err) } @@ -451,7 +477,7 @@ func derBytesCertToCertAndPem(derBytes []byte) (*x509.Certificate, string, error return cert, pemCert, nil } -//GenUserX509Cert(userName string, userPubkey string, caCertString string, caPrivateKeyString string) +// GenUserX509Cert(userName string, userPubkey string, caCertString string, caPrivateKeyString string) func TestGenUserX509CertGoodNoRealm(t *testing.T) { userPub, caCert, caPriv := setupX509Generator(t) @@ -509,7 +535,7 @@ func TestGenx509CertGoodWithRealm(t *testing.T) { // 6. kerberos realm info! } -//GenSelfSignedCACert +// GenSelfSignedCACert func TestGenSelfSignedCACertGood(t *testing.T) { caPriv, err := GetSignerFromPEMBytes([]byte(testSignerPrivateKey)) if err != nil {