Implement authorization import and revocation checks (T-503)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user