Skip to content

fix: JWT signing key is never the SPIRE key, even when SPIRE is active #71

@manzil-infinity180

Description

@manzil-infinity180

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:

  1. aflock get_token → decoded JWT header kid should equal the SPIRE SVID string, not "ephemeral-ecdsa-p256"
  2. JWT validates against the X.509 public key in the SPIRE trust bundle
  3. Attestation keyid and JWT kid reference the same SPIFFE ID

Related

Metadata

Metadata

Labels

bugSomething isn't workingsecuritySecurity vulnerability or concern

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions