124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
package licensing
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"softbox.local/core/internal/canonicaljson"
|
|
)
|
|
|
|
func TestRevocationVerifierAndStateBoundaries(t *testing.T) {
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
verifier, err := NewRevocationVerifier(publicKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
generated := time.Date(2026, 7, 20, 0, 0, 0, 0, time.UTC)
|
|
document := signedRevocationDocument(t, privateKey, generated, generated.Add(24*time.Hour), []string{"lic-revoked"})
|
|
list, err := verifier.Verify(document)
|
|
if err != nil {
|
|
t.Fatalf("Verify() error = %v", err)
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
licenseID string
|
|
now time.Time
|
|
want RevocationState
|
|
}{
|
|
{"current", "lic-active", generated.Add(time.Hour), RevocationStateCurrent},
|
|
{"revoked", "lic-revoked", generated.Add(time.Hour), RevocationStateRevoked},
|
|
{"at expiry", "lic-active", generated.Add(24 * time.Hour), RevocationStateCurrent},
|
|
{"at grace end", "lic-active", generated.Add(24*time.Hour + RevocationGrace), RevocationStateGrace},
|
|
{"after grace", "lic-active", generated.Add(24*time.Hour + RevocationGrace + time.Second), RevocationStateUnavailable},
|
|
{"before generated", "lic-active", generated.Add(-time.Second), RevocationStateUnavailable},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if got := list.StateFor(test.licenseID, test.now); got != test.want {
|
|
t.Fatalf("StateFor() = %q, want %q", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
list.RevokedLicenseIDs[0] = "lic-mutated"
|
|
if again, err := verifier.Verify(document); err != nil || again.RevokedLicenseIDs[0] != "lic-revoked" {
|
|
t.Fatalf("Verify() after output mutation = %#v, %v", again, err)
|
|
}
|
|
}
|
|
|
|
func TestRevocationVerifierFailsClosed(t *testing.T) {
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
verifier, err := NewRevocationVerifier(publicKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
generated := time.Date(2026, 7, 20, 0, 0, 0, 0, time.UTC)
|
|
valid := signedRevocationDocument(t, privateKey, generated, generated.Add(time.Hour), nil)
|
|
tampered := append([]byte(nil), valid...)
|
|
tampered[5] = 'x'
|
|
tooLong := signedRevocationDocument(t, privateKey, generated, generated.Add(MaxRevocationValidity+time.Second), nil)
|
|
for _, test := range []struct {
|
|
name string
|
|
document []byte
|
|
want error
|
|
}{
|
|
{"empty", nil, ErrInvalidRevocation},
|
|
{"tampered", tampered, ErrRevocationSignatureInvalid},
|
|
{"interval too long", tooLong, ErrInvalidRevocation},
|
|
{"duplicate", []byte(`{"schema_version":1,"schema_version":1}`), ErrDuplicateField},
|
|
{"missing signature", []byte(`{"schema_version":1,"generated_at":"2026-07-20T00:00:00Z","expires_at":"2026-07-20T01:00:00Z","revoked_license_ids":[]}`), ErrRevocationSignatureMissing},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, err := verifier.Verify(test.document); !errors.Is(err, test.want) {
|
|
t.Fatalf("Verify() error = %v, want %v", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
if _, err := NewRevocationVerifier(make([]byte, ed25519.PublicKeySize-1)); !errors.Is(err, ErrPublicKeyInvalid) {
|
|
t.Fatalf("NewRevocationVerifier(short) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func signedRevocationDocument(
|
|
t *testing.T,
|
|
privateKey ed25519.PrivateKey,
|
|
generated time.Time,
|
|
expires time.Time,
|
|
revoked []string,
|
|
) []byte {
|
|
t.Helper()
|
|
payload := map[string]any{
|
|
"schema_version": json.Number("1"),
|
|
"generated_at": generated.Format(timestampLayout),
|
|
"expires_at": expires.Format(timestampLayout),
|
|
"revoked_license_ids": stringSliceAsAny(revoked),
|
|
}
|
|
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 stringSliceAsAny(values []string) []any {
|
|
items := make([]any, len(values))
|
|
for index, value := range values {
|
|
items[index] = value
|
|
}
|
|
return items
|
|
}
|