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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
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.<payload>.<r>.<s>
//
// where <payload> is base64url(JSON claims) and <r>,<s> are base64url of the
// big-endian bytes of the ECDSA signature over the ASCII string
// "v1.<payload>" 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
}
|