aboutsummaryrefslogtreecommitdiff
path: root/verify
diff options
context:
space:
mode:
authoraxtloss <axtlos@getcryst.al>2024-02-28 23:42:26 +0100
committeraxtloss <axtlos@getcryst.al>2024-02-28 23:42:26 +0100
commit3341bd0e945341528033ec6ebaef4f611f654ebe (patch)
treef82d4557ac097d08583e56152352cbd5abd335ea /verify
parent91d58f9ae9e9d9adc2e19a0b56d2b9757f6696d6 (diff)
downloadfsverify-3341bd0e945341528033ec6ebaef4f611f654ebe.tar.gz
fsverify-3341bd0e945341528033ec6ebaef4f611f654ebe.tar.bz2
restructure repository layout
Diffstat (limited to 'verify')
-rw-r--r--verify/cmd/root.go22
-rw-r--r--verify/cmd/verify.go174
-rw-r--r--verify/config/config.go15
-rw-r--r--verify/core/crypt.go27
-rw-r--r--verify/core/storage.go246
-rw-r--r--verify/core/verification.go165
-rwxr-xr-xverify/fsverifybin0 -> 6109315 bytes
-rw-r--r--verify/go.mod18
-rw-r--r--verify/go.sum29
-rw-r--r--verify/main.go13
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
new file mode 100755
index 0000000..89d1dfe
--- /dev/null
+++ b/verify/fsverify
Binary files differ
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()
+}