Summary
In MCP mode, initAuth() always generates a fresh ephemeral ECDSA P-256 key for JWT signing — even when initSigning() has just successfully connected to SPIRE and obtained an SVID. The JWT signing identity and the attestation signing identity are two independent ephemeral objects, which contradicts the paper §3.2 model of a single server identity backed by SPIRE.
Evidence
internal/mcp/server.go:392-400:
func (s *Server) initAuth() error {
// For now, always use ephemeral key. When SPIRE integration provides a
// crypto.Signer, use auth.NewTokenIssuerFromSigner instead.
issuer, err := auth.NewTokenIssuer()
if err != nil {
return fmt.Errorf("create token issuer: %w", err)
}
s.tokenIssuer = issuer
return nil
}
The comment acknowledges the gap. auth.NewTokenIssuerFromSigner exists at internal/auth/jwt.go:69 but has zero callers.
Why this matters
- Paper §3.2 diagram shows one "Signing Key (Server Holds)" object used for the agent's JWT and attestations alike
- With separate ephemeral keys, the SPIRE SVID only gates attestation signing; the JWT is effectively self-signed by a key that dies with the process
- Verifying JWTs against any external trust anchor (SPIRE trust bundle, OIDC discovery, etc.) is impossible
- Session replays across processes fail even when they should succeed — each new
aflock serve process has a new key
Proposed fix
In initAuth(), when s.signer has completed Initialize() (SPIRE branch) successfully, extract the crypto.Signer and use it:
func (s *Server) initAuth() error {
if s.signer != nil {
if id, _ := s.signer.GetSigningIdentity(); id != nil {
if priv, ok := id.PrivateKey.(crypto.Signer); ok {
s.tokenIssuer = auth.NewTokenIssuerFromSigner(priv, id.SPIFFEID.String())
return nil
}
}
}
issuer, err := auth.NewTokenIssuer()
if err != nil {
return fmt.Errorf("create token issuer: %w", err)
}
s.tokenIssuer = issuer
return nil
}
Notes:
- SPIRE's default X509-SVID key is ECDSA P-256 by default →
jwt.SigningMethodES256 stays correct
- Fulcio path also provides a
crypto.Signer (via rookery/plugins/signers/fulcio) but with a very short TTL (~10 min); not a great JWT signer — keep ephemeral for that branch
- For the ephemeral branch, current behavior is fine
Verification
After fix, with SPIRE active:
aflock get_token → decoded JWT header kid should equal the SPIRE SVID string, not "ephemeral-ecdsa-p256"
- JWT validates against the X.509 public key in the SPIRE trust bundle
- Attestation
keyid and JWT kid reference the same SPIFFE ID
Related
Summary
In MCP mode,
initAuth()always generates a fresh ephemeral ECDSA P-256 key for JWT signing — even wheninitSigning()has just successfully connected to SPIRE and obtained an SVID. The JWT signing identity and the attestation signing identity are two independent ephemeral objects, which contradicts the paper §3.2 model of a single server identity backed by SPIRE.Evidence
internal/mcp/server.go:392-400:The comment acknowledges the gap.
auth.NewTokenIssuerFromSignerexists atinternal/auth/jwt.go:69but has zero callers.Why this matters
aflock serveprocess has a new keyProposed fix
In
initAuth(), whens.signerhas completedInitialize()(SPIRE branch) successfully, extract thecrypto.Signerand use it:Notes:
jwt.SigningMethodES256stays correctcrypto.Signer(viarookery/plugins/signers/fulcio) but with a very short TTL (~10 min); not a great JWT signer — keep ephemeral for that branchVerification
After fix, with SPIRE active:
aflock get_token→ decoded JWT headerkidshould equal the SPIRE SVID string, not"ephemeral-ecdsa-p256"keyidand JWTkidreference the same SPIFFE IDRelated