package main import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/oidc" ) // OIDCAttestor implements the SPIRE WorkloadAttestor plugin interface. // // When SPIRE Agent needs to attest a workload, it calls Attest() with the // workload's process ID. This plugin reads the workload's OIDC token and // returns selectors based on the verified claims. // // Selectors produced: // - oidc:sub: — OIDC subject claim // - oidc:iss: — OIDC issuer // - oidc:email: — OIDC email claim (if present) // - oidc:group: — One per OIDC group claim (if present) type OIDCAttestor struct { verifier oidc.Verifier audience string tokenPath string // Path pattern for discovering OIDC tokens (supports /proc//root/ prefix) } // OIDCAttestorConfig holds plugin-specific configuration. type OIDCAttestorConfig struct { Issuer string `hcl:"issuer"` Audience string `hcl:"audience"` JWKSURL string `hcl:"jwks_url"` TokenPath string `hcl:"token_path"` // e.g., "/var/run/secrets/tokens/oidc-token" } // Configure initializes the attestor with the provided configuration. func (a *OIDCAttestor) Configure(cfg OIDCAttestorConfig) error { if cfg.TokenPath == "" { cfg.TokenPath = "/var/run/secrets/tokens/oidc-token" } verifier, err := oidc.NewVerifier(oidc.Config{ Issuer: cfg.Issuer, Audience: cfg.Audience, JWKSURL: cfg.JWKSURL, }) if err != nil { return fmt.Errorf("oidc-attestor: configure verifier: %w", err) } a.verifier = verifier a.audience = cfg.Audience a.tokenPath = cfg.TokenPath return nil } // Attest reads the OIDC token for the given PID and returns selectors. func (a *OIDCAttestor) Attest(ctx context.Context, pid int32) ([]string, error) { // Read the token from the workload's filesystem namespace. tokenFile := filepath.Join(fmt.Sprintf("/proc/%d/root", pid), a.tokenPath) tokenBytes, err := os.ReadFile(tokenFile) if err != nil { return nil, fmt.Errorf("oidc-attestor: read token for pid %d: %w", pid, err) } rawToken := strings.TrimSpace(string(tokenBytes)) if rawToken == "" { return nil, fmt.Errorf("oidc-attestor: empty token for pid %d", pid) } claims, err := a.verifier.Verify(ctx, rawToken, a.audience) if err != nil { return nil, fmt.Errorf("oidc-attestor: verify token for pid %d: %w", pid, err) } // Build selectors from verified claims. var selectors []string if claims.Subject != "" { selectors = append(selectors, "oidc:sub:"+claims.Subject) } if claims.Issuer != "" { selectors = append(selectors, "oidc:iss:"+claims.Issuer) } if claims.Email != "" { selectors = append(selectors, "oidc:email:"+claims.Email) } for _, group := range claims.Groups { selectors = append(selectors, "oidc:group:"+group) } return selectors, nil }