commit f266968e6dfdffd8b49fde6795848cd2f80e1f5a Author: Xe Date: Fri Jan 21 21:25:39 2022 -0500 initial commit Signed-off-by: Xe diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..051d09d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +eval "$(lorri direnv)" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b169f30 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Xe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/changeset/changeset.go b/changeset/changeset.go new file mode 100644 index 0000000..a7a9023 --- /dev/null +++ b/changeset/changeset.go @@ -0,0 +1,54 @@ +package changeset + +import ( + "fmt" + + "golang.org/x/crypto/ssh" +) + +// Metadata is the metadata for the changeset. +type Metadata struct { + // The name of the service + Name string + // The git hash of the repo checkout that made the service + Version string + // Slug SHA256 hash, this is what is signed + Hash []byte + // The SSH signatures for this ChangeSet + Signatures []*ssh.Signature +} + +type ChangeSet struct { + Metadata Metadata + SlugFile string +} + +func (cs ChangeSet) Validate(trustedKeys []ssh.PublicKey, minNeeded int) error { + var gotKeys = map[string]struct{}{} + for _, sig := range cs.Metadata.Signatures { + for _, pubkey := range trustedKeys { + fp := ssh.FingerprintSHA256(pubkey) + + if _, got := gotKeys[fp]; got { + return fmt.Errorf("changeset: %v signed this changeset more than once", fp) + } + + if fp != string(sig.Rest) { + continue + } + + err := pubkey.Verify(cs.Metadata.Hash, sig) + if err != nil { + return err + } + + gotKeys[fp] = struct{}{} + } + } + + if gk := len(gotKeys); gk != minNeeded { + return fmt.Errorf("wanted %d keys, only signed with %d keys", minNeeded, gk) + } + + return nil +} diff --git a/changeset/changeset_test.go b/changeset/changeset_test.go new file mode 100644 index 0000000..212615c --- /dev/null +++ b/changeset/changeset_test.go @@ -0,0 +1,82 @@ +package changeset + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/json" + "os" + "testing" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +func sha256sum(data []byte) []byte { + h := sha256.New() + h.Write(data) + return h.Sum(nil) +} + +func TestChangeSetSimpleValidate(t *testing.T) { + a := agent.NewKeyring() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + + err = a.Add(agent.AddedKey{ + PrivateKey: key, + }) + if err != nil { + t.Fatal(err) + } + + slugLoc := "../var/thoth.md" + data, err := os.ReadFile(slugLoc) + if err != nil { + t.Fatal(err) + } + + checksum := sha256sum(data) + var sigs []*ssh.Signature + var pubkeys []ssh.PublicKey + aPubkeys, err := a.List() + if err != nil { + t.Fatal(err) + } + + for _, ak := range aPubkeys { + pubkeys = append(pubkeys, ak) + } + + for _, ak := range pubkeys { + sig, err := a.Sign(ak, checksum) + if err != nil { + t.Fatal(err) + } + + sig.Rest = []byte(ssh.FingerprintSHA256(ak)) + + sigs = append(sigs, sig) + } + + cs := ChangeSet{ + Metadata: Metadata{ + Name: "test", + Version: "test", + Hash: checksum, + Signatures: sigs, + }, + SlugFile: slugLoc, + } + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + enc.Encode(cs) + + err = cs.Validate(pubkeys, 1) + if err != nil { + t.Fatal(err) + } +} diff --git a/cmd/thoth/main.go b/cmd/thoth/main.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/cmd/thoth/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b1785f2 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module tulpa.dev/cadey/thoth + +go 1.16 + +require golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..360fc51 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI= +golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1d79e9a --- /dev/null +++ b/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "flag" + "log" + "net" + "os" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +var ( + fname = flag.String("fname", "var/thoth.md", "file to sign and validate") + + trustedPubkeys = []string{ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPg9gYKVglnO2HQodSJt4z4mNrUSUiyJQ7b+J798bwD9", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPYr9hiLtDHgd6lZDgQMkJzvYeAXmePOrgFaWHAjJvNU", + } +) + +func main() { + flag.Parse() + + // ssh-agent(1) provides a UNIX socket at $SSH_AUTH_SOCK. + socket := os.Getenv("SSH_AUTH_SOCK") + conn, err := net.Dial("unix", socket) + if err != nil { + log.Fatalf("Failed to open SSH_AUTH_SOCK: %v", err) + } + + agentClient := agent.NewClient(conn) + + data, err := os.ReadFile(*fname) + if err != nil { + log.Fatal(err) + } + + var sigs []*ssh.Signature + var pubkeys []ssh.PublicKey + for _, pkString := range trustedPubkeys { + pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pkString)) + if err != nil { + log.Fatal(err) + } + pubkeys = append(pubkeys, pubkey) + + sig, err := agentClient.Sign(pubkey, data) + if err != nil { + log.Fatal(err) + } + + sig.Rest = []byte(ssh.FingerprintSHA256(pubkey)) + + sigs = append(sigs, sig) + } + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + enc.Encode(sigs) + os.Stdout.Sync() + + for _, sig := range sigs { + for _, pubkey := range pubkeys { + if ssh.FingerprintSHA256(pubkey) != string(sig.Rest) { + continue + } + + err := pubkey.Verify(data, sig) + if err != nil { + log.Fatal(err) + } + } + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..67222cb --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + go gopls goimports + + # keep this line if you use bash + pkgs.bashInteractive + ]; +} diff --git a/var/thoth.md b/var/thoth.md new file mode 100644 index 0000000..fad70cd --- /dev/null +++ b/var/thoth.md @@ -0,0 +1,27 @@ +--- +title: A Tool to Aid Forgetfulness +date: 2022-01-12 +series: stories +--- + +The Egyptian God Thoth lived in the Egyptian city of Naucratis. Thoth was the +inventor of many arts such as math and astronomy, but the most significant was +the invention of writing. Thoth showed writing to the king of Egypt, claiming +that it would make Egyptians wiser and give them better memories; that it it +would vastly improve both the memory and the wit of the Egyptian people. + +The king replied: "Thoth, you invented this tool. As such you are not the best +one to judge such things. You have not created a tool to aid memory, you have +created a tool to aid forgetfulness. Learners will not use their memories, they +will blindly trust these sigils and not remember for themselves. + +"You have discovered an aid to vague recollection, as the users of this tool +will not be given truth. They will only be given a semblance of truth. + +"They will be hearers of many things and learners of nothing. They will appear +to know all the knowledge of the world yet when asked they will only be the +middleman to external forces that are trusted without verification. They will +know wisdom, but not truth." + +Adapted from The Dialogues of Plato in Five Volumes, 3rd ed. Oxford +University, 1892. Vol. 1 pp. 483-489.