Skip to content

Commit

Permalink
Add GitHub enterprise extension (#188)
Browse files Browse the repository at this point in the history
* Added Generic SSH addons, no tests

* tests and upping version

* minor spacing fix

* more space fixing
  • Loading branch information
cviecco authored Feb 10, 2023
1 parent 57f4eca commit 678d2a0
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 23 deletions.
28 changes: 26 additions & 2 deletions cmd/keymasterd/certgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
"os"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
Expand Down
43 changes: 42 additions & 1 deletion cmd/keymasterd/certgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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")
}
}
}
10 changes: 10 additions & 0 deletions cmd/keymasterd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion keymaster.spec
Original file line number Diff line number Diff line change
@@ -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

Expand Down
36 changes: 24 additions & 12 deletions lib/certgen/certgen.go
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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{
Expand All @@ -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 {
Expand All @@ -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" {
Expand Down Expand Up @@ -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:
Expand Down
40 changes: 33 additions & 7 deletions lib/certgen/certgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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-----`
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 678d2a0

Please sign in to comment.