Pulse/pkg/licensing/provider_chain_test.go
rcourtman d25b99cbcc Make provider-MSP client runtimes verifiably licensed via chained entitlement leases
Provider-hosted MSP client workspaces previously sat at Community tier
forever: the runtime refreshed leases against the built-in Pulse Cloud URL
(hibernated, 522) instead of the provider control plane, and release-build
images verify leases only against the embedded Pulse key, which an
operator-generated CP_TRIAL_ACTIVATION_PRIVATE_KEY can never satisfy.

- Inject PULSE_PRO_TRIAL_SIGNUP_URL=CP_BASE_URL into client containers so
  lease refresh targets the provider control plane.
- Chain trust through the Pulse-signed provider MSP license: the license
  binds the provider's lease signing public key
  (entitlement_signing_public_key claim); the control plane embeds the
  license in every lease (provider_license claim); release-build runtimes
  verify embedded Pulse root -> provider license -> lease signature.
- Cap chain-verified leases at ProviderChainedLeaseCapabilities: MSP tier
  plus white_label (branded per-client reports), minus Pulse-service-backed
  relay/mobile_app/push_notifications, which otherwise loop doomed
  registrations against Pulse's relay.
- Fail fast at control-plane startup when the license does not bind the
  configured signing key, instead of provisioning silently unlicensed
  client workspaces.

Verified live on a Colima harness: release-tagged tenant image with test
embedded root, Traefik TLS, full provider-msp proof, tenant reports
valid=true plan_version=msp_growth with white_label and zero relay
failures.
2026-06-10 13:58:52 +01:00

199 lines
7.4 KiB
Go

package licensing
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"strings"
"testing"
"time"
)
// mintTestProviderMSPLicense signs an MSP license with the shared test root
// key, binding the given lease signing public key. setupTestPublicKey must be
// installed for ValidateLicense to accept it.
func mintTestProviderMSPLicense(t *testing.T, tier Tier, leaseSigningPublicKey ed25519.PublicKey, expiresIn time.Duration) string {
t.Helper()
testKeyPairInit()
claims := Claims{
LicenseID: "lic_provider_chain_test",
Email: "provider@example-msp.com",
Tier: tier,
IssuedAt: time.Now().Unix(),
PlanVersion: "msp_starter",
}
if expiresIn != 0 {
claims.ExpiresAt = time.Now().Add(expiresIn).Unix()
}
if len(leaseSigningPublicKey) > 0 {
claims.EntitlementSigningPublicKey = base64.StdEncoding.EncodeToString(leaseSigningPublicKey)
}
payload, err := json.Marshal(claims)
if err != nil {
t.Fatalf("marshal provider license claims: %v", err)
}
return signTestJWT(t, payload, testPrivateKey)
}
func signTestProviderLease(t *testing.T, signingKey ed25519.PrivateKey, providerLicense string, capabilities []string) string {
t.Helper()
claims := EntitlementLeaseClaims{
OrgID: "default",
InstanceHost: "t-acme.pulse.example-msp.com",
PlanVersion: "msp_starter",
SubscriptionState: SubStateActive,
Capabilities: capabilities,
ProviderLicense: providerLicense,
}
token, err := SignEntitlementLeaseToken(signingKey, claims)
if err != nil {
t.Fatalf("sign provider lease: %v", err)
}
return token
}
func TestEntitlementLeaseProviderChainVerifies(t *testing.T) {
setupTestPublicKey(t)
providerPub, providerPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate provider key pair: %v", err)
}
license := mintTestProviderMSPLicense(t, TierMSP, providerPub, 30*24*time.Hour)
token := signTestProviderLease(t, providerPriv, license, []string{FeatureWhiteLabel, FeatureMultiTenant})
claims, err := VerifyEntitlementLeaseToken(token, testPublicKey, "t-acme.pulse.example-msp.com", time.Now())
if err != nil {
t.Fatalf("VerifyEntitlementLeaseToken() error = %v, want chain-verified lease", err)
}
got := strings.Join(claims.Capabilities, ",")
if !strings.Contains(got, FeatureWhiteLabel) {
t.Fatalf("chain-verified lease lost white_label: capabilities = %q", got)
}
}
func TestEntitlementLeaseProviderChainCapsCapabilities(t *testing.T) {
setupTestPublicKey(t)
providerPub, providerPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate provider key pair: %v", err)
}
license := mintTestProviderMSPLicense(t, TierMSP, providerPub, 30*24*time.Hour)
// multi_user is enterprise-only and relay is Pulse-service-backed; a
// provider-minted lease must not be able to grant either even though
// the lease signature verifies.
token := signTestProviderLease(t, providerPriv, license, []string{FeatureWhiteLabel, FeatureMultiUser, FeatureRelay})
claims, err := VerifyEntitlementLeaseToken(token, testPublicKey, "t-acme.pulse.example-msp.com", time.Now())
if err != nil {
t.Fatalf("VerifyEntitlementLeaseToken() error = %v", err)
}
for _, capability := range claims.Capabilities {
switch capability {
case FeatureMultiUser:
t.Fatalf("chained lease escaped the MSP capability ceiling: %v", claims.Capabilities)
case FeatureRelay, FeatureMobileApp, FeaturePushNotifications:
t.Fatalf("chained lease must not grant Pulse-service-backed capability %q: %v", capability, claims.Capabilities)
}
}
if got := strings.Join(claims.Capabilities, ","); !strings.Contains(got, FeatureWhiteLabel) {
t.Fatalf("capability cap removed white_label, which MSP leases legitimately carry: %q", got)
}
}
func TestEntitlementLeaseProviderChainRejectsWrongSigner(t *testing.T) {
setupTestPublicKey(t)
providerPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate provider key pair: %v", err)
}
_, otherPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate other key pair: %v", err)
}
license := mintTestProviderMSPLicense(t, TierMSP, providerPub, 30*24*time.Hour)
token := signTestProviderLease(t, otherPriv, license, []string{FeatureWhiteLabel})
if _, err := VerifyEntitlementLeaseToken(token, testPublicKey, "t-acme.pulse.example-msp.com", time.Now()); err == nil {
t.Fatal("lease signed by a key the provider license does not bind must be rejected")
}
}
func TestEntitlementLeaseProviderChainRejectsNonMSPLicense(t *testing.T) {
setupTestPublicKey(t)
providerPub, providerPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate provider key pair: %v", err)
}
license := mintTestProviderMSPLicense(t, TierPro, providerPub, 30*24*time.Hour)
token := signTestProviderLease(t, providerPriv, license, []string{FeatureWhiteLabel})
if _, err := VerifyEntitlementLeaseToken(token, testPublicKey, "t-acme.pulse.example-msp.com", time.Now()); err == nil {
t.Fatal("non-MSP license must not bind an entitlement lease signing key")
}
}
func TestEntitlementLeaseProviderChainRejectsUnboundLicense(t *testing.T) {
setupTestPublicKey(t)
_, providerPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate provider key pair: %v", err)
}
license := mintTestProviderMSPLicense(t, TierMSP, nil, 30*24*time.Hour)
token := signTestProviderLease(t, providerPriv, license, []string{FeatureWhiteLabel})
if _, err := VerifyEntitlementLeaseToken(token, testPublicKey, "t-acme.pulse.example-msp.com", time.Now()); err == nil {
t.Fatal("license without an entitlement signing key binding must not chain")
}
}
func TestEntitlementLeaseProviderChainRejectsForgedLicense(t *testing.T) {
setupTestPublicKey(t)
providerPub, providerPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate provider key pair: %v", err)
}
// License signed by the provider's own key instead of the Pulse root: a
// self-issued chain must die at license validation.
claims := Claims{
LicenseID: "lic_forged",
Email: "attacker@example.com",
Tier: TierMSP,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(30 * 24 * time.Hour).Unix(),
PlanVersion: "msp_starter",
EntitlementSigningPublicKey: base64.StdEncoding.EncodeToString(providerPub),
}
payload, err := json.Marshal(claims)
if err != nil {
t.Fatalf("marshal forged license claims: %v", err)
}
forgedLicense := signTestJWT(t, payload, providerPriv)
token := signTestProviderLease(t, providerPriv, forgedLicense, []string{FeatureWhiteLabel})
if _, err := VerifyEntitlementLeaseToken(token, testPublicKey, "t-acme.pulse.example-msp.com", time.Now()); err == nil {
t.Fatal("a provider license not signed by the Pulse root must not chain")
}
}
func TestEntitlementLeaseWithoutProviderLicenseUsesDirectRoot(t *testing.T) {
setupTestPublicKey(t)
token := signTestProviderLease(t, testPrivateKey, "", []string{FeatureWhiteLabel})
claims, err := VerifyEntitlementLeaseToken(token, testPublicKey, "t-acme.pulse.example-msp.com", time.Now())
if err != nil {
t.Fatalf("direct-root lease verification regressed: %v", err)
}
// Direct-root leases are not capability-capped; the signer is Pulse.
if got := strings.Join(claims.Capabilities, ","); !strings.Contains(got, FeatureWhiteLabel) {
t.Fatalf("direct-root lease capabilities = %q", got)
}
}