diff options
Diffstat (limited to 'verify')
-rw-r--r-- | verify/cmd/root.go | 22 | ||||
-rw-r--r-- | verify/cmd/verify.go | 174 | ||||
-rw-r--r-- | verify/config/config.go | 15 | ||||
-rw-r--r-- | verify/core/crypt.go | 27 | ||||
-rw-r--r-- | verify/core/storage.go | 246 | ||||
-rw-r--r-- | verify/core/verification.go | 165 | ||||
-rwxr-xr-x | verify/fsverify | bin | 0 -> 6109315 bytes | |||
-rw-r--r-- | verify/go.mod | 18 | ||||
-rw-r--r-- | verify/go.sum | 29 | ||||
-rw-r--r-- | verify/main.go | 13 |
10 files changed, 709 insertions, 0 deletions
diff --git a/verify/cmd/root.go b/verify/cmd/root.go new file mode 100644 index 0000000..d212e4e --- /dev/null +++ b/verify/cmd/root.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "os" +) + +var rootCmd = &cobra.Command{ + Use: "fsverify", +} + +func init() { + rootCmd.AddCommand(NewVerifyCommand()) +} + +func Execute() { + // cobra does not exit with a non-zero return code when failing + // solution from https://github.com/spf13/cobra/issues/221 + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/verify/cmd/verify.go b/verify/cmd/verify.go new file mode 100644 index 0000000..d0360f6 --- /dev/null +++ b/verify/cmd/verify.go @@ -0,0 +1,174 @@ +package cmd + +import ( + "bytes" + "fmt" + "math" + "os" + "sync" + + "github.com/axtloss/fsverify/config" + "github.com/axtloss/fsverify/core" + "github.com/spf13/cobra" +) + +var validateFailed bool + +func NewVerifyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "verify", + Short: "Verify the root filesystem based on the given verification", + RunE: ValidateCommand, + SilenceUsage: true, + } + + return cmd +} + +// validateThread validates a chain of nodes against a given byte slice +func validateThread(blockStart int, blockEnd int, bundleSize int, diskBytes []byte, n int, dbfile string, waitGroup *sync.WaitGroup, errChan chan error) { + defer waitGroup.Done() + defer close(errChan) + var reader *bytes.Reader + blockCount := math.Floor(float64(bundleSize / 2000)) + totalReadBlocks := 0 + + db, err := core.OpenDB(dbfile, true) + if err != nil { + errChan <- err + } + + reader = bytes.NewReader(diskBytes) + + node, err := core.GetNode(fmt.Sprintf("Entrypoint%d", n), db) + if err != nil { + errChan <- err + } + block, i, err := core.ReadBlock(node, reader, totalReadBlocks) + totalReadBlocks = i + + err = core.VerifyBlock(block, node) + if err != nil { + errChan <- err + } + + for int64(totalReadBlocks) < int64(blockCount) { + if validateFailed { + return + } + nodeSum, err := node.GetHash() + if err != nil { + fmt.Println("Using node ", nodeSum) + errChan <- err + } + node, err = core.GetNode(nodeSum, db) + if err != nil { + fmt.Println("Failed to get next node") + errChan <- err + } + part, i, err := core.ReadBlock(node, reader, totalReadBlocks) + totalReadBlocks = i + if err != nil { + errChan <- err + validateFailed = true + return + } + err = core.VerifyBlock(part, node) + if err != nil { + errChan <- err + validateFailed = true + return + } + + } + +} + +func ValidateCommand(_ *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("Usage: fsverify verify [disk]") + } + header, err := core.ReadHeader(config.FsVerifyPart) + if err != nil { + return err + } + + // Check if the partition is even correct + // this does not check if the partition has been tampered with + // it only checks if the specified partition is even an fsverify partition + if header.MagicNumber != 0xACAB { + return fmt.Errorf("sanity bit does not match. Expected %d, got %d", 0xACAB, header.MagicNumber) + } + + fmt.Println("Reading DB") + dbfile, err := core.ReadDB(config.FsVerifyPart) + if err != nil { + return err + } + key, err := core.ReadKey() + if err != nil { + return err + } + fmt.Println("Key: " + key) + verified, err := core.VerifySignature(key, header.Signature, dbfile) + if err != nil { + return err + } else if !verified { + return fmt.Errorf("Signature verification failed\n") + } else { + fmt.Println("Signature verification success!") + } + + fmt.Println("----") + disk, err := os.Open(args[0]) + if err != nil { + return err + } + defer disk.Close() + diskInfo, err := disk.Stat() + if err != nil { + return err + } + diskSize := diskInfo.Size() + + // If the filesystem size has increased ever since the fsverify partition was created + // it would mean that fsverify is not able to verify the entire partition, making it useless + if header.FilesystemSize*header.FilesystemUnit != int(diskSize) { + return fmt.Errorf("disk size does not match disk size specified in header. Expected %d, got %d", header.FilesystemSize*header.FilesystemUnit, diskSize) + } + + bundleSize := math.Floor(float64(diskSize / int64(config.ProcCount))) + diskBytes := make([]byte, diskSize) + _, err = disk.Read(diskBytes) + if err != nil { + return err + } + reader := bytes.NewReader(diskBytes) + var waitGroup sync.WaitGroup + errChan := make(chan error) + validateFailed = false + for i := 0; i < config.ProcCount; i++ { + // To ensure that each thread only uses the byte area it is meant to use, a copy of the + // area is made + diskBytes, err := core.CopyByteArea(i*(int(bundleSize)), (i+1)*(int(bundleSize)), reader) + if err != nil { + fmt.Println("Failed to copy byte area ", i*int(bundleSize), " ", (i+1)+int(bundleSize)) + return err + } + waitGroup.Add(1) + go validateThread(i*int(bundleSize), (i+1)*int(bundleSize), int(bundleSize), diskBytes, i, dbfile, &waitGroup, errChan) + } + + go func() { + waitGroup.Wait() + close(errChan) + }() + + for err := range errChan { + if err != nil { + return err + } + } + + return nil +} diff --git a/verify/config/config.go b/verify/config/config.go new file mode 100644 index 0000000..965243d --- /dev/null +++ b/verify/config/config.go @@ -0,0 +1,15 @@ +package config + +// How the public key is stored +// 0: external file, 1: external storage device, 2: tpm2, 3: usb serial +var KeyStore = 3 + +// Where the public key is stored, only applies for KeyStore = 0, 1 and 3 +var KeyLocation = "/dev/ttyACM1" + +// The amount of threads the DB was created with, has to be the amount of processes +// verifysetup was set to use +var ProcCount = 12 + +// Which partition/file to use as the fsverify partition +var FsVerifyPart = "./verifysetup/part.fsverify" diff --git a/verify/core/crypt.go b/verify/core/crypt.go new file mode 100644 index 0000000..067a0b3 --- /dev/null +++ b/verify/core/crypt.go @@ -0,0 +1,27 @@ +package core + +import ( + "bytes" + "crypto/sha1" + "fmt" + "io" + "strings" +) + +// calculateStringHash calculates the sha1 checksum of a given string a. +func calculateStringHash(a string) (string, error) { + hash := sha1.New() + hash.Write([]byte(a)) + hashInBytes := hash.Sum(nil)[:20] + return strings.TrimSpace(fmt.Sprintf("%x", hashInBytes)), nil +} + +// CalculateBlockHash calculates the sha1 checksum of a given byte slice b. +func CalculateBlockHash(b []byte) (string, error) { + hash := sha1.New() + if _, err := io.Copy(hash, bytes.NewReader(b)); err != nil { + return "", err + } + hashInBytes := hash.Sum(nil)[:20] + return strings.TrimSpace(fmt.Sprintf("%x", hashInBytes)), nil +} diff --git a/verify/core/storage.go b/verify/core/storage.go new file mode 100644 index 0000000..182e1ec --- /dev/null +++ b/verify/core/storage.go @@ -0,0 +1,246 @@ +package core + +import ( + "bufio" + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + bolt "go.etcd.io/bbolt" + "io" + "os" +) + +// Header contains all information stored in the header of a fsverify partition. +type Header struct { + MagicNumber int + Signature string + FilesystemSize int + FilesystemUnit int + TableSize int + TableUnit int +} + +// Node contains all information stored in a database node. +// If the Node is the first node in the database, PrevNodeSum should be set to Entrypoint. +type Node struct { + BlockStart int + BlockEnd int + BlockSum string + PrevNodeSum string +} + +// GetHash returns the hash of all fields of a Node combined. +// The Node fields are combined in the order BlockStart, BlockEnd, BlockSum and PrevNodeSum +func (n *Node) GetHash() (string, error) { + return calculateStringHash(fmt.Sprintf("%d%d%s%s", n.BlockStart, n.BlockEnd, n.BlockSum, n.PrevNodeSum)) +} + +// parseUnitSpec parses the file size unit specified in the header and returns it as an according multiplier. +// In the case of an invalid Unit byte the function returns -1. +func parseUnitSpec(size []byte) int { + switch size[0] { + case 0: + return 1 + case 1: + return 1000 + case 2: + return 1000 * 1000 + case 3: + return 1000 * 1000 * 10000 + case 4: + return 100000000000000 + case 5: + return 1000000000000000 + default: + return -1 + } +} + +// ReadHeader reads the partition header and puts it in a variable of type Header. +// If any field fails to be read, the function returns an empty Header struct and the error. +func ReadHeader(partition string) (Header, error) { + _, exist := os.Stat(partition) + if os.IsNotExist(exist) { + return Header{}, fmt.Errorf("Cannot find partition %s", partition) + } + part, err := os.Open(partition) + if err != nil { + return Header{}, err + } + defer part.Close() + + header := Header{} + reader := bufio.NewReader(part) + // Since the size of each field is already known + // it is best to hard code them, in the case + // that a field goes over its allocated size + // fsverify should (and will) fail + MagicNumber := make([]byte, 2) + UntrustedHash := make([]byte, 100) + TrustedHash := make([]byte, 88) + FilesystemSize := make([]byte, 4) + FilesystemUnit := make([]byte, 1) + TableSize := make([]byte, 4) + TableUnit := make([]byte, 1) + + _, err = reader.Read(MagicNumber) + MagicNum := binary.BigEndian.Uint16(MagicNumber) + if MagicNum != 0xACAB { // The Silliest of magic numbers + return Header{}, err + } + header.MagicNumber = int(MagicNum) + + _, err = reader.Read(UntrustedHash) + if err != nil { + return Header{}, err + } + _, err = reader.Read(TrustedHash) + if err != nil { + return Header{}, err + } + _, err = reader.Read(FilesystemSize) + if err != nil { + return Header{}, err + } + _, err = reader.Read(FilesystemUnit) + if err != nil { + return Header{}, err + } + _, err = reader.Read(TableSize) + if err != nil { + return Header{}, err + } + _, err = reader.Read(TableUnit) + if err != nil { + return Header{}, err + } + + header.Signature = fmt.Sprintf("untrusted comment: fsverify\n%s\ntrusted comment: fsverify\n%s\n", string(UntrustedHash), string(TrustedHash)) + header.FilesystemSize = int(binary.BigEndian.Uint32(FilesystemSize)) + header.TableSize = int(binary.BigEndian.Uint32(TableSize)) + header.FilesystemUnit = parseUnitSpec(FilesystemUnit) + header.TableUnit = parseUnitSpec(TableUnit) + if header.FilesystemUnit == -1 || header.TableUnit == -1 { + return Header{}, fmt.Errorf("unit size for Filesystem or Table invalid: fs: %x, table: %x", FilesystemUnit, TableUnit) + } + return header, nil +} + +// ReadDB reads the database from a fsverify partition. +// It verifies the the size of the database with the size specified in the partition header and returns an error if the sizes do not match. +// Due to limitations with bbolt the database gets written to a temporary path and the function returns the path to the database. +func ReadDB(partition string) (string, error) { + _, exist := os.Stat(partition) + if os.IsNotExist(exist) { + return "", fmt.Errorf("Cannot find partition %s", partition) + } + part, err := os.Open(partition) + if err != nil { + return "", err + } + defer part.Close() + reader := bufio.NewReader(part) + + // The area taken up by the header + // it is useless for this reader instance + // and will be skipped completely + _, err = reader.Read(make([]byte, 200)) + if err != nil { + fmt.Println(err) + return "", err + } + + header, err := ReadHeader(partition) + if err != nil { + fmt.Println(err) + return "", err + } + + // Reading the specified table size allows for tamper protection + // in the case that the partition was tampered with "lazily" + // meaning that only the database was modified, and not the header + // if that is the case, the database would be lacking data, making it unusable + db := make([]byte, header.TableSize*header.TableUnit) + n, err := io.ReadFull(reader, db) + if err != nil { + return "", err + } + if n != header.TableSize*header.TableUnit { + return "", fmt.Errorf("Database is not expected size. Expected %d, got %d", header.TableSize*header.TableUnit, n) + } + fmt.Printf("db: %d\n", n) + + // Write the database to a temporary directory + // to ensure that it disappears after the next reboot + temp, err := os.MkdirTemp("", "*-fsverify") + if err != nil { + return "", err + } + + // The file permission is immediately set to 0700 + // this ensures that the database is not modified + // after it has been written + err = os.WriteFile(temp+"/verify.db", db, 0700) + if err != nil { + return "", err + } + + return temp + "/verify.db", nil +} + +// OpenDB opens a bbolt database and returns a bbolt instance. +func OpenDB(dbpath string, readonly bool) (*bolt.DB, error) { + _, exist := os.Stat(dbpath) + if os.IsNotExist(exist) { + os.Create(dbpath) + } + db, err := bolt.Open(dbpath, 0777, &bolt.Options{ReadOnly: readonly}) + if err != nil { + return nil, err + } + return db, nil +} + +// GetNode retrieves a Node from the database based on the hash identifier. +// If db is set to nil, the function will open the database in read-only mode itself. +func GetNode(checksum string, db *bolt.DB) (Node, error) { + var err error + var deferDB bool + if db == nil { + db, err = OpenDB("my.db", true) + if err != nil { + return Node{}, err + } + deferDB = true + } + var node Node + err = db.View(func(tx *bolt.Tx) error { + nodes := tx.Bucket([]byte("Nodes")) + app := nodes.Get([]byte(checksum)) + err := json.Unmarshal(app, &node) + return err + }) + if deferDB { + defer db.Close() + } + return node, err +} + +// CopyByteArea copies an area of bytes from a reader. +// It verifies that the reader reads the wanted amount of bytes, and returns an error if this is not the case. +func CopyByteArea(start int, end int, reader *bytes.Reader) ([]byte, error) { + if end-start < 0 { + return []byte{}, fmt.Errorf("tried creating byte slice with negative length. %d to %d total %d\n", start, end, end-start) + } else if end-start > 2000 { + return []byte{}, fmt.Errorf("tried creating byte slice with length over 2000. %d to %d total %d\n", start, end, end-start) + } + bytes := make([]byte, end-start) + n, err := reader.ReadAt(bytes, int64(start)) + if err != nil { + return nil, err + } else if n != end-start { + return nil, fmt.Errorf("Unable to read requested size. Expected %d, got %d", end-start, n) + } + return bytes, nil +} diff --git a/verify/core/verification.go b/verify/core/verification.go new file mode 100644 index 0000000..289ce1e --- /dev/null +++ b/verify/core/verification.go @@ -0,0 +1,165 @@ +package core + +import ( + "bufio" + "bytes" + "fmt" + "os" + "strings" + + "aead.dev/minisign" + "github.com/axtloss/fsverify/config" + "github.com/tarm/serial" +) + +// fileReadKey reads the public minisign key from a file specified in config.KeyLocation. +func fileReadKey() (string, error) { + if _, err := os.Stat(config.KeyLocation); os.IsNotExist(err) { + return "", fmt.Errorf("Key location %s does not exist", config.KeyLocation) + } + file, err := os.Open(config.KeyLocation) + if err != nil { + return "", err + } + defer file.Close() + // A public key is never longer than 56 bytes + key := make([]byte, 56) + reader := bufio.NewReader(file) + n, err := reader.Read(key) + if n != 56 { + return "", fmt.Errorf("Key does not match expected key size. Expected 56, got %d", n) + } + if err != nil { + return "", err + } + return string(key), nil +} + +// serialReadKey reads the public minisign key from a usb tty specified in config.KeyLocation. +func serialReadKey() (string, error) { + // Since the usb serial is tested with an arduino + // it is assumed that the tty device does not always exist + // and can be manually plugged in by the user + if _, err := os.Stat(config.KeyLocation); !os.IsNotExist(err) { + fmt.Println("Reconnect arduino now") + for true { + if _, err := os.Stat(config.KeyLocation); os.IsNotExist(err) { + break + } + } + } else { + fmt.Println("Connect arduino now") + } + for true { + if _, err := os.Stat(config.KeyLocation); !os.IsNotExist(err) { + break + } + } + fmt.Println("Arduino connected") + c := &serial.Config{Name: config.KeyLocation, Baud: 9600} + s, err := serial.OpenPort(c) + if err != nil { + return "", err + } + + key := "" + for true { + buf := make([]byte, 128) + n, err := s.Read(buf) + if err != nil { + return "", err + } + defer s.Close() + key = key + fmt.Sprintf("%q", buf[:n]) + // ensure that two tab sequences are read + // meaning that the entire key has been captured + // since the key is surrounded by a tab sequence + if strings.Count(key, "\\t") == 2 { + break + } + } + key = strings.ReplaceAll(key, "\\t", "") + key = strings.ReplaceAll(key, "\"", "") + if len(key) != 56 { + return "", fmt.Errorf("Key does not match expected key size. Expected 56, got %d", len(key)) + } + return key, nil +} + +// ReadKey is a wrapper function to call the proper readKey function according to config.KeyStore. +func ReadKey() (string, error) { + switch config.KeyStore { + case 0: + return fileReadKey() + case 1: + return fileReadKey() + case 2: + return "", nil // TPM + case 3: + return serialReadKey() + } + return "", nil +} + +// ReadBlock reads a data area of a bytes.Reader specified in the given node. +// It additionally verifies that the amount of bytes read equal the wanted amount and returns an error if this is not the case. +func ReadBlock(node Node, part *bytes.Reader, totalReadBlocks int) ([]byte, int, error) { + if node.BlockEnd-node.BlockStart < 0 { + return []byte{}, -1, fmt.Errorf("tried creating byte slice with negative length. %d to %d total %d\n", node.BlockStart, node.BlockEnd, node.BlockEnd-node.BlockStart) + } else if node.BlockEnd-node.BlockStart > 2000 { + return []byte{}, -1, fmt.Errorf("tried creating byte slice with length over 2000. %d to %d total %d\n", node.BlockStart, node.BlockEnd, node.BlockEnd-node.BlockStart) + } + block := make([]byte, node.BlockEnd-node.BlockStart) + blockSize := node.BlockEnd - node.BlockStart + _, err := part.Seek(int64(node.BlockStart), 0) + if err != nil { + return []byte{}, -1, err + } + n, err := part.Read(block) + if err != nil { + return block, -1, err + } else if n != blockSize { + return block, -1, fmt.Errorf("Did not read correct amount of bytes. Expected: %d, Got: %d", blockSize, n) + } + return block, totalReadBlocks + 1, err +} + +// VerifySignature verifies the database using a given signature and public key. +func VerifySignature(key string, signature string, database string) (bool, error) { + var pk minisign.PublicKey + if err := pk.UnmarshalText([]byte(key)); err != nil { + return false, err + } + + data, err := os.ReadFile(database) + if err != nil { + return false, err + } + + return minisign.Verify(pk, data, []byte(signature)), nil +} + +// VerifyBlock verifies a byte slice with the hash in a given Node. +func VerifyBlock(block []byte, node Node) error { + calculatedBlockHash, err := CalculateBlockHash(block) + if err != nil { + return err + } + wantedBlockHash := node.BlockSum + if strings.Compare(calculatedBlockHash, strings.TrimSpace(wantedBlockHash)) == 0 { + return nil + } + return fmt.Errorf("Node %s ranging from %d to %d does not match block. Expected %s, got %s.", node.PrevNodeSum, node.BlockStart, node.BlockEnd, wantedBlockHash, calculatedBlockHash) +} + +// VerifyNode verifies that the current Node is valid by matching the checksum of it with the PrevNodeSum field of the next node. +func VerifyNode(node Node, nextNode Node) error { + nodeHash, err := calculateStringHash(fmt.Sprintf("%d%d%s%s", node.BlockStart, node.BlockEnd, node.BlockSum, node.PrevNodeSum)) + if err != nil { + return err + } + if strings.Compare(nodeHash, nextNode.PrevNodeSum) != 0 { + return fmt.Errorf("Node %s is not valid!", node.PrevNodeSum) + } + return nil +} diff --git a/verify/fsverify b/verify/fsverify Binary files differnew file mode 100755 index 0000000..89d1dfe --- /dev/null +++ b/verify/fsverify diff --git a/verify/go.mod b/verify/go.mod new file mode 100644 index 0000000..1d863fa --- /dev/null +++ b/verify/go.mod @@ -0,0 +1,18 @@ +module github.com/axtloss/fsverify + +go 1.21.6 + +require ( + github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 + github.com/spf13/cobra v1.8.0 + github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 + go.etcd.io/bbolt v1.3.8 +) + +require ( + aead.dev/minisign v0.2.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) diff --git a/verify/go.sum b/verify/go.sum new file mode 100644 index 0000000..a83ed25 --- /dev/null +++ b/verify/go.sum @@ -0,0 +1,29 @@ +aead.dev/minisign v0.2.1 h1:Z+7HA9dsY/eGycYj6kpWHpcJpHtjAwGiJFvbiuO9o+M= +aead.dev/minisign v0.2.1/go.mod h1:oCOjeA8VQNEbuSCFaaUXKekOusa/mll6WtMoO5JY4M4= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/verify/main.go b/verify/main.go new file mode 100644 index 0000000..4500a61 --- /dev/null +++ b/verify/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "github.com/axtloss/fsverify/cmd" +) + +var ( + Version = "0.1.0" +) + +func main() { + cmd.Execute() +} |