Implement authorization import and revocation checks (T-503)

This commit is contained in:
ila
2026-07-20 09:07:30 +08:00
parent b4453130de
commit 76f6108496
36 changed files with 2197 additions and 68 deletions
+123
View File
@@ -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
}