Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions malwaredb/feed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ func coseWrap(payload []byte) []byte {
return append(encodeUint(6, 18), arr...)
}

func useUnsignedFeedHead(t *testing.T) {
t.Helper()
old := verifyFeedHead
verifyFeedHead = ExtractCOSEPayload
t.Cleanup(func() { verifyFeedHead = old })
}

func TestUpdateFromSnapshot(t *testing.T) {
useUnsignedFeedHead(t)
entries := []any{[]any{"left-pad", []any{0}}}
snap, err := encodeCBOR(map[int]any{3: entries})
if err != nil {
Expand Down Expand Up @@ -95,4 +103,25 @@ func TestUpdateFromSnapshot(t *testing.T) {
if _, err := os.Stat(filepath.Join(root, "malware.db")); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(root, HeadFileName)); err != nil {
t.Fatal(err)
}
}

func TestUpdateRejectsUnsignedHead(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/v1/head.cose", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(coseWrap([]byte{0xa1, 0x01, 0x01}))
})
srv := httptest.NewServer(mux)
defer srv.Close()
_, err := Update(UpdateOptions{
Root: t.TempDir(),
BaseURL: srv.URL,
License: "tmp",
Now: time.Unix(100, 0),
})
if err == nil {
t.Fatal("unsigned head must fail VerifySign1")
}
}
2 changes: 1 addition & 1 deletion malwaredb/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ replace common/token v1.0.0 => ../token
replace common/urlguard v1.0.0 => ../urlguard

require (
common/cose v1.0.0
common/mirrorauth v1.0.0
common/settings v1.0.0
common/token v1.0.0
github.com/Masterminds/semver/v3 v3.4.0
)

require (
common/cose v1.0.0 // indirect
common/fs v1.0.0 // indirect
common/modulefirewall v1.0.0 // indirect
common/preferences v1.0.0 // indirect
Expand Down
85 changes: 85 additions & 0 deletions malwaredb/trust.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package malwaredb

import (
"fmt"
"io"
"os"
"path/filepath"
)

// PairTrusted reports whether root holds a malware.db whose state_hash matches
// a COSE-verified head.cose beside it.
func PairTrusted(root string) error {
raw, err := os.ReadFile(filepath.Join(root, DBFileName))
if err != nil {
return err
}
db, err := Parse(raw)
if err != nil {
return err
}
headRaw, err := os.ReadFile(filepath.Join(root, HeadFileName))
if err != nil {
return err
}
payload, err := verifyFeedHead(headRaw)
if err != nil {
return err
}
head, err := ParseHead(payload)
if err != nil {
return err
}
if err := db.CheckState(head.StateHash); err != nil {
return err
}
return nil
}

// CopySeed copies malware.db and head.cose from srcDir into destRoot when dest
// does not already hold a trusted pair. It refuses to overwrite a trusted live pair.
func CopySeed(srcDir, destRoot string) error {
if err := PairTrusted(destRoot); err == nil {
return nil
}
if err := os.MkdirAll(destRoot, 0o755); err != nil {
return err
}
for _, name := range []string{HeadFileName, DBFileName} {
if err := copyFile(filepath.Join(srcDir, name), filepath.Join(destRoot, name)); err != nil {
return fmt.Errorf("%s: %w", name, err)
}
}
return PairTrusted(destRoot)
}

func copyFile(src, dest string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return err
}
tmp := dest + ".tmp"
out, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
out.Close()
_ = os.Remove(tmp)
return err
}
if err := out.Sync(); err != nil {
out.Close()
_ = os.Remove(tmp)
return err
}
if err := out.Close(); err != nil {
_ = os.Remove(tmp)
return err
}
return os.Rename(tmp, dest)
}
91 changes: 91 additions & 0 deletions malwaredb/trust_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package malwaredb

import (
"os"
"path/filepath"
"testing"
)

func writeTrustedPair(t *testing.T, dir string, state [32]byte) {
t.Helper()
headPayload, err := encodeCBOR(map[int]any{
2: 1,
3: 100,
4: 100 + 72*3600,
5: state[:],
6: []any{1, make([]byte, 32), 1},
7: []any{},
})
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, HeadFileName), coseWrap(headPayload), 0o644); err != nil {
t.Fatal(err)
}
encoded, err := Encode([]Entry{{Name: "left-pad", Kind: KindAll}}, 1, 100, state)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, DBFileName), encoded, 0o644); err != nil {
t.Fatal(err)
}
}

func TestCopySeedSkipsTrustedDest(t *testing.T) {
useUnsignedFeedHead(t)
var state [32]byte
state[0] = 9
src := t.TempDir()
dest := t.TempDir()
writeTrustedPair(t, src, state)
writeTrustedPair(t, dest, state)
if err := CopySeed(src, dest); err != nil {
t.Fatal(err)
}
}

func TestCopySeedFillsMissingDest(t *testing.T) {
useUnsignedFeedHead(t)
var state [32]byte
state[0] = 9
src := t.TempDir()
dest := t.TempDir()
writeTrustedPair(t, src, state)
if err := CopySeed(src, dest); err != nil {
t.Fatal(err)
}
if err := PairTrusted(dest); err != nil {
t.Fatal(err)
}
}

func TestPairTrustedRejectsStateMismatch(t *testing.T) {
useUnsignedFeedHead(t)
root := t.TempDir()
var state [32]byte
state[0] = 9
headPayload, err := encodeCBOR(map[int]any{
2: 1,
3: 100,
4: 100 + 72*3600,
5: make([]byte, 32),
6: []any{1, make([]byte, 32), 1},
7: []any{},
})
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, HeadFileName), coseWrap(headPayload), 0o644); err != nil {
t.Fatal(err)
}
encoded, err := Encode([]Entry{{Name: "left-pad", Kind: KindAll}}, 1, 100, state)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, DBFileName), encoded, 0o644); err != nil {
t.Fatal(err)
}
if err := PairTrusted(root); err == nil {
t.Fatal("mismatched state_hash must fail")
}
}
31 changes: 28 additions & 3 deletions malwaredb/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package malwaredb
import (
"bytes"
"compress/gzip"
commoncose "common/cose"
"fmt"
"io"
"net/http"
Expand All @@ -11,7 +12,24 @@ import (
"time"
)

const DefaultFeedBase = "https://cdn.author.io/metadata/nvm-windows/malicious_packages"
const (
DefaultFeedBase = "https://cdn.author.io/metadata/nvm-windows/malicious_packages"
DBFileName = "malware.db"
HeadFileName = "head.cose"
FeedSignerOrg = "Author Software Inc."
)

// verifyFeedHead verifies a COSE_Sign1 malware feed head and returns the payload.
// Tests replace this with ExtractCOSEPayload for unsigned fixtures.
var verifyFeedHead = verifyFeedHeadCOSE

func verifyFeedHeadCOSE(raw []byte) ([]byte, error) {
payload, err := commoncose.VerifySign1(raw, []string{FeedSignerOrg})
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrFeed, err)
}
return payload, nil
}

type UpdateOptions struct {
Root string
Expand Down Expand Up @@ -40,7 +58,8 @@ func Update(opts UpdateOptions) (UpdateResult, error) {
if opts.Now.IsZero() {
opts.Now = time.Now()
}
dbPath := filepath.Join(opts.Root, "malware.db")
dbPath := filepath.Join(opts.Root, DBFileName)
headPath := filepath.Join(opts.Root, HeadFileName)
if opts.Background {
if _, err := os.Stat(dbPath); err == nil {
time.Sleep(time.Duration(time.Now().UnixNano() % int64(15*time.Minute)))
Expand All @@ -64,7 +83,7 @@ func Update(opts UpdateOptions) (UpdateResult, error) {
if err != nil {
return UpdateResult{}, err
}
payload, err := ExtractCOSEPayload(headBody)
payload, err := verifyFeedHead(headBody)
if err != nil {
return UpdateResult{}, err
}
Expand All @@ -74,6 +93,9 @@ func Update(opts UpdateOptions) (UpdateResult, error) {
}
result := UpdateResult{Seq: head.Seq, PreviousSeq: previousSeq}
if previousSeq == head.Seq && previousHash == head.StateHash {
if _, err := os.Stat(headPath); err != nil {
_ = writeDB(headPath, headBody)
}
return result, nil
}
if head.Expires > 0 && opts.Now.Unix() >= int64(head.Expires) && previousSeq > 0 {
Expand Down Expand Up @@ -129,6 +151,9 @@ func Update(opts UpdateOptions) (UpdateResult, error) {
if err != nil {
return UpdateResult{}, err
}
if err := writeDB(headPath, headBody); err != nil {
return UpdateResult{}, err
}
if err := writeDB(dbPath, encoded); err != nil {
return UpdateResult{}, err
}
Expand Down
Loading