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, ".") }