git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / internal / auth / vault_test.go /
internal/auth/vault_test.go · 3.6 KiB · raw
view: Source ☐ force
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package auth

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	"upspin.io/upspin"
)

// testIssuer generates an ephemeral P-256 key and returns the upspin public-key
// string plus a mintToken function that signs tokens exactly as the vault issuer
// will (this is the executable spec for vault's mintToken).
func testIssuer(t *testing.T) (upspin.PublicKey, func(claims vaultClaims) string) {
	t.Helper()
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	pub := upspin.PublicKey(fmt.Sprintf("p256\n%s\n%s\n", priv.X.String(), priv.Y.String()))

	mint := func(claims vaultClaims) string {
		payload, err := json.Marshal(claims)
		if err != nil {
			t.Fatal(err)
		}
		payloadB64 := base64.RawURLEncoding.EncodeToString(payload)
		hash := sha256.Sum256([]byte("v1." + payloadB64))
		r, s, err := ecdsa.Sign(rand.Reader, priv, hash[:])
		if err != nil {
			t.Fatal(err)
		}
		return "v1." + payloadB64 + "." +
			base64.RawURLEncoding.EncodeToString(r.Bytes()) + "." +
			base64.RawURLEncoding.EncodeToString(s.Bytes())
	}
	return pub, mint
}

func TestVaultVerify(t *testing.T) {
	pub, mint := testIssuer(t)
	const issuer = "vault@cosmocr.at"
	lookup := func(u upspin.UserName) (upspin.PublicKey, error) {
		if u != issuer {
			return "", fmt.Errorf("unexpected lookup for %q", u)
		}
		return pub, nil
	}
	v := NewVaultVerifier(issuer, "gitweb", lookup)

	valid := vaultClaims{Iss: issuer, Sub: "alice@d.com", Aud: "gitweb",
		Iat: time.Now().Unix(), Exp: time.Now().Add(time.Hour).Unix()}

	sub, err := v.Verify(mint(valid))
	if err != nil {
		t.Fatalf("Verify valid token: %v", err)
	}
	if sub != "alice@d.com" {
		t.Fatalf("sub = %q, want alice@d.com", sub)
	}
}

func TestVaultVerifyRejects(t *testing.T) {
	pub, mint := testIssuer(t)
	const issuer = "vault@cosmocr.at"
	v := NewVaultVerifier(issuer, "gitweb", func(upspin.UserName) (upspin.PublicKey, error) {
		return pub, nil
	})

	now := time.Now()
	base := vaultClaims{Iss: issuer, Sub: "alice@d.com", Aud: "gitweb",
		Iat: now.Unix(), Exp: now.Add(time.Hour).Unix()}

	// Expired.
	exp := base
	exp.Exp = now.Add(-time.Minute).Unix()
	if _, err := v.Verify(mint(exp)); err == nil {
		t.Error("expired token accepted")
	}

	// Wrong audience.
	aud := base
	aud.Aud = "someone-else"
	if _, err := v.Verify(mint(aud)); err == nil {
		t.Error("wrong-audience token accepted")
	}

	// Wrong issuer.
	iss := base
	iss.Iss = "attacker@evil.com"
	if _, err := v.Verify(mint(iss)); err == nil {
		t.Error("wrong-issuer token accepted")
	}

	// Tampered signature (flip the payload but keep the old signature).
	good := mint(base)
	tampered := tamper(t, good)
	if _, err := v.Verify(tampered); err == nil {
		t.Error("tampered token accepted")
	}

	// Signed by a different key.
	otherPub, otherMint := testIssuer(t)
	_ = otherPub
	if _, err := v.Verify(otherMint(base)); err == nil {
		t.Error("token signed by wrong key accepted")
	}
}

// tamper swaps in a forged payload (a different subject) while keeping the
// original signature segments, so the signature no longer matches.
func tamper(t *testing.T, token string) string {
	t.Helper()
	parts := strings.Split(token, ".") // v1 . payload . r . s
	if len(parts) != 4 {
		t.Fatalf("unexpected token shape: %q", token)
	}
	forged := vaultClaims{Iss: "vault@cosmocr.at", Sub: "attacker@d.com", Aud: "gitweb",
		Iat: time.Now().Unix(), Exp: time.Now().Add(time.Hour).Unix()}
	payload, _ := json.Marshal(forged)
	parts[1] = base64.RawURLEncoding.EncodeToString(payload)
	return strings.Join(parts, ".")
}