diff --git a/internal/cubecos/license.go b/internal/cubecos/license.go index 6af96103..37c3bd89 100644 --- a/internal/cubecos/license.go +++ b/internal/cubecos/license.go @@ -2,7 +2,6 @@ package cubecos import ( "bufio" - "fmt" "os" "os/exec" "path/filepath" @@ -362,13 +361,25 @@ func checkLicenseErr(err error) error { } func parseLicenseDat(file string) (*licenses.License, error) { - err := unzipLicense(file) + work, err := os.MkdirTemp(filepath.Dir(file), "members") if err != nil { + log.Errorf("licenses: failed to create work dir(%v)", err) return nil, err } - dir, name := getLicenseDirAndName(file) - dat, err := os.Open(filepath.Join(dir, fmt.Sprintf("%s.dat", name))) + defer os.RemoveAll(work) + err = unzipLicense(file, work) + if err != nil { + return nil, err + } + + stem, err := licenseMemberStem(work) + if err != nil { + log.Errorf("licenses: %v(%s)", err, filepath.Base(file)) + return nil, err + } + + dat, err := os.Open(stem + ".dat") if err != nil { return nil, err } @@ -378,14 +389,13 @@ func parseLicenseDat(file string) (*licenses.License, error) { setLicenseDat(dat, license) setLicenseDatStatus( license, - checkImportLicense(file, *license), + checkImportLicense(stem, *license), ) return license, nil } -func unzipLicense(license string) error { - dir, _ := getLicenseDirAndName(license) +func unzipLicense(license string, dir string) error { err := zip.DecompressFromTo(license, dir) if err != nil { log.Errorf("licenses: failed to unzip license(%v)", err) @@ -395,14 +405,33 @@ func unzipLicense(license string) error { return nil } -func checkImportLicense(file string, license licenses.License) error { - dir, file := getLicenseDirAndName(file) +// licenseMemberStem returns the shared path prefix of the archive's .dat/.sig +// pair. The member names need not match the uploaded filename. +func licenseMemberStem(dir string) (string, error) { + dats, err := filepath.Glob(filepath.Join(dir, "*.dat")) + if err != nil { + return "", err + } + + for _, dat := range dats { + stem := strings.TrimSuffix(dat, ".dat") + _, err := os.Stat(stem + ".sig") + if err == nil { + return stem, nil + } + } + + return "", errors.ErrLicenseMalformedArchive +} + +// stem is the member path prefix; hex_config license_check appends .dat/.sig. +func checkImportLicense(stem string, license licenses.License) error { product := "def" if license.Product.Name == licenses.CubeCMP { product = "cmp" } - _, err := exec.Command("hex_config", "license_check", product, filepath.Join(dir, file)).Output() + _, err := exec.Command("hex_config", "license_check", product, stem).Output() return checkLicenseErr(err) } diff --git a/internal/cubecos/license_verify_test.go b/internal/cubecos/license_verify_test.go new file mode 100644 index 00000000..2389d223 --- /dev/null +++ b/internal/cubecos/license_verify_test.go @@ -0,0 +1,121 @@ +package cubecos + +import ( + "archive/zip" + "os" + "path/filepath" + "testing" +) + +// writeLicense builds a .license archive at dir/ whose members carry +// memberStem, which need not match filename. +func writeLicense(t *testing.T, dir, filename, memberStem, dat string) string { + t.Helper() + + path := filepath.Join(dir, filename) + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + + defer f.Close() + zw := zip.NewWriter(f) + for ext, body := range map[string]string{"dat": dat, "sig": "signature-bytes"} { + w, err := zw.Create(memberStem + "." + ext) + if err != nil { + t.Fatal(err) + } + if _, err := w.Write([]byte(body)); err != nil { + t.Fatal(err) + } + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } + + return path +} + +const testDat = "license.name=NYCU ADFP3.0\n" + + "license.type=enterprise\n" + + "issue.hardware=GRRYSF4,DVRYSF4\n" + + "product=CubeCOS\n" + +// Member names need not match the uploaded filename. +func TestParseLicenseDatIgnoresUploadedFilename(t *testing.T) { + dir := t.TempDir() + path := writeLicense(t, dir, "nycu-adfp3-0 (1).license", "nycu-adfp3-0", testDat) + + license, err := parseLicenseDat(path) + if err != nil { + t.Fatalf("parseLicenseDat: %v", err) + } + if license.Name != "NYCU ADFP3.0" { + t.Errorf("Name = %q, want %q", license.Name, "NYCU ADFP3.0") + } + if license.Issue.Hardware != "GRRYSF4,DVRYSF4" { + t.Errorf("Hardware = %q", license.Issue.Hardware) + } +} + +// A stale pair beside the upload must not be picked up. +func TestParseLicenseDatIgnoresStaleMembers(t *testing.T) { + dir := t.TempDir() + for _, ext := range []string{"dat", "sig"} { + stale := filepath.Join(dir, "someone-elses."+ext) + if err := os.WriteFile(stale, []byte("license.name=someone else\n"), 0o600); err != nil { + t.Fatal(err) + } + } + + path := writeLicense(t, dir, "mine.license", "mine", testDat) + license, err := parseLicenseDat(path) + if err != nil { + t.Fatalf("parseLicenseDat: %v", err) + } + if license.Name != "NYCU ADFP3.0" { + t.Errorf("Name = %q, want the uploaded license, not the stale pair", license.Name) + } +} + +// An archive without a .dat/.sig pair is genuinely malformed. +func TestParseLicenseDatRejectsArchiveWithoutPair(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "broken.license") + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + zw := zip.NewWriter(f) + w, err := zw.Create("readme.txt") + if err != nil { + t.Fatal(err) + } + w.Write([]byte("not a license")) + zw.Close() + f.Close() + + if _, err := parseLicenseDat(path); err == nil { + t.Fatal("parseLicenseDat accepted an archive with no .dat/.sig pair") + } +} + +// parseLicenseDat must not leave extracted members behind. +func TestParseLicenseDatCleansUpMembers(t *testing.T) { + dir := t.TempDir() + path := writeLicense(t, dir, "tidy.license", "tidy", testDat) + + if _, err := parseLicenseDat(path); err != nil { + t.Fatal(err) + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + if e.Name() != "tidy.license" { + t.Errorf("left behind %q in the verify dir", e.Name()) + } + } +} diff --git a/internal/definition/v1/errors/errors.go b/internal/definition/v1/errors/errors.go index add60e93..7412a8dc 100644 --- a/internal/definition/v1/errors/errors.go +++ b/internal/definition/v1/errors/errors.go @@ -27,6 +27,7 @@ var ( ErrLicenseInvalidHardware = errors.New("license's hardware serial is not matched with the current system") ErrLicenseInvalidSignature = errors.New("license's signature is invalid") ErrLicenseSystemCompromised = errors.New("license system is compromised") + ErrLicenseMalformedArchive = errors.New("license archive holds no .dat/.sig pair") ErrSdkExecutionFailure = errors.New("sdk execution error") ErrUnknownSettingType = errors.New("unknown setting type") ErrInvalidListenAddress = errors.New("invalid listen address")