Implement authorization import and revocation checks (T-503)
This commit is contained in:
@@ -47,6 +47,8 @@ type InstalledApp struct {
|
||||
Entrypoint string `json:"entrypoint,omitempty"`
|
||||
WorkingDirectory string `json:"working_directory,omitempty"`
|
||||
MinOS string `json:"min_os,omitempty"`
|
||||
ProductID string `json:"product_id,omitempty"`
|
||||
SupportsTrial bool `json:"supports_trial"`
|
||||
RequiresAdmin bool `json:"requires_admin"`
|
||||
Files []InstalledFile `json:"files"`
|
||||
}
|
||||
@@ -332,6 +334,9 @@ func (record InstalledApp) validate() error {
|
||||
record.MinOS != "windows-10" && record.MinOS != "windows-11" {
|
||||
return fmt.Errorf("%w: min_os=%q", ErrInstalledAppInvalid, record.MinOS)
|
||||
}
|
||||
if record.ProductID != "" && !appIDPattern.MatchString(record.ProductID) {
|
||||
return fmt.Errorf("%w: invalid product_id", ErrInstalledAppInvalid)
|
||||
}
|
||||
if record.Files == nil {
|
||||
return fmt.Errorf("%w: files must be an array", ErrInstalledAppInvalid)
|
||||
}
|
||||
|
||||
@@ -306,6 +306,7 @@ func validInstalledApp() InstalledApp {
|
||||
Version: "1.2.0",
|
||||
Architecture: "amd64",
|
||||
Channel: "stable",
|
||||
ProductID: "product-json-parser",
|
||||
Files: []InstalledFile{
|
||||
{
|
||||
Path: "JsonParser.exe",
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"softbox.local/core/licensing"
|
||||
)
|
||||
|
||||
const MaxLicenseDocumentBytes int64 = 1 << 20
|
||||
|
||||
var (
|
||||
ErrLicenseStoreInvalid = errors.New("license store is invalid")
|
||||
ErrLicenseStoreUnsafe = errors.New("license store layout is unsafe")
|
||||
)
|
||||
|
||||
var licenseDocumentNamePattern = regexp.MustCompile(`^[0-9a-f]{64}\.license$`)
|
||||
|
||||
// LicenseStore stores only documents that have already passed the caller's
|
||||
// signature and machine-binding verification. It never returns source paths.
|
||||
type LicenseStore struct {
|
||||
root string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewLicenseStore creates a store rooted at the product-level licenses folder.
|
||||
func NewLicenseStore(root string) *LicenseStore {
|
||||
return &LicenseStore{root: root}
|
||||
}
|
||||
|
||||
// Import verifies then atomically persists one license. imported is false when
|
||||
// the same verified document is already present.
|
||||
func (store *LicenseStore) Import(
|
||||
document []byte,
|
||||
verifier licensing.Verifier,
|
||||
expectedMachineHash string,
|
||||
) (license licensing.License, imported bool, err error) {
|
||||
if store == nil {
|
||||
return licensing.License{}, false, ErrLicenseStoreUnsafe
|
||||
}
|
||||
if len(document) == 0 || int64(len(document)) > MaxLicenseDocumentBytes {
|
||||
return licensing.License{}, false, ErrLicenseStoreInvalid
|
||||
}
|
||||
license, err = verifier.Verify(document, expectedMachineHash)
|
||||
if err != nil {
|
||||
return licensing.License{}, false, err
|
||||
}
|
||||
digest := sha256.Sum256(document)
|
||||
name := fmtLicenseDocumentName(digest)
|
||||
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
directory, err := store.ensureDocumentsDirectory()
|
||||
if err != nil {
|
||||
return licensing.License{}, false, err
|
||||
}
|
||||
target := filepath.Join(directory, name)
|
||||
if existing, found, err := readStoredDocument(target); err != nil {
|
||||
return licensing.License{}, false, err
|
||||
} else if found {
|
||||
if !bytes.Equal(existing, document) {
|
||||
return licensing.License{}, false, ErrLicenseStoreInvalid
|
||||
}
|
||||
return license, false, nil
|
||||
}
|
||||
if err := writeNewLicenseDocument(directory, target, document); err != nil {
|
||||
return licensing.License{}, false, err
|
||||
}
|
||||
return license, true, nil
|
||||
}
|
||||
|
||||
// List revalidates every cached license before returning a detached list.
|
||||
func (store *LicenseStore) List(
|
||||
verifier licensing.Verifier,
|
||||
expectedMachineHash string,
|
||||
) ([]licensing.License, error) {
|
||||
if store == nil {
|
||||
return nil, ErrLicenseStoreUnsafe
|
||||
}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
directory, exists, err := store.inspectDocumentsDirectory()
|
||||
if err != nil || !exists {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return nil, ErrLicenseStoreInvalid
|
||||
}
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !licenseDocumentNamePattern.MatchString(entry.Name()) {
|
||||
return nil, ErrLicenseStoreUnsafe
|
||||
}
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
sort.Strings(names)
|
||||
licenses := make([]licensing.License, 0, len(names))
|
||||
for _, name := range names {
|
||||
document, found, err := readStoredDocument(filepath.Join(directory, name))
|
||||
if err != nil || !found {
|
||||
return nil, ErrLicenseStoreInvalid
|
||||
}
|
||||
digest := sha256.Sum256(document)
|
||||
if name != fmtLicenseDocumentName(digest) {
|
||||
return nil, ErrLicenseStoreInvalid
|
||||
}
|
||||
license, err := verifier.Verify(document, expectedMachineHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
license.Products = append([]string(nil), license.Products...)
|
||||
licenses = append(licenses, license)
|
||||
}
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
// StoreRevocations verifies and atomically replaces the only revocation cache.
|
||||
func (store *LicenseStore) StoreRevocations(
|
||||
document []byte,
|
||||
verifier licensing.RevocationVerifier,
|
||||
) (licensing.RevocationList, error) {
|
||||
if store == nil {
|
||||
return licensing.RevocationList{}, ErrLicenseStoreUnsafe
|
||||
}
|
||||
if len(document) == 0 || int64(len(document)) > MaxLicenseDocumentBytes {
|
||||
return licensing.RevocationList{}, ErrLicenseStoreInvalid
|
||||
}
|
||||
list, err := verifier.Verify(document)
|
||||
if err != nil {
|
||||
return licensing.RevocationList{}, err
|
||||
}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
root, err := store.ensureRoot()
|
||||
if err != nil {
|
||||
return licensing.RevocationList{}, err
|
||||
}
|
||||
if err := writeReplacementDocument(root, filepath.Join(root, "revocations-v1.json"), document); err != nil {
|
||||
return licensing.RevocationList{}, err
|
||||
}
|
||||
list.RevokedLicenseIDs = append([]string(nil), list.RevokedLicenseIDs...)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// LoadRevocations revalidates the cached list. found is false only when no
|
||||
// cache has ever been stored; malformed storage is never treated as absent.
|
||||
func (store *LicenseStore) LoadRevocations(
|
||||
verifier licensing.RevocationVerifier,
|
||||
) (list licensing.RevocationList, found bool, err error) {
|
||||
if store == nil {
|
||||
return licensing.RevocationList{}, false, ErrLicenseStoreUnsafe
|
||||
}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
root, exists, err := store.inspectRoot()
|
||||
if err != nil || !exists {
|
||||
return licensing.RevocationList{}, false, err
|
||||
}
|
||||
document, found, err := readStoredDocument(filepath.Join(root, "revocations-v1.json"))
|
||||
if err != nil || !found {
|
||||
return licensing.RevocationList{}, found, err
|
||||
}
|
||||
list, err = verifier.Verify(document)
|
||||
if err != nil {
|
||||
return licensing.RevocationList{}, true, err
|
||||
}
|
||||
list.RevokedLicenseIDs = append([]string(nil), list.RevokedLicenseIDs...)
|
||||
return list, true, nil
|
||||
}
|
||||
|
||||
func (store *LicenseStore) ensureDocumentsDirectory() (string, error) {
|
||||
root, err := store.ensureRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
directory := filepath.Join(root, "v1")
|
||||
if err := os.Mkdir(directory, 0o700); err != nil && !os.IsExist(err) {
|
||||
return "", ErrLicenseStoreInvalid
|
||||
}
|
||||
if err := requireLicenseDirectory(directory); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
func (store *LicenseStore) inspectDocumentsDirectory() (string, bool, error) {
|
||||
root, exists, err := store.inspectRoot()
|
||||
if err != nil || !exists {
|
||||
return "", false, err
|
||||
}
|
||||
directory := filepath.Join(root, "v1")
|
||||
info, err := os.Lstat(directory)
|
||||
if os.IsNotExist(err) {
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return "", false, ErrLicenseStoreUnsafe
|
||||
}
|
||||
return directory, true, nil
|
||||
}
|
||||
|
||||
func (store *LicenseStore) ensureRoot() (string, error) {
|
||||
if store == nil || store.root == "" {
|
||||
return "", ErrLicenseStoreUnsafe
|
||||
}
|
||||
root, err := filepath.Abs(store.root)
|
||||
if err != nil {
|
||||
return "", ErrLicenseStoreUnsafe
|
||||
}
|
||||
if err := os.MkdirAll(root, 0o700); err != nil {
|
||||
return "", ErrLicenseStoreInvalid
|
||||
}
|
||||
if err := requireLicenseDirectory(root); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func (store *LicenseStore) inspectRoot() (string, bool, error) {
|
||||
if store == nil || store.root == "" {
|
||||
return "", false, ErrLicenseStoreUnsafe
|
||||
}
|
||||
root, err := filepath.Abs(store.root)
|
||||
if err != nil {
|
||||
return "", false, ErrLicenseStoreUnsafe
|
||||
}
|
||||
info, err := os.Lstat(root)
|
||||
if os.IsNotExist(err) {
|
||||
return "", false, nil
|
||||
}
|
||||
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return "", false, ErrLicenseStoreUnsafe
|
||||
}
|
||||
return root, true, nil
|
||||
}
|
||||
|
||||
func requireLicenseDirectory(path string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return ErrLicenseStoreUnsafe
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readStoredDocument(path string) ([]byte, bool, error) {
|
||||
info, err := os.Lstat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() ||
|
||||
info.Size() <= 0 || info.Size() > MaxLicenseDocumentBytes {
|
||||
return nil, false, ErrLicenseStoreUnsafe
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, false, ErrLicenseStoreInvalid
|
||||
}
|
||||
defer file.Close()
|
||||
document, err := io.ReadAll(io.LimitReader(file, MaxLicenseDocumentBytes+1))
|
||||
if err != nil || len(document) == 0 || int64(len(document)) > MaxLicenseDocumentBytes {
|
||||
return nil, false, ErrLicenseStoreInvalid
|
||||
}
|
||||
return document, true, nil
|
||||
}
|
||||
|
||||
func writeNewLicenseDocument(directory, target string, document []byte) error {
|
||||
temporary, err := os.CreateTemp(directory, ".license-*.tmp")
|
||||
if err != nil {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
if err := writeAndCloseLicenseDocument(temporary, document); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(temporaryPath, target); err != nil {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeReplacementDocument(directory, target string, document []byte) error {
|
||||
temporary, err := os.CreateTemp(directory, ".revocations-*.tmp")
|
||||
if err != nil {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
if err := writeAndCloseLicenseDocument(temporary, document); err != nil {
|
||||
return err
|
||||
}
|
||||
backup := target + ".backup"
|
||||
if existing, found, err := readStoredDocument(target); err != nil {
|
||||
return err
|
||||
} else if found {
|
||||
if len(existing) == 0 {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
if info, err := os.Lstat(backup); err == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || os.Remove(backup) != nil {
|
||||
return ErrLicenseStoreUnsafe
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return ErrLicenseStoreUnsafe
|
||||
}
|
||||
if err := os.Rename(target, backup); err != nil {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
if err := os.Rename(temporaryPath, target); err != nil {
|
||||
_ = os.Rename(backup, target)
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
if err := os.Remove(backup); err != nil && !os.IsNotExist(err) {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := os.Rename(temporaryPath, target); err != nil {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAndCloseLicenseDocument(file *os.File, document []byte) error {
|
||||
if err := file.Chmod(0o600); err != nil {
|
||||
file.Close()
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
if _, err := file.Write(document); err != nil {
|
||||
file.Close()
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
file.Close()
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return ErrLicenseStoreInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fmtLicenseDocumentName(digest [sha256.Size]byte) string {
|
||||
const hex = "0123456789abcdef"
|
||||
name := make([]byte, sha256.Size*2+len(".license"))
|
||||
for index, value := range digest {
|
||||
name[index*2] = hex[value>>4]
|
||||
name[index*2+1] = hex[value&0x0f]
|
||||
}
|
||||
copy(name[sha256.Size*2:], ".license")
|
||||
return string(name)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"softbox.local/core/internal/canonicaljson"
|
||||
"softbox.local/core/licensing"
|
||||
)
|
||||
|
||||
const storageTestMachineHash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
func TestLicenseStoreImportsRevalidatesAndDeduplicates(t *testing.T) {
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifier, err := licensing.NewVerifier(publicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := NewLicenseStore(filepath.Join(t.TempDir(), "licenses"))
|
||||
document := storageSignedLicense(t, privateKey, "lic-storage-test", []string{"product-test"})
|
||||
license, imported, err := store.Import(document, verifier, storageTestMachineHash)
|
||||
if err != nil || !imported || !license.AuthorizesProduct("product-test") {
|
||||
t.Fatalf("Import() = (%#v, %t, %v)", license, imported, err)
|
||||
}
|
||||
if _, imported, err := store.Import(document, verifier, storageTestMachineHash); err != nil || imported {
|
||||
t.Fatalf("duplicate Import() = (%t, %v), want false, nil", imported, err)
|
||||
}
|
||||
licenses, err := store.List(verifier, storageTestMachineHash)
|
||||
if err != nil || len(licenses) != 1 || !licenses[0].AuthorizesProduct("product-test") {
|
||||
t.Fatalf("List() = (%#v, %v)", licenses, err)
|
||||
}
|
||||
licenses[0].Products[0] = "mutated"
|
||||
again, err := store.List(verifier, storageTestMachineHash)
|
||||
if err != nil || !again[0].AuthorizesProduct("product-test") {
|
||||
t.Fatalf("List() after output mutation = (%#v, %v)", again, err)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(filepath.Join(store.root, "v1"))
|
||||
if err != nil || len(entries) != 1 {
|
||||
t.Fatalf("stored entries = %#v, %v", entries, err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(store.root, "v1", entries[0].Name()), []byte("{}"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.List(verifier, storageTestMachineHash); !errors.Is(err, ErrLicenseStoreInvalid) {
|
||||
// List returns the verifier's stable failure, not a partial authorization set.
|
||||
t.Fatalf("List(tampered) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseStoreRejectsDigestNameMismatch(t *testing.T) {
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifier, err := licensing.NewVerifier(publicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := NewLicenseStore(filepath.Join(t.TempDir(), "licenses"))
|
||||
document := storageSignedLicense(t, privateKey, "lic-storage-test", []string{"product-test"})
|
||||
if _, _, err := store.Import(document, verifier, storageTestMachineHash); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
directory := filepath.Join(store.root, "v1")
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil || len(entries) != 1 {
|
||||
t.Fatalf("stored entries = %#v, %v", entries, err)
|
||||
}
|
||||
if err := os.Rename(filepath.Join(directory, entries[0].Name()), filepath.Join(directory, "0000000000000000000000000000000000000000000000000000000000000000.license")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.List(verifier, storageTestMachineHash); !errors.Is(err, ErrLicenseStoreInvalid) {
|
||||
t.Fatalf("List(digest mismatch) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseStoreRevocationCacheIsVerifiedAndAtomic(t *testing.T) {
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifier, err := licensing.NewRevocationVerifier(publicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := NewLicenseStore(filepath.Join(t.TempDir(), "licenses"))
|
||||
generated := time.Date(2026, 7, 20, 0, 0, 0, 0, time.UTC)
|
||||
document := storageSignedRevocations(t, privateKey, generated, generated.Add(time.Hour), []string{"lic-storage-test"})
|
||||
stored, err := store.StoreRevocations(document, verifier)
|
||||
if err != nil || len(stored.RevokedLicenseIDs) != 1 {
|
||||
t.Fatalf("StoreRevocations() = (%#v, %v)", stored, err)
|
||||
}
|
||||
loaded, found, err := store.LoadRevocations(verifier)
|
||||
if err != nil || !found || loaded.RevokedLicenseIDs[0] != "lic-storage-test" {
|
||||
t.Fatalf("LoadRevocations() = (%#v, %t, %v)", loaded, found, err)
|
||||
}
|
||||
if _, err := store.StoreRevocations([]byte("{}"), verifier); !errors.Is(err, licensing.ErrRevocationSignatureMissing) {
|
||||
t.Fatalf("StoreRevocations(invalid) error = %v", err)
|
||||
}
|
||||
again, found, err := store.LoadRevocations(verifier)
|
||||
if err != nil || !found || again.RevokedLicenseIDs[0] != "lic-storage-test" {
|
||||
t.Fatalf("invalid update changed cache: (%#v, %t, %v)", again, found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseStoreRejectsUnsafeCachedEntry(t *testing.T) {
|
||||
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifier, err := licensing.NewVerifier(publicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := filepath.Join(t.TempDir(), "licenses")
|
||||
if err := os.MkdirAll(filepath.Join(root, "v1"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "v1", "unexpected.txt"), []byte("x"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := NewLicenseStore(root).List(verifier, storageTestMachineHash); !errors.Is(err, ErrLicenseStoreUnsafe) {
|
||||
t.Fatalf("List(unsafe entry) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func storageSignedLicense(t *testing.T, privateKey ed25519.PrivateKey, licenseID string, products []string) []byte {
|
||||
t.Helper()
|
||||
payload := map[string]any{
|
||||
"schema_version": json.Number("1"),
|
||||
"license_id": licenseID,
|
||||
"machine_hash": storageTestMachineHash,
|
||||
"products": storageStringSliceAsAny(products),
|
||||
"issued_at": "2026-07-20T00:00:00Z",
|
||||
"perpetual": true,
|
||||
"update_policy": "updates-until-2027-12-31",
|
||||
"rebind_policy": "self-service-1-per-90d",
|
||||
}
|
||||
canonical, err := canonicaljson.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload["signature"] = base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, canonical))
|
||||
document, err := canonicaljson.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return document
|
||||
}
|
||||
|
||||
func storageSignedRevocations(t *testing.T, privateKey ed25519.PrivateKey, generated, expires time.Time, ids []string) []byte {
|
||||
t.Helper()
|
||||
payload := map[string]any{
|
||||
"schema_version": json.Number("1"),
|
||||
"generated_at": generated.Format("2006-01-02T15:04:05Z"),
|
||||
"expires_at": expires.Format("2006-01-02T15:04:05Z"),
|
||||
"revoked_license_ids": storageStringSliceAsAny(ids),
|
||||
}
|
||||
canonical, err := canonicaljson.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload["signature"] = base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, canonical))
|
||||
document, err := canonicaljson.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return document
|
||||
}
|
||||
|
||||
func storageStringSliceAsAny(values []string) []any {
|
||||
items := make([]any, len(values))
|
||||
for index, value := range values {
|
||||
items[index] = value
|
||||
}
|
||||
return items
|
||||
}
|
||||
Reference in New Issue
Block a user