package auth
import ( "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "math/big" "strings" "sync" "time"
"upspin.io/factotum"
"upspin.io/upspin"
)
// Vault SSO token format (agreed with the vault issuer):
//
// v1...
//
// where is base64url(JSON claims) and , are base64url of the
// big-endian bytes of the ECDSA signature over the ASCII string
// "v1." made with the issuer's upspin key. Splitting the token on "."
// yields exactly four fields. Keeping R and S as separate segments makes the
// encoding independent of the signing curve (P-256 vs P-384).
const vaultTokenPrefix = "v1."
// vaultClaims are the token's JSON payload.
type vaultClaims struct {
Iss string json:"iss" // issuer upspin user, e.g. "vault@cosmocr.at"
Sub string json:"sub" // authenticated upspin user
Aud string json:"aud" // intended audience, e.g. "gitweb"
Iat int64 json:"iat" // issued-at (unix seconds)
Exp int64 json:"exp" // expiry (unix seconds)
}
// LooksLikeVaultToken reports whether s has the vault token shape, so the // authenticator can route it to VaultVerifier instead of the PAT path. func LooksLikeVaultToken(s string) bool { return strings.HasPrefix(s, vaultTokenPrefix) }
// KeyLookup returns the upspin public key of a user (typically via the // keyserver). It is injected so tests can supply a fixed key. type KeyLookup func(upspin.UserName) (upspin.PublicKey, error)
// VaultVerifier validates vault-issued SSO bearer tokens. It trusts tokens // signed by a single pinned issuer for a single audience, verifying the // signature against the issuer's upspin public key. type VaultVerifier struct { issuer upspin.UserName audience string lookup KeyLookup now func() time.Time
mu sync.Mutex
key upspin.PublicKey // cached issuer key
}
// NewVaultVerifier returns a verifier that accepts tokens whose iss equals // issuer and aud equals audience, checking signatures with keys from lookup. func NewVaultVerifier(issuer upspin.UserName, audience string, lookup KeyLookup) *VaultVerifier { return &VaultVerifier{issuer: issuer, audience: audience, lookup: lookup, now: time.Now} }
// Verify checks a token and returns the authenticated subject (upspin user). func (v *VaultVerifier) Verify(token string) (string, error) { if !strings.HasPrefix(token, vaultTokenPrefix) { return "", fmt.Errorf("not a vault token") } parts := strings.Split(token, ".") if len(parts) != 4 || parts[0] != "v1" { return "", fmt.Errorf("malformed vault token") } payloadB64, rB64, sB64 := parts[1], parts[2], parts[3]
payload, err := base64.RawURLEncoding.DecodeString(payloadB64)
if err != nil {
return "", fmt.Errorf("bad token payload: %w", err)
}
var claims vaultClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return "", fmt.Errorf("bad token claims: %w", err)
}
if claims.Iss != string(v.issuer) {
return "", fmt.Errorf("token issuer %q not trusted", claims.Iss)
}
if claims.Aud != v.audience {
return "", fmt.Errorf("token audience %q not for this service", claims.Aud)
}
if claims.Sub == "" {
return "", fmt.Errorf("token has no subject")
}
if v.now().Unix() >= claims.Exp {
return "", fmt.Errorf("token expired")
}
sig, err := decodeSignature(rB64, sB64)
if err != nil {
return "", err
}
key, err := v.issuerKey()
if err != nil {
return "", err
}
hash := sha256.Sum256([]byte(vaultTokenPrefix + payloadB64))
if err := factotum.Verify(hash[:], sig, key); err != nil {
return "", fmt.Errorf("token signature invalid: %w", err)
}
return claims.Sub, nil
}
func (v *VaultVerifier) issuerKey() (upspin.PublicKey, error) { v.mu.Lock() key := v.key v.mu.Unlock() if key != "" { return key, nil } key, err := v.lookup(v.issuer) if err != nil { return "", fmt.Errorf("look up issuer key: %w", err) } v.mu.Lock() v.key = key v.mu.Unlock() return key, nil }
func decodeSignature(rB64, sB64 string) (upspin.Signature, error) { rb, err := base64.RawURLEncoding.DecodeString(rB64) if err != nil { return upspin.Signature{}, fmt.Errorf("bad signature R: %w", err) } sb, err := base64.RawURLEncoding.DecodeString(sB64) if err != nil { return upspin.Signature{}, fmt.Errorf("bad signature S: %w", err) } return upspin.Signature{ R: new(big.Int).SetBytes(rb), S: new(big.Int).SetBytes(sb), }, nil }