Skip to content

license-go

Latest Release License: Apache 2.0 CI/CD Go Version Security Policy

license-go is the centralized, secure Go licensing framework and CLI for the Divmora organization. It eliminates duplicated licensing implementations across products such as gitlab-fleet-governor and otel-aws-log-processor, providing a unified claims schema, Ed25519 cryptographic signing and verification, and an embeddable client SDK.


Key Features

  • Asymmetric Cryptography (Ed25519): Fast, compact, tamper-proof license generation and verification.
  • Zero External Crypto Dependencies: Built purely on Go standard library (crypto/ed25519, crypto/x509, encoding/pem). No CGo or heavy third-party bloat.
  • Dual Representation:
    • Armored Text Blocks: Human-friendly files with boundary headers (-----BEGIN DIVMORA LICENSE KEY-----).
    • Compact Tokens: Single-line DIV1.<payload>.<signature> strings for environment variables and CLI arguments.
  • Rich Claims Schema: Supports customer details, product targeting, subscription tiers (community, starter, pro, enterprise, trial), expiration dates, feature flags, numerical quotas/limits, and custom key-value metadata.
  • Offline Revocation Lists (CRL): Support cryptographically signed revocation lists (crl.divcrl) to invalidate compromised, leaked, or refunded licenses in air-gapped environments without network access.
  • Cryptographic Release Attestation: Cryptographically seal release binaries with Ed25519 signatures, commit hashes, build dates, and SHA-256 digests (release.sig).
  • Background Daemon Manager: Built-in Manager for long-running services with hot-reloading from disk and automated expiration warnings.
  • CLI Included: cmd/license-cli provides instant keygen, issue, verify, inspect, crl, and release attestation subcommands for CI/CD and administration.

Installation

go get github.com/divmora/license-go

To install the CLI tool:

go install github.com/divmora/license-go/cmd/license-cli@latest

Quickstart: Client Integration

1. Simple Verification

In your product (e.g. gitlab-fleet-governor or otel-aws-log-processor):

package main

import (
	"fmt"
	"log"

	license "github.com/divmora/license-go/pkg/license"
)

// Divmora public key (can be embedded or loaded from config/secret)
const publicKeyPEM = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----`

func main() {
	// 1. Create a validator configured for your product
	validator, err := license.NewValidatorFromPEM(
		[]byte(publicKeyPEM),
		license.WithProduct("gitlab-fleet-governor"),
	)
	if err != nil {
		log.Fatalf("failed to initialize validator: %v", err)
	}

	// 2. Verify license automatically from environment (DIVMORA_LICENSE_KEY / DIVMORA_LICENSE_FILE / /etc/divmora/license.key)
	// Or explicitly pass a file path: validator.VerifyFromFile("/etc/divmora/license.key")
	claims, err := validator.VerifyEnv()
	if err != nil {
		log.Fatalf("License verification failed: %v", err)
	}

	fmt.Printf("Licensed to: %s (Plan: %s)\n", claims.Customer.Name, claims.Plan)
	fmt.Printf("Days remaining: %d\n", claims.DaysRemaining())

	// 3. Check feature entitlements
	if claims.HasFeature("ha") {
		fmt.Println("High Availability mode enabled")
	}

	// 4. Enforce quota limits
	currentRunners := int64(120)
	if err := claims.CheckLimit("max_runners", currentRunners); err != nil {
		log.Fatalf("Quota exceeded: %v", err)
	}
}

2. Standardized Environment Resolution

license-go provides standardized resolution hierarchies eliminating custom loading boilerplate for both licenses and verification public keys:

License Resolution Hierarchy

Priority Source Description
1 explicitSource argument Direct file path or inline token string passed to ResolveToken(), VerifyResolved(), or -license CLI flag.
2 DIVMORA_LICENSE_KEY Environment variable containing raw compact token (DIV1...) or armored PEM block text. Ideal for Docker, Lambda, and 12-factor apps.
3 DIVMORA_LICENSE_FILE Environment variable containing filesystem path to license file. Ideal for Kubernetes Secrets & ConfigMap mounts.
4 /etc/divmora/license.key Default Linux/container filesystem location if file exists.

Public Verification KeyRing Resolution Hierarchy

Priority Source Description
0 Programmatic Override In-memory override via SetVerificationPublicKey(key) or SetVerificationKeyRing(ring) (ideal for automated unit/integration tests).
1 Explicit Embedded Keys (Default) Embedded fallback public keys passed to NewValidatorWithFallbackKey() or ResolveKeyRing() act as an immutable root of trust to prevent environment-based trust root spoofing.
2 DIVMORA_PUBLIC_KEYS_PEM Multi-key or single-key PKIX PEM bundle text string or filesystem path (active when no explicit keys are passed, or when WithAllowEnvKeyOverride(true) is configured).
3 DIVMORA_PUBLIC_KEY Single Ed25519 public key (base64 raw 32-byte, base64 PKIX DER, inline PEM, or filesystem path).
4 DIVMORA_PUBLIC_KEY_FILE Filesystem path to public key file on disk.
5 /etc/divmora/public.pem Default Linux/container filesystem location if file exists.
6 Fallback Keys (Opt-In Override) Used when WithAllowEnvKeyOverride(true) or SetAllowEnvKeyOverride(true) is enabled and no environment variables are set.

Offline Revocation List (CRL) Resolution Hierarchy

Priority Source Description
1 Explicit Argument / Option Direct path or raw token passed to WithRevocationList(...), WithRevocationListFile(...), ResolveRevocationList(path), or -crl CLI flag.
2 DIVMORA_CRL Environment variable containing raw compact token (DIVCRL1...) or armored PEM block text. Ideal for Docker, Lambda, and air-gapped environments.
3 DIVMORA_CRL_FILE Environment variable containing filesystem path to revocation list file (.divcrl).
4 /etc/divmora/crl.divcrl Default Linux/container filesystem location if file exists.

Important

Trust Root Spoofing Defense: To prevent attackers in untrusted container/Kubernetes environments from replacing the vendor's public key with a forged key via DIVMORA_PUBLIC_KEY, NewValidatorWithFallbackKey(vendorKey) and ResolveKeyRing(vendorKey) treat explicit embedded keys as authoritative by default. Use WithAllowEnvKeyOverride(true) only if you intentionally wish to allow environment variables to override embedded keys (e.g. in staging/dev environments).

// Direct resolution helpers:
token, err := license.ResolveToken() // Returns license token string from env/file
resolved, err := license.ResolveLicense() // Returns content + FilePath for hot reloading

// Offline Certificate Revocation List (CRL) resolution:
crl, err := license.ResolveRevocationList("") // Auto-resolves from DIVMORA_CRL / DIVMORA_CRL_FILE / /etc/divmora/crl.divcrl

// Public verification KeyRing resolution:
ring, err := license.ResolveKeyRing(embeddedFallbackKey) // Resolves trusted KeyRing
pubKey, err := license.ResolvePublicKey(embeddedFallbackKey) // Resolves primary public key

// Zero-boilerplate validator initialization with auto-resolved CRL:
validator, err := license.NewValidatorFromEnv(
	license.WithProduct("gitlab-fleet-governor"),
	license.WithAutoResolvedRevocationList(),
)
// Or compile-time //go:embed helper:
// //go:embed public.pem
// var embeddedPublicKey []byte
validator, err := license.NewValidatorFromEmbeddedPEM(embeddedPublicKey, 
	license.WithProduct("gitlab-fleet-governor"),
	license.WithAutoResolvedRevocationList(),
)

3. Daemon Background Manager (Hot-Reloading & Expiry Alerts)

For long-running microservices and daemons, use license.NewManager. If LicenseFile and LicenseString are omitted, Manager automatically resolves from DIVMORA_LICENSE_FILE (enabling hot-reloading) or DIVMORA_LICENSE_KEY:

package main

import (
	"context"
	"log"
	"time"

	license "github.com/divmora/license-go/pkg/license"
)

func main() {
	validator, err := license.NewValidatorFromPEMFile(
		"/etc/divmora/public.pem",
		license.WithProduct("otel-aws-log-processor"),
	)
	if err != nil {
		log.Fatal(err)
	}

	// Auto-resolves from DIVMORA_LICENSE_FILE or DIVMORA_LICENSE_KEY if LicenseFile is omitted:
	mgr, err := license.NewManager(license.ManagerConfig{
		Validator:         validator,
		CheckInterval:     1 * time.Hour,
		ExpiryWarningDays: 14,
		OnExpiringSoon: func(c *license.Claims, daysRemaining int) {
			log.Printf("WARNING: License expires in %d days!", daysRemaining)
		},
		OnGracePeriod: func(c *license.Claims, graceDaysRemaining int) {
			log.Printf("NOTICE: License operating in grace period (%d days left)!", graceDaysRemaining)
		},
		OnExpired: func(c *license.Claims) {
			log.Printf("CRITICAL: License has expired!")
		},
		OnReloaded: func(newClaims, oldClaims *license.Claims) {
			log.Printf("INFO: License reloaded! New tier: %s", newClaims.Plan)
		},
		OnError: func(err error) {
			log.Printf("License check error: %v", err)
		},
	})
	if err != nil {
		log.Fatalf("License initialization failed: %v", err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Start background monitoring and file watching
	mgr.Start(ctx)
	defer mgr.Stop()

	// Thread-safe queries anytime in your service:
	if mgr.HasFeature("s3_export") {
		// enable S3 exporter...
	}
}

4. BSL 1.1 Dual-Licensing & Additional Use Grants (AUG)

For products governed under Business Source License 1.1 (BSL 1.1), license-go provides first-class modeling of Additional Use Grants (e.g. unlimited non-production/staging exemptions and free community tiers). Prior to the Change Date, the software dynamically evaluates operational context against configured grants. Once the Change Date arrives, the software converts autonomously to open source (Apache-2.0), granting unrestricted access:

policy := license.BSLPolicy{
	ReleaseDate:       officialReleaseDate, // Anchored by attestation
	ChangePeriodYears: 3,                   // Converts to Apache-2.0 in 3 years
	AdditionalUseGrants: []license.BSLAdditionalUseGrant{
		// 1. Unlimited non-production exemption (staging, test, dev, demo):
		license.NewNonProductionGrant("Non-Production Exemption"),
		// 2. Free community tier in production up to 10 nodes (SSO requires commercial license):
		license.NewFreeTierGrant("Community Free Tier", map[string]int64{"max_nodes": 10}, "sso"),
	},
}

validator, err := license.NewValidator(pubKey,
	license.WithProduct("gitlab-fleet-governor"),
	license.WithBSLPolicy(policy),
	license.WithCurrentEnvironment(os.Getenv("ENV")),
	license.WithCurrentUsage(map[string]int64{"max_nodes": currentNodes}),
)

// Zero-license resolution: succeeds if usage satisfies Additional Use Grants or if Change Date reached!
claims, err := validator.VerifyEnv()
if err != nil {
	// Fails with *CommercialLicenseRequiredError if usage exceeds free bounds
	log.Fatalf("Commercial license required: %v", err)
}

Claims Structure Reference

type Customer struct {
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
    OrgID string `json:"org_id,omitempty"`
}

type Scope struct {
    Environments []string            `json:"environments,omitempty"` // ["production", "staging"]
    Accounts     []string            `json:"accounts,omitempty"`     // AWS Account IDs, cloud tenant IDs
    Regions      []string            `json:"regions,omitempty"`      // Cloud regions ["us-east-1", "eu-west-*"]
    Clusters     []string            `json:"clusters,omitempty"`     // Kubernetes / ECS cluster IDs
    Namespaces   []string            `json:"namespaces,omitempty"`   // GitLab groups/projects ["acme-corp/*"]
    Hosts        []string            `json:"hosts,omitempty"`        // Hostnames, FQDNs, domains ["*.acme.corp"]
    Resources    []string            `json:"resources,omitempty"`    // Monitored resources (ARNs, wildcards, names)
    Custom       map[string][]string `json:"custom,omitempty"`       // Arbitrary product scope dimensions
}

type Claims struct {
    ID              string            `json:"id"`
    Customer        Customer          `json:"customer"`
    Product         string            `json:"product"`  // Product name, suite ("divmora-suite"), or wildcard ("*")
    Plan            string            `json:"plan"`     // "community", "starter", "pro", "enterprise", "trial"
    IssuedAt        time.Time         `json:"issued_at"`
    NotBefore       time.Time         `json:"not_before,omitempty"`
    ExpiresAt       time.Time         `json:"expires_at"` // zero time = perpetual
    GracePeriodDays int               `json:"grace_period_days,omitempty"` // Buffer days after ExpiresAt
    Features        []string          `json:"features,omitempty"`
    Limits          map[string]int64  `json:"limits,omitempty"`   // -1 = unlimited
    Scope           *Scope            `json:"scope,omitempty"`    // Scoping constraints
    Environment     string            `json:"environment,omitempty"`
    Fingerprint     string            `json:"fingerprint,omitempty"`
    Metadata        map[string]string `json:"metadata,omitempty"`
}

Useful Evaluation Methods

  • claims.IsValidForProduct(name string) bool (supports exact match, wildcards * / all, suite bundles divmora-suite / suite, multi-product lists, and glob patterns)
  • claims.HasFeature(name string) bool (supports exact match, wildcards * / all, and glob patterns like audit:*)
  • claims.IsInGracePeriod() bool (true if passed ExpiresAt but within GracePeriodDays)
  • claims.GraceDaysRemaining() int (remaining grace buffer days before hard cutoff)
  • claims.EffectiveExpiration() time.Time (final cutoff: ExpiresAt + GracePeriodDays)
  • claims.Status() Status (returns ACTIVE, GRACE_PERIOD, EXPIRED, or NOT_YET_VALID)
  • claims.IsBoundToFingerprint() bool (true if license is bound to a specific node/cluster)
  • claims.MatchesFingerprint(hostFingerprint string) bool (case-insensitive fingerprint evaluation)
  • claims.IsInScope(dimension, target string) bool (checks authorization against any scope dimension)
  • claims.IsEnvironmentAllowed(env string) bool
  • claims.IsAccountAllowed(account string) bool
  • claims.IsRegionAllowed(region string) bool
  • claims.IsClusterAllowed(cluster string) bool
  • claims.IsNamespaceAllowed(namespace string) bool
  • claims.IsHostAllowed(host string) bool
  • claims.IsResourceAllowed(resource string) bool (supports exact match, glob patterns, short IDs, and AWS ARN suffix extraction)
  • claims.AssertResource(resource string) error
  • claims.CheckResourceLimit(currentUsage int64) error (checks quota against Limits["max_resources"])
  • license.GetMaxResources(claims) int (helper extracting max_resources quota limit)
  • license.GetMaxAccounts(claims) int (helper extracting max_accounts quota limit)
  • license.GetAllowedResources(claims) []string (helper extracting allowed resources list)
  • license.MatchResourcePattern(pattern, resource string) bool (pattern matching engine for resource IDs and ARNs)
  • claims.AssertScope(dimension, target string) error
  • claims.AssertFeature(name string) error
  • claims.CheckLimit(name string, currentUsage int64) error
  • claims.IsPerpetual() bool
  • claims.IsExpired() bool
  • claims.DaysRemaining() int
  • claims.GetMetadata(key string) (string, bool)

CLI Usage

1. Generate Keypair

license-cli keygen -out-dir ./keys

Outputs:

  • ./keys/private.pem (mode 0600 - keep secure, Divmora internal only)
  • ./keys/public.pem (mode 0644 - embed or distribute with products)

2. Issue a License

license-cli issue \
  -private-key ./keys/private.pem \
  -customer "Acme Corp" \
  -email "admin@acme.corp" \
  -org-id "org_acme_corp_01" \
  -product "gitlab-fleet-governor" \
  -plan enterprise \
  -valid-days 365 \
  -grace-days 14 \
  -features "ha,audit-logs,auto-scaling" \
  -limits "max_runners=200,max_nodes=10" \
  -scope-envs "production" \
  -scope-accounts "123456789012" \
  -scope-regions "us-east-1,eu-west-1" \
  -scope-clusters "prod-eks-01" \
  -scope-namespaces "gitlab.com/acme-corp/*" \
  -scope-hosts "*.acme.corp" \
  -scope-resources "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/prod-*/*,arn:aws:cloudfront::*:distribution/*" \
  -scope-custom "tier=platinum,gold;datacenter=dc-east,dc-west" \
  -meta "billing_id=inv-9981,contact=admin@acme.corp" \
  -fingerprint "node-cluster-01" \
  -out ./acme.license.key

3. Verify a License

# Verify explicit license with optional scope assertion flags:
license-cli verify \
  -public-key ./keys/public.pem \
  -product "gitlab-fleet-governor" \
  -env "production" \
  -namespace "gitlab.com/acme-corp/fleet" \
  -host "runner-01.acme.corp" \
  -resource "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/prod-alb/1234567890abcdef" \
  -custom-scope "tier=platinum,datacenter=dc-east" \
  -fingerprint "node-cluster-01" \
  -license ./acme.license.key

# Or verify automatically with zero configuration (resolves license from DIVMORA_LICENSE_KEY/FILE
# and public key from DIVMORA_PUBLIC_KEY / DIVMORA_PUBLIC_KEYS_PEM / /etc/divmora/public.pem):
license-cli verify -product "gitlab-fleet-governor"

# Verify license enforced with release attestation sidecar and binary checksum:
license-cli verify \
  -product "gitlab-fleet-governor" \
  -license ./acme.license.key \
  -release-attestation ./release.sig \
  -binary ./bin/gitlab-fleet-governor \
  -require-release-attestation

4. Display Standardized License Status Card (license-cli status)

Display a visual status card with active tier, countdown, BSL 1.1 open-source transition, and resource quota utilization table:

# Display status with live runtime usage counts:
license-cli status \
  -product "gitlab-fleet-governor" \
  -license ./acme.license.key \
  -usage "max_runners=142,max_nodes=6"

# Or display compact status card from environment:
license-cli status -compact

5. Inspect a License (No Key Required)

# Direct file / token inspect:
license-cli inspect -license ./acme.license.key

# Or inspect automatically from $DIVMORA_LICENSE_KEY or $DIVMORA_LICENSE_FILE:
license-cli inspect

6. Evaluate BSL 1.1 Dual-Licensing Entitlement (license-cli bsl-eval)

Evaluate deployment operational context against BSL 1.1 terms and Additional Use Grants to determine whether commercial licensing is required:

# Evaluate entitlement for staging environment:
license-cli bsl-eval \
  -release-date 2025-01-01 \
  -years 3 \
  -env staging \
  -product "gitlab-fleet-governor" \
  -usage "max_nodes=100,max_runners=500"

# Evaluate production free tier quota with custom limits and feature exclusions:
license-cli bsl-eval \
  -release-date 2025-01-01 \
  -env production \
  -free-limits "max_nodes=10,max_runners=50" \
  -usage "max_nodes=6,max_runners=20" \
  -excluded-features "sso,audit-logs"

# Or output machine-readable JSON for automated CI/CD gating:
license-cli bsl-eval -release-date 2025-01-01 -env staging -json

7. Mint a Release Attestation (CI/CD Pipeline)

Mint an Ed25519 cryptographic release attestation token or armored PEM sidecar (release.sig) in your CI/CD release build step to certify binary build authenticity, official Git commit, SemVer version, authoritative BSL 1.1 release date, and binary SHA-256 digest:

license-cli sign-release \
  -private-key ./keys/private.pem \
  -product "gitlab-fleet-governor" \
  -version "v2.5.0" \
  -git-commit "${CI_COMMIT_SHA}" \
  -binary "./bin/gitlab-fleet-governor" \
  -authority "divmora.com/release" \
  -out "./bin/release.sig" \
  -armored

8. Verify Release Provenance & Binary Integrity

Verify an official release binary against a cryptographic release attestation:

license-cli verify-release \
  -public-key ./keys/public.pem \
  -attestation ./bin/release.sig \
  -product "gitlab-fleet-governor" \
  -version "v2.5.0" \
  -git-commit "${CI_COMMIT_SHA}" \
  -binary "./bin/gitlab-fleet-governor"

9. Inspect Release Attestation Claims (No Key Required)

Inspect the unverified release claims embedded within an armored .sig file or compact token:

# Standard formatted inspection:
license-cli inspect-release -attestation ./bin/release.sig

# Or output raw JSON:
license-cli inspect-release -attestation ./bin/release.sig -json

10. Offline Certificate Revocation Lists (license-cli crl)

Manage and enforce cryptographically signed Revocation Lists (DIVCRL1) for air-gapped environments without network connectivity:

# 1. Mint a signed CRL (inline entries or via JSON entries file):
license-cli crl sign \
  -private-key ./keys/private.pem \
  -entries "lic-corp-1234=compromised,lic-corp-5678=refunded" \
  -next-update "30d" \
  -product "gitlab-fleet-governor" \
  -out ./crl.divcrl

# 2. Inspect revoked entries in a CRL (no key required):
license-cli crl inspect -crl ./crl.divcrl

# 3. Check whether a specific license ID is revoked:
license-cli crl check -crl ./crl.divcrl -id "lic-corp-1234"

# 4. Verify CRL cryptographic signature against KeyRing:
license-cli crl verify -crl ./crl.divcrl -public-key ./keys/public.pem

# 5. Fetch, verify, and cache CRL from a remote distribution point:
license-cli crl sync \
  -url "https://crl.divmora.com/gitlab-fleet-governor.divcrl" \
  -public-key ./keys/public.pem \
  -cache-file /var/lib/divmora/crl.cache \
  -out ./crl.divcrl \
  -verbose

# 6. Enforce local CRL during license verification:
license-cli verify \
  -product "gitlab-fleet-governor" \
  -license ./acme.license.key \
  -crl ./crl.divcrl

# 7. Dynamically fetch, verify, and enforce CRL with strict non-revocation requirement:
license-cli verify \
  -product "gitlab-fleet-governor" \
  -license ./acme.license.key \
  -crl-url "https://crl.divmora.com/gitlab-fleet-governor.divcrl" \
  -require-crl

Development & Building

This repository provides standardized make targets:

make build         # Compile license-cli binary into bin/
make test          # Run all unit and integration tests
make test-race     # Run tests with race detector
make test-coverage # Generate coverage report
make fmt           # Format code
make lint          # Run golangci-lint
make clean         # Remove temporary build artifacts

Documentation & Specifications


License

This software is licensed under the Apache License, Version 2.0. See the LICENSE file for complete terms and conditions.

Copyright (c) 2026 DIVMORA Technologies.

About

Pure Go software licensing framework & CLI toolkit for Divmora products (Ed25519 signing, verification, and background daemon)

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages