// Package auth runs the same `ssh-keygen -Y verify` the user can run, hiding nothing. package auth import ( "context" "crypto/rand" "encoding/base64" "errors" "os" "os/exec" "path/filepath" "strings" "github.com/barerepo/server/internal/gitx" "github.com/barerepo/server/internal/store" ) // Namespace keeps a signature made here from working elsewhere, and the reverse. 31.3. const Namespace = "barerepo-auth" // SignupNamespace is separate, so a captured sign-in cannot claim an account. const SignupNamespace = "barerepo-signup" // NewNonce is 32 random bytes, encoded so a person can paste it. Chapter 10 step 2. func NewNonce() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(buf), nil } var errBadSignature = errors.New("that signature does not match any key on this account") // Verify tries every key one at a time, because chapter 32.5 needs to know which one answered. func Verify(ctx context.Context, account, nonce, signature string, keys []store.PubKey) (*store.PubKey, error) { return verify(ctx, Namespace, account, nonce, signature, keys) } // VerifySignup stops one person claiming an account with another's published public key. func VerifySignup(ctx context.Context, name, nonce, signature, pubkey string) error { algo, blob, comment, fp, err := store.ParsePubKey(pubkey) if err != nil { return err } key := store.PubKey{Algo: algo, Blob: blob, Comment: comment, Fingerprint: fp} if _, err := verify(ctx, SignupNamespace, name, nonce, signature, []store.PubKey{key}); err != nil { if errors.Is(err, errBadSignature) { return errors.New("that signature was not made by the key you pasted, with -n barerepo-signup") } return err } return nil } func verify(ctx context.Context, namespace, account, nonce, signature string, keys []store.PubKey) (*store.PubKey, error) { if len(keys) == 0 || !gitx.ValidName(account) { return nil, errBadSignature } if !strings.Contains(signature, "BEGIN SSH SIGNATURE") { return nil, errors.New("that is not an ssh signature. it starts with -----BEGIN SSH SIGNATURE-----") } dir, err := os.MkdirTemp("", "barerepo-auth-") if err != nil { return nil, err } defer os.RemoveAll(dir) sigPath := filepath.Join(dir, "nonce.sig") if err := os.WriteFile(sigPath, []byte(signature), 0o600); err != nil { return nil, err } signersPath := filepath.Join(dir, "allowed_signers") for i := range keys { k := keys[i] line := account + " " + k.Algo + " " + k.Blob + "\n" if err := os.WriteFile(signersPath, []byte(line), 0o600); err != nil { return nil, err } cmd := exec.CommandContext(ctx, "ssh-keygen", "-Y", "verify", "-f", signersPath, "-I", account, "-n", namespace, "-s", sigPath) cmd.Stdin = strings.NewReader(nonce) cmd.Env = []string{"PATH=/usr/local/bin:/usr/bin:/bin", "HOME=" + dir, "LC_ALL=C"} if out, err := cmd.CombinedOutput(); err == nil { return &k, nil } else { _ = out // ssh-keygen's wording is not ours to show } } return nil, errBadSignature }