package auth import ( "context" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/barerepo/server/internal/store" ) // makeKey generates a real pair and returns the private path and the stored PubKey. func makeKey(t *testing.T, comment string) (string, store.PubKey) { t.Helper() if _, err := exec.LookPath("ssh-keygen"); err != nil { t.Skip("ssh-keygen is not installed") } dir := t.TempDir() priv := filepath.Join(dir, "id") cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-N", "", "-C", comment, "-f", priv, "-q") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("ssh-keygen: %v\n%s", err, out) } raw, err := os.ReadFile(priv + ".pub") if err != nil { t.Fatal(err) } algo, blob, cmt, fp, err := store.ParsePubKey(string(raw)) if err != nil { t.Fatal(err) } return priv, store.PubKey{Algo: algo, Blob: blob, Comment: cmt, Fingerprint: fp} } // sign does what chapter 31.3 tells the user to do, by hand. func sign(t *testing.T, priv, nonce, namespace string) string { t.Helper() dir := t.TempDir() msg := filepath.Join(dir, "nonce") if err := os.WriteFile(msg, []byte(nonce), 0o600); err != nil { t.Fatal(err) } cmd := exec.Command("ssh-keygen", "-Y", "sign", "-f", priv, "-n", namespace, msg) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("sign: %v\n%s", err, out) } sig, err := os.ReadFile(msg + ".sig") if err != nil { t.Fatal(err) } return string(sig) } func TestVerify(t *testing.T) { ctx := context.Background() privA, keyA := makeKey(t, "laptop") privB, keyB := makeKey(t, "uproar") _, keyC := makeKey(t, "somebody else") nonce, err := NewNonce() if err != nil { t.Fatal(err) } // The key that signed is on the account. if k, err := Verify(ctx, "john", nonce, sign(t, privA, nonce, Namespace), []store.PubKey{keyA}); err != nil || k == nil { t.Errorf("a good signature was rejected: %v", err) } // Chapter 10 step 5: every key is tried, not just the first. k, err := Verify(ctx, "john", nonce, sign(t, privB, nonce, Namespace), []store.PubKey{keyA, keyB}) if err != nil { t.Errorf("the second key on the account was not tried: %v", err) } else if k.Fingerprint != keyB.Fingerprint { // Chapter 32.5 times each key, so the right key has to come back, not just a yes. t.Errorf("Verify named the wrong key: got %s, want %s", k.Fingerprint, keyB.Fingerprint) } // A key that is not on the account. if _, err := Verify(ctx, "john", nonce, sign(t, privA, nonce, Namespace), []store.PubKey{keyC}); err == nil { t.Error("a signature from an unknown key was accepted") } // A signature over a different nonce. This is the replay case. other, _ := NewNonce() if _, err := Verify(ctx, "john", nonce, sign(t, privA, other, Namespace), []store.PubKey{keyA}); err == nil { t.Error("a signature over a different nonce was accepted") } // A signature made for another service. This is what the namespace is for. if _, err := Verify(ctx, "john", nonce, sign(t, privA, nonce, "git"), []store.PubKey{keyA}); err == nil { t.Error("a signature from another namespace was accepted") } // Nothing that is not a signature gets as far as ssh-keygen. for _, junk := range []string{"", "hello", "-----BEGIN RSA PRIVATE KEY-----"} { if _, err := Verify(ctx, "john", nonce, junk, []store.PubKey{keyA}); err == nil { t.Errorf("Verify accepted %q", junk) } } // An account with no keys can never sign in. if _, err := Verify(ctx, "john", nonce, sign(t, privA, nonce, Namespace), nil); err == nil { t.Error("an account with no keys was signed in") } } func TestNonceIsUnique(t *testing.T) { seen := map[string]bool{} for i := 0; i < 100; i++ { n, err := NewNonce() if err != nil { t.Fatal(err) } if len(n) < 40 || seen[n] { t.Fatalf("nonce %q is short or repeated", n) } seen[n] = true } if strings.Contains(strings.Join(keysOf(seen), ""), " ") { t.Error("a nonce contains a space, which breaks the echo -n in chapter 31.3") } } func keysOf(m map[string]bool) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } return out } // Signup proves the private half, in its own namespace, so no sign-in can be replayed. func TestVerifySignup(t *testing.T) { ctx := context.Background() priv, key := makeKey(t, "laptop") pub := key.Algo + " " + key.Blob + " laptop" nonce, _ := NewNonce() if err := VerifySignup(ctx, "rock", nonce, sign(t, priv, nonce, SignupNamespace), pub); err != nil { t.Errorf("a good signup signature was rejected: %v", err) } // The sign-in namespace must not work here. if err := VerifySignup(ctx, "rock", nonce, sign(t, priv, nonce, Namespace), pub); err == nil { t.Error("a barerepo-auth signature was accepted at signup") } // Nor should a signature from a different key than the one being claimed. _, other := makeKey(t, "other") otherPub := other.Algo + " " + other.Blob + " other" if err := VerifySignup(ctx, "rock", nonce, sign(t, priv, nonce, SignupNamespace), otherPub); err == nil { t.Error("a signature from a different key was accepted") } if err := VerifySignup(ctx, "rock", nonce, "not a signature", pub); err == nil { t.Error("junk was accepted as a signature") } }