Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
This commit is contained in:
162
pkg/license/store.go
Normal file
162
pkg/license/store.go
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
Volt Platform — License Persistence
|
||||
Store and retrieve license data and cryptographic keys
|
||||
*/
|
||||
package license
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
LicenseDir = "/etc/volt/license"
|
||||
LicenseFile = "/etc/volt/license/license.yaml"
|
||||
NodeKeyFile = "/etc/volt/license/node.key"
|
||||
NodePubFile = "/etc/volt/license/node.pub"
|
||||
)
|
||||
|
||||
// Store handles license persistence
|
||||
type Store struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
// NewStore creates a license store with the default directory
|
||||
func NewStore() *Store {
|
||||
return &Store{Dir: LicenseDir}
|
||||
}
|
||||
|
||||
// licensePath returns the full path for the license file
|
||||
func (s *Store) licensePath() string {
|
||||
return filepath.Join(s.Dir, "license.yaml")
|
||||
}
|
||||
|
||||
// keyPath returns the full path for the node private key
|
||||
func (s *Store) keyPath() string {
|
||||
return filepath.Join(s.Dir, "node.key")
|
||||
}
|
||||
|
||||
// pubPath returns the full path for the node public key
|
||||
func (s *Store) pubPath() string {
|
||||
return filepath.Join(s.Dir, "node.pub")
|
||||
}
|
||||
|
||||
// Load reads the license from disk
|
||||
func (s *Store) Load() (*License, error) {
|
||||
data, err := os.ReadFile(s.licensePath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("no license found (not registered)")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read license: %w", err)
|
||||
}
|
||||
|
||||
var lic License
|
||||
if err := yaml.Unmarshal(data, &lic); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse license: %w", err)
|
||||
}
|
||||
|
||||
return &lic, nil
|
||||
}
|
||||
|
||||
// Save writes the license to disk
|
||||
func (s *Store) Save(lic *License) error {
|
||||
if err := os.MkdirAll(s.Dir, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create license directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(lic)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal license: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(s.licensePath(), data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write license: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRegistered checks if a valid license exists on disk
|
||||
func (s *Store) IsRegistered() bool {
|
||||
_, err := s.Load()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsExpired checks if the current license has expired
|
||||
func (s *Store) IsExpired() (bool, error) {
|
||||
lic, err := s.Load()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if lic.ExpiresAt.IsZero() {
|
||||
return false, nil // no expiry = never expires
|
||||
}
|
||||
return time.Now().After(lic.ExpiresAt), nil
|
||||
}
|
||||
|
||||
// HasFeature checks if the current license tier includes a feature
|
||||
func (s *Store) HasFeature(feature string) (bool, error) {
|
||||
lic, err := s.Load()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return TierIncludes(lic.Tier, feature), nil
|
||||
}
|
||||
|
||||
// GenerateKeypair generates an X25519 keypair and stores it on disk
|
||||
func (s *Store) GenerateKeypair() (pubHex string, err error) {
|
||||
if err := os.MkdirAll(s.Dir, 0700); err != nil {
|
||||
return "", fmt.Errorf("failed to create license directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate X25519 keypair using crypto/ecdh
|
||||
curve := ecdh.X25519()
|
||||
privKey, err := curve.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate keypair: %w", err)
|
||||
}
|
||||
|
||||
// Encode to hex
|
||||
privHex := hex.EncodeToString(privKey.Bytes())
|
||||
pubHex = hex.EncodeToString(privKey.PublicKey().Bytes())
|
||||
|
||||
// Store private key (restrictive permissions)
|
||||
if err := os.WriteFile(s.keyPath(), []byte(privHex+"\n"), 0600); err != nil {
|
||||
return "", fmt.Errorf("failed to write private key: %w", err)
|
||||
}
|
||||
|
||||
// Store public key
|
||||
if err := os.WriteFile(s.pubPath(), []byte(pubHex+"\n"), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write public key: %w", err)
|
||||
}
|
||||
|
||||
return pubHex, nil
|
||||
}
|
||||
|
||||
// ReadPublicKey reads the stored node public key
|
||||
func (s *Store) ReadPublicKey() (string, error) {
|
||||
data, err := os.ReadFile(s.pubPath())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read public key: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Remove deletes the license and keypair from disk
|
||||
func (s *Store) Remove() error {
|
||||
files := []string{s.licensePath(), s.keyPath(), s.pubPath()}
|
||||
for _, f := range files {
|
||||
if err := os.Remove(f); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove %s: %w", f, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user