package licensing import ( "bytes" "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "os" "path/filepath" "reflect" "strings" "testing" "softbox.local/core/internal/canonicaljson" ) type licenseVectorCorpus struct { SchemaVersion int `json:"schema_version"` PublicKeyBase64 string `json:"public_key_base64"` Vectors []licenseVector `json:"vectors"` } type licenseVector struct { Name string `json:"name"` Document string `json:"document"` SignedPayloadBase64 string `json:"signed_payload_base64"` Signature string `json:"signature"` ExpectedMachineHash string `json:"expected_machine_hash"` ExpectedProducts []string `json:"expected_products"` WantError string `json:"want_error"` } func TestVerifierStaticCorpus(t *testing.T) { corpus := readLicenseVectorCorpus(t) publicKey := corpusPublicKey(t, corpus) verifier, err := NewVerifier(publicKey) if err != nil { t.Fatalf("NewVerifier() error = %v", err) } valid := corpusValidVector(t, corpus) for _, vector := range corpus.Vectors { vector := vector t.Run(vector.Name, func(t *testing.T) { license, err := verifier.Verify([]byte(vector.Document), valid.ExpectedMachineHash) if vector.WantError != "" { want := licenseVectorError(t, vector.WantError) if !errors.Is(err, want) { t.Fatalf("Verify() error = %v, want %v", err, want) } return } if err != nil { t.Fatalf("Verify() error = %v", err) } if !reflect.DeepEqual(license.Products, vector.ExpectedProducts) { t.Fatalf("Products = %q, want %q", license.Products, vector.ExpectedProducts) } if !license.AuthorizesProduct("product-json-parser") || license.AuthorizesProduct("other-product") { t.Fatal("AuthorizesProduct() did not preserve the verified product set") } }) } expectedPayload, err := base64.StdEncoding.DecodeString(valid.SignedPayloadBase64) if err != nil { t.Fatalf("decode signed payload: %v", err) } payload := canonicalLicensePayload(t, []byte(valid.Document)) if !bytes.Equal(payload, expectedPayload) { t.Fatalf("canonical signing bytes = %q, want static corpus %q", payload, expectedPayload) } signature, err := base64.StdEncoding.DecodeString(valid.Signature) if err != nil { t.Fatalf("decode signature: %v", err) } if !ed25519.Verify(publicKey, expectedPayload, signature) { t.Fatal("static public key does not verify static signing bytes") } } func TestVerifierCanonicalWhitespaceAndKeyOrder(t *testing.T) { corpus := readLicenseVectorCorpus(t) publicKey := corpusPublicKey(t, corpus) valid := corpusValidVector(t, corpus) var wire licenseWire if err := json.Unmarshal([]byte(valid.Document), &wire); err != nil { t.Fatalf("decode static document: %v", err) } equivalent := ` { "update_policy" : "` + wire.UpdatePolicy + `", "signature" : "` + wire.Signature + `", "schema_version" : 1, "rebind_policy" : "` + wire.RebindPolicy + `", "products" : [ "` + wire.Products[0] + `", "` + wire.Products[1] + `" ], "perpetual" : true, "machine_hash" : "` + wire.MachineHash + `", "license_id" : "` + wire.LicenseID + `", "issued_at" : "` + wire.IssuedAt + `" } ` verifier, err := NewVerifier(publicKey) if err != nil { t.Fatalf("NewVerifier() error = %v", err) } license, err := verifier.Verify([]byte(equivalent), valid.ExpectedMachineHash) if err != nil { t.Fatalf("Verify(equivalent) error = %v", err) } if !reflect.DeepEqual(license.Products, valid.ExpectedProducts) { t.Fatalf("equivalent products = %q", license.Products) } } func TestVerifierFailsClosed(t *testing.T) { corpus := readLicenseVectorCorpus(t) publicKey := corpusPublicKey(t, corpus) valid := corpusValidVector(t, corpus) verifier, err := NewVerifier(publicKey) if err != nil { t.Fatalf("NewVerifier() error = %v", err) } tests := []struct { name string document []byte hash string wantErr error }{ {name: "wrong machine", document: []byte(valid.Document), hash: strings.Repeat("f", 64), wantErr: ErrMachineMismatch}, {name: "invalid expected hash", document: []byte(valid.Document), hash: strings.ToUpper(valid.ExpectedMachineHash), wantErr: ErrMachineHashInvalid}, {name: "tampered", document: bytes.Replace([]byte(valid.Document), []byte("toolkit"), []byte("toolkitz"), 1), hash: valid.ExpectedMachineHash, wantErr: ErrSignatureInvalid}, {name: "duplicate field", document: []byte(`{"schema_version":1,"schema_version":1,"signature":"x"}`), hash: valid.ExpectedMachineHash, wantErr: ErrDuplicateField}, {name: "fractional number", document: []byte(`{"schema_version":1.5,"signature":"x"}`), hash: valid.ExpectedMachineHash, wantErr: ErrUnsupportedNumber}, {name: "invalid signature encoding", document: []byte(strings.Replace(valid.Document, `=="`, `"`, 1)), hash: valid.ExpectedMachineHash, wantErr: ErrSignatureInvalid}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := verifier.Verify(test.document, test.hash) if !errors.Is(err, test.wantErr) { t.Fatalf("Verify() error = %v, want %v", err, test.wantErr) } }) } } func TestVerifierCopiesPublicKeyAndProducts(t *testing.T) { corpus := readLicenseVectorCorpus(t) publicKey := corpusPublicKey(t, corpus) valid := corpusValidVector(t, corpus) verifier, err := NewVerifier(publicKey) if err != nil { t.Fatalf("NewVerifier() error = %v", err) } publicKey[0] ^= 0xff first, err := verifier.Verify([]byte(valid.Document), valid.ExpectedMachineHash) if err != nil { t.Fatalf("Verify(first) error = %v", err) } second, err := verifier.Verify([]byte(valid.Document), valid.ExpectedMachineHash) if err != nil { t.Fatalf("Verify(second) error = %v", err) } first.Products[0] = "changed" if !second.AuthorizesProduct("product-json-parser") || second.AuthorizesProduct("changed") { t.Fatal("Verify() returned products sharing mutable state") } if _, err := NewVerifier(make([]byte, ed25519.PublicKeySize-1)); !errors.Is(err, ErrPublicKeyInvalid) { t.Fatalf("NewVerifier(short key) error = %v, want ErrPublicKeyInvalid", err) } var unconfigured Verifier if _, err := unconfigured.Verify([]byte(valid.Document), valid.ExpectedMachineHash); !errors.Is(err, ErrPublicKeyInvalid) { t.Fatalf("zero Verifier.Verify() error = %v, want ErrPublicKeyInvalid", err) } } func readLicenseVectorCorpus(t *testing.T) licenseVectorCorpus { t.Helper() path := filepath.Join("..", "..", "testdata", "license", "license-v1-vectors.json") data, err := os.ReadFile(path) if err != nil { t.Fatalf("read license vector corpus: %v", err) } var corpus licenseVectorCorpus decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() if err := decoder.Decode(&corpus); err != nil { t.Fatalf("decode license vector corpus: %v", err) } if corpus.SchemaVersion != 1 || len(corpus.Vectors) == 0 { t.Fatal("license vector corpus is incomplete") } return corpus } func corpusPublicKey(t *testing.T, corpus licenseVectorCorpus) ed25519.PublicKey { t.Helper() publicKey, err := base64.StdEncoding.DecodeString(corpus.PublicKeyBase64) if err != nil || len(publicKey) != ed25519.PublicKeySize { t.Fatal("license vector corpus has an invalid public key") } return ed25519.PublicKey(publicKey) } func corpusValidVector(t *testing.T, corpus licenseVectorCorpus) licenseVector { t.Helper() for _, vector := range corpus.Vectors { if vector.Name == "valid" { return vector } } t.Fatal("license vector corpus has no valid vector") return licenseVector{} } func licenseVectorError(t *testing.T, value string) error { t.Helper() switch value { case "invalid_license": return ErrInvalidLicense default: t.Fatalf("unsupported license vector want_error %q", value) return nil } } func canonicalLicensePayload(t *testing.T, document []byte) []byte { t.Helper() value, err := canonicaljson.Parse(document) if err != nil { t.Fatalf("parse license document: %v", err) } root, ok := value.(map[string]any) if !ok { t.Fatal("license document root is not an object") } delete(root, "signature") payload, err := canonicaljson.Marshal(root) if err != nil { t.Fatalf("canonicalize license document: %v", err) } return payload }