Commit 7f5c45d0af
Verified · cmc
Layout: unified · split
cmd/gitbayd/system.go +7 −2
| @@ -3,6 +3,7 @@ package main | |||
| 3 | import ( | 3 | import ( |
| 4 | "fmt" | 4 | "fmt" |
| 5 | "os" | 5 | "os" |
| 6 | "time" | ||
| 6 | 7 | ||
| 7 | "github.com/spf13/cobra" | 8 | "github.com/spf13/cobra" |
| 8 | "golang.org/x/crypto/ssh" | 9 | "golang.org/x/crypto/ssh" |
| @@ -43,8 +44,8 @@ func authorizedKeysCmd() *cobra.Command { | |||
| 43 | return nil // unparseable key: no output, auth fails | 44 | return nil // unparseable key: no output, auth fails |
| 44 | } | 45 | } |
| 45 | key, err := st.SSHKeyByFingerprint(ssh.FingerprintSHA256(pub)) | 46 | key, err := st.SSHKeyByFingerprint(ssh.FingerprintSHA256(pub)) |
| 46 | if err != nil { | 47 | if err != nil || key.Expired(time.Now()) { |
| 47 | return nil // unknown key: no output, auth fails | 48 | return nil // unknown or expired key: no output, auth fails |
| 48 | } | 49 | } |
| 49 | self, err := os.Executable() | 50 | self, err := os.Executable() |
| 50 | if err != nil { | 51 | if err != nil { |
| @@ -82,6 +83,10 @@ func shellCmd() *cobra.Command { | |||
| 82 | fmt.Fprintln(os.Stderr, "key no longer registered") | 83 | fmt.Fprintln(os.Stderr, "key no longer registered") |
| 83 | os.Exit(protocol.ExitDenied) | 84 | os.Exit(protocol.ExitDenied) |
| 84 | } | 85 | } |
| 86 | if key.Expired(time.Now()) { | ||
| 87 | fmt.Fprintln(os.Stderr, "this key has expired; remove it and add a new one") | ||
| 88 | os.Exit(protocol.ExitDenied) | ||
| 89 | } | ||
| 85 | user, err := st.UserByID(key.UserID) | 90 | user, err := st.UserByID(key.UserID) |
| 86 | if err != nil { | 91 | if err != nil { |
| 87 | fmt.Fprintln(os.Stderr, "account no longer exists") | 92 | fmt.Fprintln(os.Stderr, "account no longer exists") |
internal/sshd/revoke_test.go +57
| @@ -2,12 +2,16 @@ package sshd | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "bytes" | 4 | "bytes" |
| 5 | "crypto/ed25519" | ||
| 6 | "crypto/rand" | ||
| 5 | "errors" | 7 | "errors" |
| 6 | "strings" | 8 | "strings" |
| 7 | "testing" | 9 | "testing" |
| 8 | "time" | 10 | "time" |
| 9 | 11 | ||
| 10 | "golang.org/x/crypto/ssh" | 12 | "golang.org/x/crypto/ssh" |
| 13 | |||
| 14 | "gitbay.org/gitbay/internal/store" | ||
| 11 | ) | 15 | ) |
| 12 | 16 | ||
| 13 | // execStatus runs cmd on a new session and returns its exit status and | 17 | // execStatus runs cmd on a new session and returns its exit status and |
| @@ -110,3 +114,56 @@ func TestSweepCutsOutOfProcessRevocation(t *testing.T) { | |||
| 110 | ts.srv.sweepOnce() | 114 | ts.srv.sweepOnce() |
| 111 | waitClosed(t, ts.client) | 115 | waitClosed(t, ts.client) |
| 112 | } | 116 | } |
| 117 | |||
| 118 | // A key that expires while connected: the next exec is refused, and | ||
| 119 | // the sweep closes the connection. | ||
| 120 | func TestExpiredKeyRefusedAndCut(t *testing.T) { | ||
| 121 | ts := newTestServer(t) | ||
| 122 | past := time.Now().Add(-time.Second).UTC().Format("2006-01-02T15:04:05.000Z") | ||
| 123 | if _, err := ts.st.DB.Exec("UPDATE ssh_keys SET expires_at = ? WHERE id = ?", past, ts.keyID); err != nil { | ||
| 124 | t.Fatal(err) | ||
| 125 | } | ||
| 126 | if code, errOut := execStatus(ts.client, "whoami"); code != 4 || !strings.Contains(errOut, "expired") { | ||
| 127 | t.Fatalf("whoami with an expired key: %d %q", code, errOut) | ||
| 128 | } | ||
| 129 | ts.srv.sweepOnce() | ||
| 130 | waitClosed(t, ts.client) | ||
| 131 | } | ||
| 132 | |||
| 133 | // An expiring key may not mint. | ||
| 134 | func TestExpiringKeyCannotMint(t *testing.T) { | ||
| 135 | ts := newTestServer(t) | ||
| 136 | future := time.Now().Add(time.Hour).UTC().Format("2006-01-02T15:04:05.000Z") | ||
| 137 | if _, err := ts.st.DB.Exec("UPDATE ssh_keys SET expires_at = ? WHERE id = ?", future, ts.keyID); err != nil { | ||
| 138 | t.Fatal(err) | ||
| 139 | } | ||
| 140 | if code, errOut := execStatus(ts.client, "token create --name x"); code != 4 || !strings.Contains(errOut, "expires") { | ||
| 141 | t.Fatalf("token create with an expiring key: %d %q", code, errOut) | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | func TestExpiredKeyRefusedAtAuth(t *testing.T) { | ||
| 146 | ts := newTestServer(t) | ||
| 147 | _, priv, err := ed25519.GenerateKey(rand.Reader) | ||
| 148 | if err != nil { | ||
| 149 | t.Fatal(err) | ||
| 150 | } | ||
| 151 | signer, err := ssh.NewSignerFromKey(priv) | ||
| 152 | if err != nil { | ||
| 153 | t.Fatal(err) | ||
| 154 | } | ||
| 155 | pub := signer.PublicKey() | ||
| 156 | past := time.Now().Add(-time.Minute) | ||
| 157 | if err := ts.st.AddSSHKeyFrom(ts.uid, ssh.FingerprintSHA256(pub), pub.Type(), pub.Marshal(), "full", "", store.KeyOrigin{ExpiresAt: &past}); err != nil { | ||
| 158 | t.Fatal(err) | ||
| 159 | } | ||
| 160 | _, err = ssh.Dial("tcp", ts.client.RemoteAddr().String(), &ssh.ClientConfig{ | ||
| 161 | User: "git", | ||
| 162 | Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, | ||
| 163 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), | ||
| 164 | Timeout: 5 * time.Second, | ||
| 165 | }) | ||
| 166 | if err == nil { | ||
| 167 | t.Fatal("an expired key authenticated") | ||
| 168 | } | ||
| 169 | } | ||
internal/sshd/sshd.go +9
| @@ -162,6 +162,10 @@ func (s *Server) authenticate(meta ssh.ConnMetadata, pub ssh.PublicKey) (*ssh.Pe | |||
| 162 | s.st.Audit(0, "auth.failed", map[string]any{"ip": ip, "fingerprint": fp}) | 162 | s.st.Audit(0, "auth.failed", map[string]any{"ip": ip, "fingerprint": fp}) |
| 163 | return nil, fmt.Errorf("unknown key %s", fp) | 163 | return nil, fmt.Errorf("unknown key %s", fp) |
| 164 | } | 164 | } |
| 165 | if key.Expired(time.Now()) { | ||
| 166 | s.st.Audit(key.UserID, "auth.expired", map[string]any{"ip": ip, "fingerprint": fp}) | ||
| 167 | return nil, fmt.Errorf("key %s has expired", fp) | ||
| 168 | } | ||
| 165 | s.authLimiter.success(ip) | 169 | s.authLimiter.success(ip) |
| 166 | return &ssh.Permissions{Extensions: map[string]string{ | 170 | return &ssh.Permissions{Extensions: map[string]string{ |
| 167 | "user-id": strconv.FormatInt(key.UserID, 10), | 171 | "user-id": strconv.FormatInt(key.UserID, 10), |
| @@ -407,6 +411,10 @@ func (s *Server) runExec(c *conn, sconn *ssh.ServerConn, ch ssh.Channel, term co | |||
| 407 | fmt.Fprintln(ch.Stderr(), "authentication temporarily unavailable") | 411 | fmt.Fprintln(ch.Stderr(), "authentication temporarily unavailable") |
| 408 | return protocol.ExitFailure | 412 | return protocol.ExitFailure |
| 409 | } | 413 | } |
| 414 | if key.Expired(time.Now()) { | ||
| 415 | fmt.Fprintln(ch.Stderr(), "this key has expired; remove it and add a new one") | ||
| 416 | return protocol.ExitDenied | ||
| 417 | } | ||
| 410 | user, err := s.st.UserByID(userID) | 418 | user, err := s.st.UserByID(userID) |
| 411 | if err != nil { | 419 | if err != nil { |
| 412 | fmt.Fprintln(ch.Stderr(), "account no longer exists") | 420 | fmt.Fprintln(ch.Stderr(), "account no longer exists") |
| @@ -484,6 +492,7 @@ func Exec(cfg config.Config, st *store.Store, user store.User, key store.SSHKey, | |||
| 484 | Stderr: stderr, | 492 | Stderr: stderr, |
| 485 | Done: done, | 493 | Done: done, |
| 486 | Stopping: stopping, | 494 | Stopping: stopping, |
| 495 | Expires: key.ExpiresAt, | ||
| 487 | } | 496 | } |
| 488 | return control.Dispatch(ctx, argv) | 497 | return control.Dispatch(ctx, argv) |
| 489 | } | 498 | } |