Commit 7f5c45d0af
Verified · cmc
Layout: unified · split
cmd/gitbayd/system.go +7 −2
| @@ -3,6 +3,7 @@ package main | ||
| 3 | 3 | import ( |
| 4 | 4 | "fmt" |
| 5 | 5 | "os" |
| 6 | "time" | |
| 6 | 7 | |
| 7 | 8 | "github.com/spf13/cobra" |
| 8 | 9 | "golang.org/x/crypto/ssh" |
| @@ -43,8 +44,8 @@ func authorizedKeysCmd() *cobra.Command { | ||
| 43 | 44 | return nil // unparseable key: no output, auth fails |
| 44 | 45 | } |
| 45 | 46 | key, err := st.SSHKeyByFingerprint(ssh.FingerprintSHA256(pub)) |
| 46 | if err != nil { | |
| 47 | return nil // unknown key: no output, auth fails | |
| 47 | if err != nil || key.Expired(time.Now()) { | |
| 48 | return nil // unknown or expired key: no output, auth fails | |
| 48 | 49 | } |
| 49 | 50 | self, err := os.Executable() |
| 50 | 51 | if err != nil { |
| @@ -82,6 +83,10 @@ func shellCmd() *cobra.Command { | ||
| 82 | 83 | fmt.Fprintln(os.Stderr, "key no longer registered") |
| 83 | 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 | 90 | user, err := st.UserByID(key.UserID) |
| 86 | 91 | if err != nil { |
| 87 | 92 | fmt.Fprintln(os.Stderr, "account no longer exists") |
internal/sshd/revoke_test.go +57
| @@ -2,12 +2,16 @@ package sshd | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "bytes" |
| 5 | "crypto/ed25519" | |
| 6 | "crypto/rand" | |
| 5 | 7 | "errors" |
| 6 | 8 | "strings" |
| 7 | 9 | "testing" |
| 8 | 10 | "time" |
| 9 | 11 | |
| 10 | 12 | "golang.org/x/crypto/ssh" |
| 13 | ||
| 14 | "gitbay.org/gitbay/internal/store" | |
| 11 | 15 | ) |
| 12 | 16 | |
| 13 | 17 | // execStatus runs cmd on a new session and returns its exit status and |
| @@ -110,3 +114,56 @@ func TestSweepCutsOutOfProcessRevocation(t *testing.T) { | ||
| 110 | 114 | ts.srv.sweepOnce() |
| 111 | 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 | 162 | s.st.Audit(0, "auth.failed", map[string]any{"ip": ip, "fingerprint": fp}) |
| 163 | 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 | 169 | s.authLimiter.success(ip) |
| 166 | 170 | return &ssh.Permissions{Extensions: map[string]string{ |
| 167 | 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 | 411 | fmt.Fprintln(ch.Stderr(), "authentication temporarily unavailable") |
| 408 | 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 | 418 | user, err := s.st.UserByID(userID) |
| 411 | 419 | if err != nil { |
| 412 | 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 | 492 | Stderr: stderr, |
| 485 | 493 | Done: done, |
| 486 | 494 | Stopping: stopping, |
| 495 | Expires: key.ExpiresAt, | |
| 487 | 496 | } |
| 488 | 497 | return control.Dispatch(ctx, argv) |
| 489 | 498 | } |