133728e barerepo 1mo
1
package repo
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"os"
133728e barerepo 1mo
7
"path/filepath"
133728e barerepo 1mo
8
"strings"
133728e barerepo 1mo
9
"time"
133728e barerepo 1mo
12
// KeyLister is the half of the store this needs, so a caller does not drag the whole database in.
133728e barerepo 1mo
13
type KeyLister interface {
133728e barerepo 1mo
14
AllKeys(ctx context.Context) ([]AuthKey, error)
133728e barerepo 1mo
17
// AuthKey is one public key and the account it authorises.
133728e barerepo 1mo
18
type AuthKey struct {
133728e barerepo 1mo
19
Account string
133728e barerepo 1mo
20
Algo string
133728e barerepo 1mo
21
Blob string
133728e barerepo 1mo
22
// Retired is when the key stopped granting access, and the zero time means it still does.
133728e barerepo 1mo
23
Retired time.Time
133728e barerepo 1mo
26
// keyOptions refuse everything ssh can do except run the one command. Chapter 41.3.
133728e barerepo 1mo
27
const keyOptions = "no-port-forwarding,no-x11-forwarding,no-agent-forwarding,no-pty"
133728e barerepo 1mo
29
// WriteAuthorizedKeys rewrites the file from the database, because the database is what grants access.
133728e barerepo 1mo
30
func WriteAuthorizedKeys(dir, bin string, keys []AuthKey) error {
133728e barerepo 1mo
31
if dir == "" {
133728e barerepo 1mo
32
return fmt.Errorf("no ssh directory to write into")
133728e barerepo 1mo
34
if bin == "" {
133728e barerepo 1mo
35
bin = "barerepo"
133728e barerepo 1mo
37
var b strings.Builder
133728e barerepo 1mo
38
b.WriteString("# Written by barerepo from its own database. Edits here are lost on the next write.\n")
133728e barerepo 1mo
39
for _, k := range keys {
133728e barerepo 1mo
40
if k.Account == "" || k.Algo == "" || k.Blob == "" {
133728e barerepo 1mo
41
continue
133728e barerepo 1mo
43
// A quote in the account name would end the command, and a name cannot hold one anyway.
133728e barerepo 1mo
44
if strings.ContainsAny(k.Account, "\"\\\n\r") {
133728e barerepo 1mo
45
continue
133728e barerepo 1mo
47
fmt.Fprintf(&b, "command=\"%s ssh --account %s\",%s %s %s\n",
133728e barerepo 1mo
48
bin, k.Account, keyOptions, k.Algo, k.Blob)
133728e barerepo 1mo
50
if err := os.MkdirAll(dir, 0o700); err != nil {
133728e barerepo 1mo
51
return err
133728e barerepo 1mo
53
// Written beside the target and moved, so a reader never sees half a file and lose every key.
133728e barerepo 1mo
54
tmp := filepath.Join(dir, ".authorized_keys.tmp")
133728e barerepo 1mo
55
if err := os.WriteFile(tmp, []byte(b.String()), 0o600); err != nil {
133728e barerepo 1mo
56
return err
133728e barerepo 1mo
58
return os.Rename(tmp, filepath.Join(dir, "authorized_keys"))
133728e barerepo 1mo
61
// WriteAllowedSigners writes the keys git checks a commit signature against, in ssh-keygen's format.
133728e barerepo 1mo
62
func WriteAllowedSigners(path string, keys []AuthKey) error {
133728e barerepo 1mo
63
if path == "" {
133728e barerepo 1mo
64
return fmt.Errorf("no allowed signers file to write")
133728e barerepo 1mo
66
var b strings.Builder
133728e barerepo 1mo
67
b.WriteString("# Written by barerepo from its own database. Edits here are lost on the next write.\n")
133728e barerepo 1mo
68
seen := map[string]bool{}
133728e barerepo 1mo
69
for _, k := range keys {
133728e barerepo 1mo
70
if k.Algo == "" || k.Blob == "" || seen[k.Blob] {
133728e barerepo 1mo
71
continue
133728e barerepo 1mo
73
seen[k.Blob] = true
133728e barerepo 1mo
74
// The principal is a wildcard because a commit names an address barerepo never issued, and the namespace is what stops a sign in signature counting as a commit signature.
133728e barerepo 1mo
75
opts := "namespaces=\"git\""
133728e barerepo 1mo
76
if !k.Retired.IsZero() {
133728e barerepo 1mo
77
// One second past retirement in local time with no suffix, which is the only spelling ssh-keygen reads as a moment.
133728e barerepo 1mo
78
opts += ",valid-before=\"" + k.Retired.Local().Add(time.Second).Format("20060102150405") + "\""
133728e barerepo 1mo
80
fmt.Fprintf(&b, "* %s %s %s\n", opts, k.Algo, k.Blob)
133728e barerepo 1mo
82
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
133728e barerepo 1mo
83
return err
133728e barerepo 1mo
85
tmp := path + ".tmp"
133728e barerepo 1mo
86
if err := os.WriteFile(tmp, []byte(b.String()), 0o600); err != nil {
133728e barerepo 1mo
87
return err
133728e barerepo 1mo
89
return os.Rename(tmp, path)