git@cosmocr.at/gitweb.git @ HEAD · summary refs log
HEAD / internal / auth / vault.go /
internal/auth/vault.go · 4.3 KiB · raw
view: SourceMarkdown ☑ force

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 }