├── .circleci └── config.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── analyze.go ├── analyze_keyset.go ├── analyze_payload.go ├── generate.go ├── generate_keyset.go ├── generate_payload.go ├── root.go ├── run.go └── version.go ├── go.mod ├── go.sum ├── images ├── .DS_Store ├── hashmap-logo-neon-with-bg.svg ├── hashmap-logo-neon.png └── hashmap-logo-neon.svg ├── internal ├── .DS_Store ├── analyze │ ├── keyset.go │ ├── keyset_test.go │ ├── payload.go │ └── payload_test.go ├── server │ ├── server.go │ └── server_test.go └── storage │ ├── dynamo.go │ ├── memory.go │ ├── memory_test.go │ ├── redis.go │ ├── redis_test.go │ ├── storage.go │ └── storage_test.go ├── main.go ├── pkg ├── .DS_Store ├── payload │ ├── json.go │ ├── json_test.go │ ├── payload.go │ ├── payload_test.go │ ├── verify.go │ └── verify_test.go └── sig │ ├── json.go │ ├── json_test.go │ ├── nacl_sign.go │ ├── nacl_sign_test.go │ ├── sig.go │ ├── sig_test.go │ ├── sigutil │ ├── sigutil.go │ └── sigutil_test.go │ ├── xmss.go │ └── xmss_test.go ├── test └── testdata │ ├── hashmap_ed25519.keyset │ └── valid_payload_expired.json └── x ├── .DS_Store ├── README.md ├── flag-exp └── main.go ├── request_demo └── main.go ├── server_tinker └── main.go ├── webapp-demo ├── .gitignore ├── Makefile ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── assets │ │ ├── bundle.js │ │ ├── fonts │ │ │ ├── hack-regular-subset.woff2 │ │ │ ├── roboto-v20-latin-300.woff2 │ │ │ ├── roboto-v20-latin-300italic.woff2 │ │ │ ├── roboto-v20-latin-700.woff2 │ │ │ └── roboto-v20-latin-700italic.woff2 │ │ └── hashmap.wasm │ ├── favicon.png │ ├── hashmap.wasm │ ├── index.html │ └── roboto-v20-latin-regular.woff2 ├── rollup.config.js ├── server │ ├── main │ └── main.go ├── src │ ├── App.svelte │ ├── Logo.svelte │ ├── WasmExec.svelte │ ├── custom.css │ ├── main.js │ ├── routes │ │ ├── About.svelte │ │ ├── Blog.svelte │ │ ├── Get.svelte │ │ └── Home.svelte │ └── wasm_exec.js ├── statik │ └── statik.go └── wasm │ └── main.go └── xmss-test └── main.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version 9 | - image: cimg/go:1.20 10 | steps: 11 | - checkout 12 | # ensure the cli tool can build 13 | - run: go test -race -coverprofile=coverage.txt -covermode=atomic ./pkg/... ./internal/... 14 | - run: bash <(curl -s https://codecov.io/bash) 15 | workflows: 16 | version: 2 17 | build: 18 | jobs: 19 | - build: 20 | context: org-global 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # tests and coverage tools 9 | *.test 10 | *.out 11 | coverage.txt 12 | 13 | vendor 14 | .idea 15 | node_modules 16 | 17 | # mac files 18 | .DS_Store -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at code@nomasters.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13.4-alpine3.10 AS builder 2 | 3 | ARG CGO_ENABLED=0 4 | 5 | RUN apk update && apk add ca-certificates 6 | 7 | WORKDIR /app 8 | COPY . . 9 | 10 | RUN go install 11 | 12 | FROM alpine:3.10 13 | 14 | COPY --from=builder /etc/ssl/certs /etc/ssl/certs 15 | COPY --from=builder /go/bin/hashmap /usr/bin/hashmap 16 | RUN addgroup -S hashmap && adduser -S hashmap -G hashmap 17 | WORKDIR /home/hashmap 18 | 19 | USER hashmap -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # hashmap docker testbed 2 | 3 | PROJECT := hashmap 4 | 5 | .PHONY: usage build ssh clean 6 | 7 | usage: 8 | @echo "Targets:" 9 | @echo " build: Create a $(PROJECT) docker image" 10 | @echo " shell: Run an interactive shell in the container" 11 | @echo " clean: Delete all docker images and containers" 12 | 13 | build: 14 | docker build --tag $(PROJECT) . 15 | 16 | shell: 17 | docker run -it $(PROJECT) /bin/sh 18 | 19 | clean: 20 | docker images -aq | xargs -n 1 docker rmi -f 21 | docker ps -aq | docker rm -f 22 | 23 | coverage-race: 24 | go test -race -coverprofile=cp.out -covermode=atomic ./pkg/... ./internal/... && go tool cover -html=cp.out 25 | 26 | coverage: 27 | go test -coverprofile=cp.out ./pkg/... ./internal/... && go tool cover -html=cp.out -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![hashmap logo](images/hashmap-logo-neon.svg) 2 | 3 | # hashmap 4 | 5 | hashmap server is a light-weight cryptographically signed public key value store inspired by JWT and IPNS. 6 | 7 | [![CircleCI][1]][2] [![Go Report Card][3]][4] [![codecov][5]][6] [![GoDoc][7]][8] 8 | 9 | [1]: https://circleci.com/gh/nomasters/hashmap.svg?style=svg 10 | [2]: https://circleci.com/gh/nomasters/hashmap 11 | [3]: https://goreportcard.com/badge/github.com/nomasters/hashmap 12 | [4]: https://goreportcard.com/report/github.com/nomasters/hashmap 13 | [5]: https://codecov.io/gh/nomasters/hashmap/branch/master/graph/badge.svg 14 | [6]: https://codecov.io/gh/nomasters/hashmap 15 | [7]: https://godoc.org/github.com/nomasters/hashmap?status.svg 16 | [8]: https://godoc.org/github.com/nomasters/hashmap 17 | 18 | ## Summary 19 | 20 | NOTE: this repo is in the midst of a large refactor for the next alpha version, the stable initial version can be found in (v0.0.4 here)[https://github.com/nomasters/hashmap/tree/v0.0.4] 21 | 22 | This documentation is going through a major rewrite. Please refer to the godoc for the behavior of tooling in `/pkg` until I get this updated. 23 | -------------------------------------------------------------------------------- /cmd/analyze.go: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // For more information, please refer to 25 | 26 | package cmd 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | // analyzeCmd represents the analyze command 35 | var analyzeCmd = &cobra.Command{ 36 | Use: "analyze", 37 | Short: "A brief description of your command", 38 | Long: `A longer description that spans multiple lines and likely contains examples 39 | and usage of using your command. For example: 40 | 41 | Cobra is a CLI library for Go that empowers applications. 42 | This application is a tool to generate the needed files 43 | to quickly create a Cobra application.`, 44 | Run: func(cmd *cobra.Command, args []string) { 45 | fmt.Println("analyze called") 46 | }, 47 | } 48 | 49 | func init() { 50 | rootCmd.AddCommand(analyzeCmd) 51 | 52 | // Here you will define your flags and configuration settings. 53 | 54 | // Cobra supports Persistent Flags which will work for this command 55 | // and all subcommands, e.g.: 56 | // analyzeCmd.PersistentFlags().String("foo", "", "A help for foo") 57 | 58 | // Cobra supports local flags which will only run when this command 59 | // is called directly, e.g.: 60 | // analyzeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/analyze_keyset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 NAME HERE 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | // "io/ioutil" 20 | // "log" 21 | 22 | // analyze "github.com/nomasters/hashmap/internal/analyze" 23 | // sigutil "github.com/nomasters/hashmap/pkg/sig/sigutil" 24 | 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | // analyzeKeysetCmd represents the analyzeKeyset command 29 | var analyzeKeysetCmd = &cobra.Command{ 30 | Use: "keyset", 31 | Short: "A brief description of your command", 32 | Long: `A longer description that spans multiple lines and likely contains examples 33 | and usage of using your command. For example: 34 | 35 | Cobra is a CLI library for Go that empowers applications. 36 | This application is a tool to generate the needed files 37 | to quickly create a Cobra application.`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | // TODO: handle custom file paths 40 | // signerBytes, err := ioutil.ReadFile("hashmap.keyset") 41 | // if err != nil { 42 | // log.Fatal(err) 43 | // } 44 | // signers, err := sigutil.Decode(signerBytes) 45 | // if err != nil { 46 | // log.Fatal(err) 47 | // } 48 | // fmt.Printf("%s\n", analyze.Signers(signers)) 49 | }, 50 | } 51 | 52 | func init() { 53 | analyzeCmd.AddCommand(analyzeKeysetCmd) 54 | 55 | // Here you will define your flags and configuration settings. 56 | 57 | // Cobra supports Persistent Flags which will work for this command 58 | // and all subcommands, e.g.: 59 | // analyzeKeysetCmd.PersistentFlags().String("foo", "", "A help for foo") 60 | 61 | // Cobra supports local flags which will only run when this command 62 | // is called directly, e.g.: 63 | // analyzeKeysetCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 64 | } 65 | -------------------------------------------------------------------------------- /cmd/analyze_payload.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 NAME HERE 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "io/ioutil" 21 | "encoding/json" 22 | "log" 23 | "os" 24 | 25 | "github.com/spf13/cobra" 26 | 27 | analyze "github.com/nomasters/hashmap/internal/analyze" 28 | ) 29 | 30 | // analyzePayloadCmd represents the analyzePayload command 31 | var analyzePayloadCmd = &cobra.Command{ 32 | Use: "payload", 33 | Short: "A brief description of your command", 34 | Long: `A longer description that spans multiple lines and likely contains examples 35 | and usage of using your command. For example: 36 | 37 | Cobra is a CLI library for Go that empowers applications. 38 | This application is a tool to generate the needed files 39 | to quickly create a Cobra application.`, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | b, err := ioutil.ReadAll(os.Stdin) 42 | if err != nil { 43 | log.Fatalln(err) 44 | } 45 | p , err := analyze.NewPayload(b) 46 | if err != nil { 47 | log.Fatalln(err) 48 | } 49 | output, err := json.Marshal(p) 50 | if err != nil { 51 | log.Fatalln(err) 52 | } 53 | fmt.Printf("%s\n",output) 54 | 55 | }, 56 | } 57 | 58 | func init() { 59 | analyzeCmd.AddCommand(analyzePayloadCmd) 60 | 61 | // Here you will define your flags and configuration settings. 62 | 63 | // Cobra supports Persistent Flags which will work for this command 64 | // and all subcommands, e.g.: 65 | // analyzePayloadCmd.PersistentFlags().String("foo", "", "A help for foo") 66 | 67 | // Cobra supports local flags which will only run when this command 68 | // is called directly, e.g.: 69 | // analyzePayloadCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 70 | } 71 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // For more information, please refer to 25 | 26 | package cmd 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | // generateCmd represents the generate command 35 | var generateCmd = &cobra.Command{ 36 | Use: "generate", 37 | Short: "A brief description of your command", 38 | Long: `A longer description that spans multiple lines and likely contains examples 39 | and usage of using your command. For example: 40 | 41 | Cobra is a CLI library for Go that empowers applications. 42 | This application is a tool to generate the needed files 43 | to quickly create a Cobra application.`, 44 | Run: func(cmd *cobra.Command, args []string) { 45 | fmt.Println("generate called") 46 | }, 47 | } 48 | 49 | func init() { 50 | rootCmd.AddCommand(generateCmd) 51 | 52 | // Here you will define your flags and configuration settings. 53 | 54 | // Cobra supports Persistent Flags which will work for this command 55 | // and all subcommands, e.g.: 56 | // generateCmd.PersistentFlags().String("foo", "", "A help for foo") 57 | 58 | // Cobra supports local flags which will only run when this command 59 | // is called directly, e.g.: 60 | // generateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/generate_keyset.go: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // For more information, please refer to 25 | 26 | package cmd 27 | 28 | import ( 29 | "fmt" 30 | "io/ioutil" 31 | "os" 32 | 33 | sigutil "github.com/nomasters/hashmap/pkg/sig/sigutil" 34 | "github.com/spf13/cobra" 35 | ) 36 | 37 | // keySetCmd represents the keySet command 38 | var keySetCmd = &cobra.Command{ 39 | Use: "keyset", 40 | Short: "generates a key-set to be used for signing payloads", 41 | Long: `A longer description that spans multiple lines and likely contains examples 42 | and usage of using your command. For example: 43 | 44 | Cobra is a CLI library for Go that empowers applications. 45 | This application is a tool to generate the needed files 46 | to quickly create a Cobra application.`, 47 | Run: func(cmd *cobra.Command, args []string) { 48 | // TODO: 49 | // - Update description 50 | // - add flags in for custom outputfile name 51 | // - add flags for custom key-set types 52 | 53 | signers := sigutil.NewDefaultSigners() 54 | b, err := sigutil.Encode(signers) 55 | if err != nil { 56 | fmt.Println(err) 57 | os.Exit(1) 58 | } 59 | if err := ioutil.WriteFile("hashmap.keyset", b, 0600); err != nil { 60 | fmt.Println(err) 61 | os.Exit(1) 62 | } 63 | }, 64 | } 65 | 66 | func init() { 67 | generateCmd.AddCommand(keySetCmd) 68 | 69 | // Here you will define your flags and configuration settings. 70 | 71 | // Cobra supports Persistent Flags which will work for this command 72 | // and all subcommands, e.g.: 73 | // keySetCmd.PersistentFlags().String("foo", "", "A help for foo") 74 | 75 | // Cobra supports local flags which will only run when this command 76 | // is called directly, e.g.: 77 | // keySetCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 78 | } 79 | -------------------------------------------------------------------------------- /cmd/generate_payload.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 NAME HERE 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "io/ioutil" 20 | "log" 21 | "time" 22 | 23 | payload "github.com/nomasters/hashmap/pkg/payload" 24 | sigutil "github.com/nomasters/hashmap/pkg/sig/sigutil" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var message string 29 | var ttl string 30 | var timestamp int64 31 | var keysetPath string 32 | var outputPath string 33 | 34 | // generatePayloadCmd represents the generatePayload command 35 | var generatePayloadCmd = &cobra.Command{ 36 | Use: "payload", 37 | Short: "A brief description of your command", 38 | Long: `A longer description that spans multiple lines and likely contains examples 39 | and usage of using your command. For example: 40 | 41 | Cobra is a CLI library for Go that empowers applications. 42 | This application is a tool to generate the needed files 43 | to quickly create a Cobra application.`, 44 | Run: func(cmd *cobra.Command, args []string) { 45 | 46 | signerBytes, err := ioutil.ReadFile(keysetPath) 47 | if err != nil { 48 | // TODO write a more meaningful error message 49 | log.Fatal(err) 50 | } 51 | signers, err := sigutil.Decode(signerBytes) 52 | if err != nil { 53 | // TODO write a more meaningful error message 54 | log.Fatal(err) 55 | } 56 | 57 | t, err := time.ParseDuration(ttl) 58 | if err != nil { 59 | // TODO write a more meaningful error message 60 | log.Fatal(err) 61 | } 62 | 63 | p, err := payload.Generate( 64 | []byte(message), 65 | signers, 66 | payload.WithTTL(t), 67 | payload.WithTimestamp(time.Unix(0, timestamp)), 68 | ) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | b, err := payload.Marshal(p) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | if err := ioutil.WriteFile(outputPath, b, 0600); err != nil { 77 | log.Fatal(err) 78 | } 79 | }, 80 | } 81 | 82 | func init() { 83 | generateCmd.AddCommand(generatePayloadCmd) 84 | 85 | generatePayloadCmd.Flags().StringVarP(&message, "message", "m", "", "The message to be stored in data of payload") 86 | generatePayloadCmd.Flags().StringVarP(&ttl, "ttl", "t", payload.DefaultTTL.String(), "ttl in XXhXXmXXs string format. Defaults to 24 hours") 87 | generatePayloadCmd.Flags().Int64VarP(×tamp, "timestamp", "s", time.Now().UnixNano(), "timestamp for message in unix-nano time. Defaults now") 88 | generatePayloadCmd.Flags().StringVarP(&keysetPath, "keyset", "k", "hashmap.keyset", "the path for the keyset file. Defaults `./hashamp.keyset`") 89 | generatePayloadCmd.Flags().StringVarP(&outputPath, "output", "o", "payload.protobuf", "the path for the output payload file. Defaults `./payload.protobuf`") 90 | } 91 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // For more information, please refer to 25 | 26 | package cmd 27 | 28 | import ( 29 | "fmt" 30 | "os" 31 | 32 | "github.com/spf13/cobra" 33 | "github.com/spf13/viper" 34 | ) 35 | 36 | var cfgFile string 37 | 38 | // rootCmd represents the base command when called without any subcommands 39 | var rootCmd = &cobra.Command{ 40 | Use: "hashmap", 41 | Short: "a light-weight and ephemeral cryptographically signed key value store", 42 | Long: `a light-weight and ephemeral cryptographically signed key value store`, 43 | } 44 | 45 | // Execute adds all child commands to the root command and sets flags appropriately. 46 | // This is called by main.main(). It only needs to happen once to the rootCmd. 47 | func Execute() { 48 | if err := rootCmd.Execute(); err != nil { 49 | fmt.Println(err) 50 | os.Exit(1) 51 | } 52 | } 53 | 54 | func init() { 55 | cobra.OnInitialize(initConfig) 56 | 57 | // Here you will define your flags and configuration settings. 58 | // Cobra supports persistent flags, which, if defined here, 59 | // will be global for your application. 60 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is in the current directory: ./hashmap.yaml)") 61 | 62 | // Cobra also supports local flags, which will only run 63 | // when this action is called directly. 64 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 65 | } 66 | 67 | // initConfig reads in config file and ENV variables if set. 68 | func initConfig() { 69 | if cfgFile != "" { 70 | // Use config file from the flag. 71 | viper.SetConfigFile(cfgFile) 72 | } else { 73 | // get the configuration file from the current directory 74 | viper.AddConfigPath(".") 75 | viper.SetConfigName("hashmap") 76 | } 77 | 78 | // support ENV for hashmap settings 79 | viper.SetEnvPrefix("hashmap") 80 | viper.BindEnv("server.host", "HASHMAP_SERVER_HOST") 81 | viper.BindEnv("server.port", "HASHMAP_SERVER_PORT") 82 | viper.BindEnv("server.tls", "HASHMAP_SERVER_TLS") 83 | viper.BindEnv("server.certfile", "HASHMAP_SERVER_CERTFILE") 84 | viper.BindEnv("server.keyfile", "HASHMAP_SERVER_KEYFILE") 85 | viper.BindEnv("storage.engine", "HASHMAP_STORAGE_ENGINE") 86 | viper.BindEnv("storage.endpoint", "HASHMAP_STORAGE_ENDPOINT") 87 | viper.BindEnv("storage.auth", "HASHMAP_STORAGE_AUTH") 88 | viper.BindEnv("storage.maxIdle", "HASHMAP_STORAGE_MAXIDLE") 89 | viper.BindEnv("storage.maxActive", "HASHMAP_STORAGE_MAXACTIVE") 90 | viper.BindEnv("storage.idleTimeout", "HASHMAP_STORAGE_IDLETIMEOUT") 91 | viper.BindEnv("storage.wait", "HASHMAP_STORAGE_WAIT") 92 | viper.BindEnv("storage.maxConnLifetime", "HASHMAP_STORAGE_MAXCONNLIFETIME") 93 | viper.BindEnv("storage.tls", "HASHMAP_STORAGE_TLS") 94 | 95 | viper.AutomaticEnv() 96 | 97 | // If a config file is found, read it in. 98 | if err := viper.ReadInConfig(); err == nil { 99 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // For more information, please refer to 25 | 26 | package cmd 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | // runCmd represents the run command 35 | var runCmd = &cobra.Command{ 36 | Use: "run", 37 | Short: "A brief description of your command", 38 | Long: `A longer description that spans multiple lines and likely contains examples 39 | and usage of using your command. For example: 40 | 41 | Cobra is a CLI library for Go that empowers applications. 42 | This application is a tool to generate the needed files 43 | to quickly create a Cobra application.`, 44 | Run: func(cmd *cobra.Command, args []string) { 45 | fmt.Println("run called") 46 | }, 47 | } 48 | 49 | func init() { 50 | rootCmd.AddCommand(runCmd) 51 | 52 | // Here you will define your flags and configuration settings. 53 | 54 | // Cobra supports Persistent Flags which will work for this command 55 | // and all subcommands, e.g.: 56 | // runCmd.PersistentFlags().String("foo", "", "A help for foo") 57 | 58 | // Cobra supports local flags which will only run when this command 59 | // is called directly, e.g.: 60 | // runCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // For more information, please refer to 25 | 26 | package cmd 27 | 28 | import ( 29 | "fmt" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | const ( 34 | version = "v0.1.0" 35 | ) 36 | 37 | // versionCmd represents the version command 38 | var versionCmd = &cobra.Command{ 39 | Use: "version", 40 | Short: "Returns the current version of hashmap.", 41 | Long: `Returns the current version of hashmap.`, 42 | Run: func(cmd *cobra.Command, args []string) { 43 | fmt.Println(version) 44 | }, 45 | } 46 | 47 | func init() { 48 | rootCmd.AddCommand(versionCmd) 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nomasters/hashmap 2 | 3 | require ( 4 | github.com/alicebob/miniredis/v2 v2.34.0 5 | github.com/danielhavir/go-xmss v0.0.0-20190612065714-fc36365f6ba0 6 | github.com/go-chi/chi/v5 v5.2.1 7 | github.com/go-chi/cors v1.2.1 8 | github.com/gomodule/redigo v1.9.2 9 | github.com/rakyll/statik v0.1.7 10 | github.com/spf13/cobra v1.9.1 11 | github.com/spf13/pflag v1.0.6 12 | github.com/spf13/viper v1.20.1 13 | golang.org/x/crypto v0.37.0 14 | ) 15 | 16 | require ( 17 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect 18 | github.com/fsnotify/fsnotify v1.9.0 // indirect 19 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 21 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 22 | github.com/sagikazarmark/locafero v0.9.0 // indirect 23 | github.com/sourcegraph/conc v0.3.0 // indirect 24 | github.com/spf13/afero v1.14.0 // indirect 25 | github.com/spf13/cast v1.7.1 // indirect 26 | github.com/subosito/gotenv v1.6.0 // indirect 27 | github.com/yuin/gopher-lua v1.1.1 // indirect 28 | go.uber.org/multierr v1.11.0 // indirect 29 | golang.org/x/sys v0.32.0 // indirect 30 | golang.org/x/text v0.24.0 // indirect 31 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | 35 | go 1.24 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= 2 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 3 | github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= 4 | github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 6 | github.com/danielhavir/go-xmss v0.0.0-20190612065714-fc36365f6ba0 h1:aqquQOOzND6btJ/dkRA08LLZVt6yfqtUPTSQnKUGNck= 7 | github.com/danielhavir/go-xmss v0.0.0-20190612065714-fc36365f6ba0/go.mod h1:/x86IGeOK3TJUCqgEz9DdMyOH3L/TBvztTN9Ry/IHyM= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 11 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 12 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 13 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 14 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 15 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 16 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 17 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 18 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 19 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 20 | github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= 21 | github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= 22 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 23 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 24 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 25 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 26 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 27 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 28 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 32 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 33 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 34 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 38 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 39 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 40 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 41 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 42 | github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= 43 | github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 44 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 45 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 46 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 47 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 48 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 49 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 50 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 51 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 52 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 53 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 54 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 55 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 56 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 57 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 58 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 59 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 60 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 61 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 62 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 63 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 64 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 65 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 66 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 67 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 68 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 69 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 72 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 73 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 74 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | -------------------------------------------------------------------------------- /images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/images/.DS_Store -------------------------------------------------------------------------------- /images/hashmap-logo-neon-with-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /images/hashmap-logo-neon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/images/hashmap-logo-neon.png -------------------------------------------------------------------------------- /images/hashmap-logo-neon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /internal/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/internal/.DS_Store -------------------------------------------------------------------------------- /internal/analyze/keyset.go: -------------------------------------------------------------------------------- 1 | package analyze 2 | 3 | import ( 4 | sigutil "github.com/nomasters/hashmap/pkg/sig/sigutil" 5 | ) 6 | 7 | type KeySet struct { 8 | Hash string `json:"pubkey_hash"` 9 | Signers []Signer `json:"signers"` 10 | Valid bool `json:"valid"` 11 | ErrorMessage string `json:"error_message,omitempty"` 12 | } 13 | 14 | type Signer struct { 15 | Type string `json:"type"` 16 | Count string `json:"count,omitempty"` 17 | PQR bool `json:"pqr"` 18 | } 19 | 20 | func NewKeySet(b []byte) (*KeySet, error) { 21 | signers, err := sigutil.Decode(b) 22 | if err != nil { 23 | return nil, err 24 | } 25 | m := []byte("hello, world") 26 | sigBundles, err := sigutil.SignAll(m, signers) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | // TODO: 32 | // write signer enum -> string function 33 | // write a function to check state on XMSS, should output XX of XXX 34 | // write a function for PQR metadata 35 | 36 | // get pubkey hash 37 | k := KeySet{ 38 | Hash: sigutil.EncodedBundleHash(sigBundles), 39 | Valid: sigutil.VerifyAll(m, sigBundles), 40 | } 41 | return &k, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/analyze/keyset_test.go: -------------------------------------------------------------------------------- 1 | package analyze 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func TestNewKeySet(t *testing.T) { 11 | keysetPath := "../../test/testdata/hashmap_ed25519.keyset" 12 | b, err := ioutil.ReadFile(keysetPath) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | k, err := NewKeySet(b) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | output, err := json.Marshal(*k) 22 | t.Log(string(output)) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /internal/analyze/payload.go: -------------------------------------------------------------------------------- 1 | package analyze 2 | 3 | import ( 4 | "time" 5 | 6 | payload "github.com/nomasters/hashmap/pkg/payload" 7 | ) 8 | 9 | // Payload is the analyze struct container for the standard 10 | // output. 11 | type Payload struct { 12 | Raw payload.Payload `json:"raw_payload"` 13 | Hash string `json:"endpoint_hash"` 14 | Timestamp time.Time `json:"timestamp"` 15 | TTL string `json:"ttl"` 16 | Expired bool `json:"expired"` 17 | ValidVersion bool `json:"valid_version"` 18 | ValidTimestamp bool `json:"valid_timestamp"` 19 | WithinSubmitWindow bool `json:"within_submit_window"` 20 | ValidTTL bool `json:"valid_ttl"` 21 | ValidSignatures bool `json:"valid_signatures"` 22 | ValidDataSize bool `json:"valid_data_size"` 23 | ValidPayloadSize bool `json:"valid_payload_size"` 24 | ErrorMessage string `json:"error_message,omitempty"` 25 | } 26 | 27 | // NewPayload returns a payload analysis and runs the entire validation suite on the output 28 | func NewPayload(b []byte) (*Payload, error) { 29 | var p Payload 30 | pl, err := payload.Unmarshal(b) 31 | if err != nil { 32 | return nil, err 33 | } 34 | p.Raw = pl 35 | p.Hash = pl.Endpoint() 36 | p.Timestamp = pl.Timestamp 37 | p.TTL = pl.TTL.String() 38 | p.analyze(pl) 39 | if err := pl.Verify(); err != nil { 40 | p.ErrorMessage = err.Error() 41 | } 42 | return &p, nil 43 | } 44 | 45 | func (p *Payload) analyze(pl payload.Payload) { 46 | now := time.Now() 47 | p.Expired = pl.IsExpired(now) 48 | p.ValidVersion = pl.ValidVersion() 49 | p.ValidTimestamp = !pl.IsInFuture(now) 50 | p.WithinSubmitWindow = pl.WithinSubmitWindow(now) 51 | p.ValidTTL = pl.ValidTTL() 52 | p.ValidSignatures = pl.VerifySignatures() 53 | p.ValidDataSize = pl.ValidDataSize() 54 | p.ValidPayloadSize = pl.ValidPayloadSize() 55 | } 56 | -------------------------------------------------------------------------------- /internal/analyze/payload_test.go: -------------------------------------------------------------------------------- 1 | package analyze 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | ) 7 | 8 | func TestNewPayload(t *testing.T) { 9 | validExpired := "../../test/testdata/valid_payload_expired.json" 10 | protoBytes, err := ioutil.ReadFile(validExpired) 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | if _, err := NewPayload(protoBytes); err != nil { 15 | t.Error(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "time" 14 | 15 | "github.com/go-chi/chi/v5" 16 | "github.com/go-chi/chi/v5/middleware" 17 | "github.com/go-chi/cors" 18 | "github.com/nomasters/hashmap/internal/storage" 19 | "github.com/nomasters/hashmap/pkg/payload" 20 | ) 21 | 22 | const ( 23 | defaultTimeout = 15 * time.Second 24 | defaultPort = 3000 25 | defaultThrottleLimit = 100 26 | defaultThrottleBacklog = 100 27 | endpointHashLength = 88 // char count for blake2b-512 base64 string 28 | ) 29 | 30 | // Run takes an arbitrary number of options and runs a server. The important steps here 31 | // are that it configured a storage interface and wires that into a router to be used 32 | // by the server handler. The runtime also leverages the Shutdown method to attempt a 33 | // graceful shutdown in the event of an Interrupt signal. The shutdown process attempts 34 | // to wait for all connections to close but is limited by the server timeout configuration 35 | // which is passed into the context for the shutdown. 36 | func Run(options ...Option) { 37 | var srv http.Server 38 | 39 | o := parseOptions(options...) 40 | s, err := storage.New(o.storage...) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | defer s.Close() 45 | 46 | srv.Addr = o.addrString() 47 | srv.Handler = newRouter(s, o) 48 | 49 | idleConnsClosed := make(chan struct{}) 50 | go func() { 51 | sigint := make(chan os.Signal, 1) 52 | signal.Notify(sigint, os.Interrupt) 53 | <-sigint 54 | ctx, cancel := context.WithTimeout(context.Background(), o.timeout) 55 | defer cancel() 56 | // We received an interrupt signal, shut down. 57 | if err := srv.Shutdown(ctx); err != nil { 58 | log.Printf("SERVER SHUTDOWN ERROR: %v", err) 59 | } 60 | close(idleConnsClosed) 61 | }() 62 | 63 | log.Printf("Server started on: %v\n", o.addrString()) 64 | if o.tls { 65 | if err := srv.ListenAndServeTLS(o.certFile, o.keyFile); err != http.ErrServerClosed { 66 | log.Printf("SERVER ERROR: %v", err) 67 | } 68 | } else { 69 | log.Println("WARNING: running in NON-TLS MODE") 70 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 71 | log.Printf("SERVER ERROR: %v", err) 72 | } 73 | } 74 | <-idleConnsClosed 75 | log.Println("\n...shutdown complete") 76 | } 77 | 78 | // Option is func signature used for setting Server Options 79 | type Option func(*options) 80 | 81 | // options contains private fields used for Option 82 | type options struct { 83 | host string 84 | port int 85 | tls bool 86 | certFile string 87 | keyFile string 88 | timeout time.Duration 89 | limit int 90 | backlog int 91 | storage []storage.Option 92 | allowedHeaders []string 93 | allowedOrigins []string 94 | baseRoute string 95 | } 96 | 97 | // addrString returns a string formatted as expected by the net libraries in go. 98 | func (o options) addrString() string { 99 | port := o.port 100 | if port == 0 { 101 | port = defaultPort 102 | } 103 | return fmt.Sprintf("%v:%v", o.host, port) 104 | } 105 | 106 | func newRouter(s storage.GetSetCloser, o options) http.Handler { 107 | r := chi.NewRouter() 108 | r.Use(newCors(o.allowedHeaders, o.allowedOrigins).Handler) 109 | r.Use(middleware.Timeout(o.timeout)) 110 | r.Use(middleware.ThrottleBacklog(o.limit, o.backlog, o.timeout)) 111 | r.Route(o.baseRoute, func(r chi.Router) { 112 | r.Use(middleware.Heartbeat("/health")) 113 | r.Post("/", postPayloadHandler(s)) 114 | r.Get("/{hash}", getPayloadByHashHandler(s)) 115 | }) 116 | return r 117 | } 118 | 119 | // badRequest silently returns 400 and logs error 120 | func badRequest(w http.ResponseWriter, v ...interface{}) { 121 | if len(v) > 0 { 122 | log.Println(v...) 123 | } 124 | http.Error(w, http.StatusText(400), http.StatusBadRequest) 125 | } 126 | 127 | // postPayloadHandler takes a storage.Setter and returns a http.HandlerFunc that 128 | // uses a limited reader set to payload.MaxPayloadSize and attempts to verify 129 | // and validate the payload in ServerMode. ServerMode verification adds an additional 130 | // time horizon check to ensure that a payload is only written to storage within a 131 | // strict time horizon. 132 | func postPayloadHandler(s storage.Setter) http.HandlerFunc { 133 | return func(w http.ResponseWriter, r *http.Request) { 134 | l := &io.LimitedReader{R: r.Body, N: payload.MaxPayloadSize} 135 | body, err := ioutil.ReadAll(l) 136 | if err != nil { 137 | badRequest(w, "read error: ", err) 138 | return 139 | } 140 | p, err := payload.Unmarshal(body) 141 | if err != nil { 142 | badRequest(w, err) 143 | return 144 | } 145 | if err := p.Verify(payload.WithServerMode(true)); err != nil { 146 | badRequest(w, err) 147 | return 148 | } 149 | k := p.Endpoint() 150 | if err := s.Set(k, body, p.TTL, p.Timestamp); err != nil { 151 | badRequest(w, err) 152 | return 153 | } 154 | w.WriteHeader(http.StatusOK) 155 | } 156 | } 157 | 158 | func getPayloadByHashHandler(s storage.Getter) http.HandlerFunc { 159 | return func(w http.ResponseWriter, r *http.Request) { 160 | k := chi.URLParam(r, "hash") 161 | if len(k) != endpointHashLength { 162 | badRequest(w, "get error. invalid hash length for:", k) 163 | return 164 | } 165 | if _, err := base64.URLEncoding.DecodeString(k); err != nil { 166 | badRequest(w, "get error. base64 decode failed for:", k, err) 167 | return 168 | } 169 | pb, err := s.Get(k) 170 | if err != nil { 171 | badRequest(w, "get error. storage get error for:", k, err) 172 | return 173 | } 174 | p, err := payload.Unmarshal(pb) 175 | if err != nil { 176 | badRequest(w, "get error. payload unmarshal failed for:", k, err) 177 | return 178 | } 179 | if err := p.Verify(payload.WithValidateEndpoint(k)); err != nil { 180 | badRequest(w, "failed get verify", k, err) 181 | return 182 | } 183 | w.Write(pb) 184 | } 185 | } 186 | 187 | // newCors returns cors settings with optional 188 | func newCors(headers, origins []string) *cors.Cors { 189 | if len(origins) == 0 { 190 | origins = []string{"*"} 191 | } 192 | if len(headers) == 0 { 193 | headers = []string{"*"} 194 | } 195 | return cors.New(cors.Options{ 196 | AllowedOrigins: origins, 197 | AllowedMethods: []string{"GET", "POST", "OPTIONS"}, 198 | AllowedHeaders: headers, 199 | AllowCredentials: true, 200 | MaxAge: 600, 201 | }) 202 | } 203 | 204 | // parseOptions takes a arbitrary number of Option funcs and returns an options struct 205 | func parseOptions(opts ...Option) (o options) { 206 | o = options{ 207 | port: defaultPort, 208 | timeout: defaultTimeout, 209 | limit: defaultThrottleLimit, 210 | backlog: defaultThrottleBacklog, 211 | baseRoute: "/", 212 | } 213 | for _, option := range opts { 214 | option(&o) 215 | } 216 | 217 | if len(o.storage) == 0 { 218 | o.storage = append(o.storage, storage.WithEngine(storage.MemoryEngine)) 219 | } 220 | 221 | return 222 | } 223 | 224 | // WithCorsAllowedHeaders takes cors pointer and returns an Option func for setting cors 225 | func WithCorsAllowedHeaders(headers []string) Option { 226 | return func(o *options) { 227 | o.allowedHeaders = headers 228 | } 229 | } 230 | 231 | // WithCorsAllowedOrigins takes cors pointer and returns an Option func for setting cors 232 | func WithCorsAllowedOrigins(origins []string) Option { 233 | return func(o *options) { 234 | o.allowedOrigins = origins 235 | } 236 | } 237 | 238 | // WithHost takes a string and returns an Option func for setting options.host 239 | func WithHost(host string) Option { 240 | return func(o *options) { 241 | o.host = host 242 | } 243 | } 244 | 245 | // WithPort takes a string and returns an Option func for setting options.port 246 | func WithPort(port int) Option { 247 | return func(o *options) { 248 | o.port = port 249 | } 250 | } 251 | 252 | // WithStorageOptions takes a set of storage options and returns an Option func for setting options.storage 253 | func WithStorageOptions(opts ...storage.Option) Option { 254 | return func(o *options) { 255 | for _, opt := range opts { 256 | o.storage = append(o.storage, opt) 257 | } 258 | } 259 | } 260 | 261 | // WithTLS takes a boolean and returns an Option func for setting options.tls 262 | func WithTLS(b bool) Option { 263 | return func(o *options) { 264 | o.tls = b 265 | } 266 | } 267 | 268 | // WithCertFile takes a string and returns an Option func for setting options.certFile 269 | func WithCertFile(f string) Option { 270 | return func(o *options) { 271 | o.certFile = f 272 | } 273 | } 274 | 275 | // WithKeyFile takes a string and returns an Option func for setting options.keyFile 276 | func WithKeyFile(f string) Option { 277 | return func(o *options) { 278 | o.keyFile = f 279 | } 280 | } 281 | 282 | // WithTimeout takes a string and returns an Option func for setting options.timeout 283 | func WithTimeout(d time.Duration) Option { 284 | return func(o *options) { 285 | o.timeout = d 286 | } 287 | } 288 | 289 | // WithThrottle takes an int and returns an Option func for setting options.limit 290 | func WithThrottle(t int) Option { 291 | return func(o *options) { 292 | o.limit = t 293 | } 294 | } 295 | 296 | // WithThrottleBacklog takes an int and returns an Option func for setting options.backlog 297 | func WithThrottleBacklog(t int) Option { 298 | return func(o *options) { 299 | o.backlog = t 300 | } 301 | } 302 | 303 | // WithBaseRoute takes a string and returns an Option func for setting options.baseRoute 304 | func WithBaseRoute(b string) Option { 305 | return func(o *options) { 306 | o.baseRoute = b 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /internal/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | -------------------------------------------------------------------------------- /internal/storage/dynamo.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | // TODO - add aws dynamodb support 4 | -------------------------------------------------------------------------------- /internal/storage/memory.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // MemoryStore is the primary in-memory data storage and retrieval struct. It contains 11 | // a sync.RWMutex and an internal map of `map[string][]byte` to store state that conforms 12 | // to the Storage interface. 13 | type MemoryStore struct { 14 | sync.RWMutex 15 | internal map[string]memVal 16 | } 17 | 18 | // memVal is the value wrapper in the MemoryStore internal map and is used to 19 | // store the timestamp of the payload to prevent replay attacks 20 | type memVal struct { 21 | payload []byte 22 | timestamp time.Time 23 | } 24 | 25 | // NewMemoryStore returns a reference to a MemoryStore with an initialized internal map 26 | func NewMemoryStore() *MemoryStore { 27 | return &MemoryStore{ 28 | internal: make(map[string]memVal), 29 | } 30 | } 31 | 32 | // Get takes a key string and returns a byte slice and error. This method uses read locks. 33 | // It returns an error if the key is not found. 34 | func (s *MemoryStore) Get(key string) ([]byte, error) { 35 | s.RLock() 36 | v, ok := s.internal[key] 37 | s.RUnlock() 38 | if !ok { 39 | return []byte{}, errors.New("key not found") 40 | } 41 | return v.payload, nil 42 | } 43 | 44 | // Set takes a key string and byte slice value and returns an error. It uses a mutex write lock for safety. 45 | // If an existing key value pair exists, it checks the timestamp and rejects <= timestamp submissions. 46 | func (s *MemoryStore) Set(key string, value []byte, ttl time.Duration, timestamp time.Time) error { 47 | s.Lock() 48 | 49 | v, ok := s.internal[key] 50 | if ok { 51 | if v.timestamp.UnixNano()/1000 >= timestamp.UnixNano()/1000 { 52 | s.Unlock() 53 | return errInvalidTimestamp 54 | } 55 | } 56 | s.internal[key] = memVal{ 57 | payload: value, 58 | timestamp: timestamp, 59 | } 60 | s.Unlock() 61 | go func() { 62 | time.Sleep(safeTTL(ttl)) 63 | s.deleteIfValueMatch(key, value) 64 | }() 65 | return nil 66 | } 67 | 68 | // Close implements the standard Close method for storage. 69 | func (s *MemoryStore) Close() error { 70 | return nil 71 | } 72 | 73 | // deleteIfValueMatch compares values and deletes if they are they same 74 | func (s *MemoryStore) deleteIfValueMatch(key string, value []byte) { 75 | s.Lock() 76 | if bytes.Equal(s.internal[key].payload, value) { 77 | delete(s.internal, key) 78 | } 79 | s.Unlock() 80 | } 81 | -------------------------------------------------------------------------------- /internal/storage/memory_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestMemoryStore_Get(t *testing.T) { 10 | t.Parallel() 11 | 12 | t.Run("normal operation", func(t *testing.T) { 13 | t.Parallel() 14 | 15 | key := "DEADBEEF" 16 | expected := []byte("such_dead_much_beef") 17 | s := NewMemoryStore() 18 | defer s.Close() 19 | s.internal[key] = memVal{ 20 | payload: expected, 21 | timestamp: time.Now(), 22 | } 23 | 24 | actual, err := s.Get(key) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | if !bytes.Equal(expected, actual) { 29 | t.Errorf("actual: %v, expected: %v", actual, expected) 30 | } 31 | }) 32 | 33 | t.Run("without relevant entry", func(t *testing.T) { 34 | t.Parallel() 35 | 36 | key := "DEADBEEF" 37 | s := NewMemoryStore() 38 | if _, err := s.Get(key); err == nil { 39 | t.Fail() 40 | } 41 | }) 42 | } 43 | 44 | func TestMemoryStore_Set(t *testing.T) { 45 | t.Parallel() 46 | 47 | t.Run("normal operation", func(t *testing.T) { 48 | t.Parallel() 49 | 50 | key := "DEADBEEF" 51 | expected := []byte("such_dead_much_beef") 52 | now := time.Now() 53 | s := NewMemoryStore() 54 | 55 | if _, err := s.Get(key); err == nil { 56 | t.Fail() 57 | } 58 | 59 | if err := s.Set(key, expected, 1*time.Second, now); err != nil { 60 | t.Error(err) 61 | } 62 | actual, err := s.Get(key) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | if !bytes.Equal(expected, actual) { 67 | t.Errorf("actual: %v, expected: %v", actual, expected) 68 | } 69 | if err := s.Set(key, expected, 1*time.Second, now.Add(-1*time.Nanosecond)); err == nil { 70 | t.Error("failed to catch stale timestamp") 71 | } 72 | }) 73 | 74 | t.Run("deleteIfValueMatch", func(t *testing.T) { 75 | t.Parallel() 76 | 77 | key := "DEADBEEF" 78 | expected := []byte("such_dead_much_beef") 79 | s := NewMemoryStore() 80 | 81 | if _, err := s.Get(key); err == nil { 82 | t.Fail() 83 | } 84 | 85 | if err := s.Set(key, expected, 1*time.Second, time.Now()); err != nil { 86 | t.Error(err) 87 | } 88 | s.deleteIfValueMatch(key, expected) 89 | if _, err := s.Get(key); err == nil { 90 | t.Error("failed to delete key") 91 | } 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /internal/storage/redis.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | ) 10 | 11 | const ( 12 | defaultRedisAddress = ":6379" 13 | ) 14 | 15 | // redisOptions specific to RedisStorage 16 | type redisOptions struct { 17 | endpoint string 18 | auth string 19 | maxIdle int 20 | maxActive int 21 | idleTimeout time.Duration 22 | wait bool 23 | maxConnLifetime time.Duration 24 | tls bool 25 | dialTLSSkipVerify bool 26 | } 27 | 28 | // RedisOption is used for special Settings in Storage 29 | type RedisOption func(*redisOptions) 30 | 31 | // parseRedisOptions takes a arbitrary number of Option funcs and returns a options struct 32 | func parseRedisOptions(opts ...RedisOption) (o redisOptions) { 33 | for _, option := range opts { 34 | option(&o) 35 | } 36 | return 37 | } 38 | 39 | // WithRedisEndpoint takes a string and returns an RedisOption 40 | func WithRedisEndpoint(e string) RedisOption { 41 | return func(o *redisOptions) { 42 | o.endpoint = e 43 | } 44 | } 45 | 46 | // WithRedisAuth takes a string and returns an RedisOption 47 | func WithRedisAuth(a string) RedisOption { 48 | return func(o *redisOptions) { 49 | o.auth = a 50 | } 51 | } 52 | 53 | // WithRedisMaxIdle takes an int and returns an RedisOption 54 | func WithRedisMaxIdle(m int) RedisOption { 55 | return func(o *redisOptions) { 56 | o.maxIdle = m 57 | } 58 | } 59 | 60 | // WithRedisMaxActive takes an int and returns an RedisOption 61 | func WithRedisMaxActive(m int) RedisOption { 62 | return func(o *redisOptions) { 63 | o.maxActive = m 64 | } 65 | } 66 | 67 | // WithRedisIdleTimeout takes a time.Duration and returns an RedisOption 68 | func WithRedisIdleTimeout(d time.Duration) RedisOption { 69 | return func(o *redisOptions) { 70 | o.idleTimeout = d 71 | } 72 | } 73 | 74 | // WithRedisWait takes a bool and returns an RedisOption 75 | func WithRedisWait(b bool) RedisOption { 76 | return func(o *redisOptions) { 77 | o.wait = b 78 | } 79 | } 80 | 81 | // WithRedisMaxConnLifetime takes a bool and returns an RedisOption 82 | func WithRedisMaxConnLifetime(d time.Duration) RedisOption { 83 | return func(o *redisOptions) { 84 | o.maxConnLifetime = d 85 | } 86 | } 87 | 88 | // WithRedisTLS takes a bool and returns an RedisOption 89 | func WithRedisTLS(b bool) RedisOption { 90 | return func(o *redisOptions) { 91 | o.tls = b 92 | } 93 | } 94 | 95 | // WithRedisDialTLSSkipVerify takes a bool and returns an RedisOption 96 | func WithRedisDialTLSSkipVerify(b bool) RedisOption { 97 | return func(o *redisOptions) { 98 | o.dialTLSSkipVerify = b 99 | } 100 | } 101 | 102 | // RedisStore is a struct with methods that conforms to the Storage Interface 103 | type RedisStore struct { 104 | pool *redis.Pool 105 | } 106 | 107 | // NewRedisStore returns a RedisStore with StorageOptions mapped to Redis Pool settings. 108 | // Optionally, if Auth is set, Auth is configured on Dial 109 | func NewRedisStore(opts ...RedisOption) *RedisStore { 110 | o := parseRedisOptions(opts...) 111 | if o.endpoint == "" { 112 | o.endpoint = defaultRedisAddress 113 | } 114 | 115 | return &RedisStore{ 116 | pool: &redis.Pool{ 117 | MaxIdle: o.maxIdle, 118 | MaxActive: o.maxActive, 119 | IdleTimeout: o.idleTimeout, 120 | Wait: o.wait, 121 | MaxConnLifetime: o.maxConnLifetime, 122 | Dial: func() (redis.Conn, error) { 123 | c, err := redis.Dial("tcp", o.endpoint, redis.DialUseTLS(o.tls), redis.DialTLSSkipVerify(o.dialTLSSkipVerify)) 124 | if err != nil { 125 | return nil, err 126 | } 127 | if o.auth != "" { 128 | if _, err := c.Do("AUTH", o.auth); err != nil { 129 | c.Close() 130 | return nil, err 131 | } 132 | } 133 | return c, nil 134 | }, 135 | }, 136 | } 137 | } 138 | 139 | // redisVal is the json container that lives in redis and is used by the Get and Set methods 140 | type redisVal struct { 141 | Payload string `json:"payload"` 142 | Timestamp int64 `json:"timestamp"` 143 | } 144 | 145 | // Get method for RedisStore 146 | func (s *RedisStore) Get(key string) ([]byte, error) { 147 | c := s.pool.Get() 148 | defer c.Close() 149 | 150 | data, err := redis.Bytes(c.Do("GET", key)) 151 | if err != nil { 152 | return []byte{}, err 153 | } 154 | 155 | var v redisVal 156 | if err := json.Unmarshal(data, &v); err != nil { 157 | return []byte{}, err 158 | } 159 | 160 | return base64.StdEncoding.DecodeString(v.Payload) 161 | } 162 | 163 | // Set method takes a key, value, and options and saves the base64 encoded value to redis. It returns an error 164 | // if one is generated by redis 165 | func (s *RedisStore) Set(key string, value []byte, ttl time.Duration, timestamp time.Time) error { 166 | v := redisVal{ 167 | Payload: base64.StdEncoding.EncodeToString(value), 168 | Timestamp: timestamp.UnixNano(), 169 | } 170 | enc, _ := json.Marshal(v) 171 | 172 | // safeSetLua is a lua script that adds a conditional check before setting a key 173 | safeSetLua := ` 174 | if redis.call("EXISTS", KEYS[1]) == 1 then 175 | local payload = redis.call("GET", KEYS[1]) 176 | local timestamp = cjson.decode(payload)["timestamp"] 177 | if timestamp/1000 >= tonumber(ARGV[2])/1000 then 178 | return nil 179 | end 180 | end 181 | return redis.call("SET", KEYS[1], ARGV[1], "EX", ARGV[3]) 182 | ` 183 | 184 | c := s.pool.Get() 185 | defer c.Close() 186 | safeSet := redis.NewScript(1, safeSetLua) 187 | // set key with value if timestamp > current timestamp, and set a 10 second TTL 188 | reply, err := safeSet.Do(c, key, enc, timestamp.UnixNano(), int(safeTTL(ttl).Seconds())) 189 | if err != nil { 190 | return err 191 | } 192 | if reply == nil { 193 | return errInvalidTimestamp 194 | } 195 | return nil 196 | } 197 | 198 | // Close implements the standard Close method for storage 199 | func (s *RedisStore) Close() error { 200 | return s.pool.Close() 201 | } 202 | -------------------------------------------------------------------------------- /internal/storage/redis_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "testing" 8 | "time" 9 | 10 | miniredis "github.com/alicebob/miniredis/v2" 11 | ) 12 | 13 | func TestRedisStore(t *testing.T) { 14 | t.Parallel() 15 | r, err := miniredis.Run() 16 | if err != nil { 17 | panic(err) 18 | } 19 | defer r.Close() 20 | 21 | auth := "super_secret_redis_auth" 22 | r.RequireAuth(auth) 23 | 24 | s := NewRedisStore( 25 | WithRedisEndpoint(r.Addr()), 26 | WithRedisDialTLSSkipVerify(false), 27 | WithRedisIdleTimeout(15*time.Second), 28 | WithRedisMaxActive(20), 29 | WithRedisMaxConnLifetime(20), 30 | WithRedisMaxIdle(20), 31 | WithRedisAuth(auth), 32 | WithRedisWait(true), 33 | WithRedisTLS(false), 34 | ) 35 | defer s.Close() 36 | 37 | t.Run("DialErr", func(t *testing.T) { 38 | s := NewRedisStore(WithRedisEndpoint(":58555")) 39 | if _, err := s.Get("dialFail"); err == nil { 40 | t.Error("failed to catch dial error on get") 41 | } 42 | if err := s.Set("dialFail", []byte{}, 0, time.Now()); err == nil { 43 | t.Error("failed to catch dial error on set") 44 | } 45 | }) 46 | 47 | t.Run("AuthErr", func(t *testing.T) { 48 | s := NewRedisStore(WithRedisEndpoint(r.Addr()), WithRedisAuth("wrong_auth")) 49 | if _, err := s.Get("authFail"); err == nil { 50 | t.Error(err) 51 | } 52 | }) 53 | 54 | t.Run("Get", func(t *testing.T) { 55 | key := "get1" 56 | expected := []byte("exp1") 57 | timestamp := time.Now() 58 | v := redisVal{ 59 | Payload: base64.StdEncoding.EncodeToString(expected), 60 | Timestamp: timestamp.UnixNano(), 61 | } 62 | enc, err := json.Marshal(v) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | 67 | r.Set(key, string(enc)) 68 | 69 | tests := []struct { 70 | key string 71 | expected []byte 72 | shouldErr bool 73 | description string 74 | }{ 75 | { 76 | key: key, 77 | expected: expected, 78 | description: "should get expected value", 79 | }, 80 | { 81 | key: "DNE", 82 | shouldErr: true, 83 | description: "should error an nil key", 84 | }, 85 | } 86 | 87 | for _, test := range tests { 88 | actual, err := s.Get(test.key) 89 | if test.shouldErr { 90 | if err == nil { 91 | t.Error(test.description) 92 | } 93 | continue 94 | } 95 | if err != nil { 96 | t.Error(test.description, err) 97 | continue 98 | } 99 | if !bytes.Equal(test.expected, actual) { 100 | t.Errorf("actual: %v, expected: %s description: %v\n", actual, test.expected, test.description) 101 | } 102 | } 103 | }) 104 | 105 | t.Run("Set", func(t *testing.T) { 106 | now := time.Now() 107 | 108 | tests := []struct { 109 | key string 110 | expected []byte 111 | ttl time.Duration 112 | timestamp time.Time 113 | fastForward time.Duration 114 | description string 115 | shouldErr bool 116 | }{ 117 | { 118 | key: "set1", 119 | expected: []byte("exp1"), 120 | ttl: 0 * time.Second, 121 | timestamp: now, 122 | fastForward: safeTTL(0*time.Second) + time.Second, 123 | description: "should conform to minTTL", 124 | }, 125 | { 126 | key: "set2", 127 | expected: []byte("exp2"), 128 | ttl: maxTTL + 30*time.Second, 129 | timestamp: now, 130 | fastForward: maxTTL + time.Second, 131 | description: "should conform to maxTTL", 132 | }, 133 | { 134 | key: "set3", 135 | expected: []byte("exp3"), 136 | ttl: minTTL + time.Second, 137 | timestamp: now, 138 | fastForward: minTTL + 2*time.Second, 139 | description: "should conform to standard TTL", 140 | }, 141 | } 142 | 143 | for _, test := range tests { 144 | if r.Exists(test.key) { 145 | t.Errorf("key: %v should not exist", test.key) 146 | continue 147 | } 148 | err := s.Set(test.key, test.expected, test.ttl, test.timestamp) 149 | if test.shouldErr { 150 | if err == nil { 151 | t.Error(test.description) 152 | } 153 | continue 154 | } 155 | if err != nil { 156 | t.Error(test.description, err) 157 | continue 158 | } 159 | if _, err := s.Get(test.key); err != nil { 160 | t.Error(test.key, "failed to get") 161 | } 162 | r.FastForward(test.fastForward) 163 | if _, err := s.Get(test.key); err == nil { 164 | t.Errorf("key: %v should not exist after ttl", test.key) 165 | } 166 | } 167 | }) 168 | t.Run("CheckTimeStampReplay", func(t *testing.T) { 169 | k := "replayattack" 170 | v1 := []byte("hello, world") 171 | v2 := []byte("bad actor") 172 | ttl := time.Second 173 | now := time.Unix(0, 1559706661127858000) 174 | s.Set(k, v1, ttl, now) 175 | if err := s.Set(k, v2, ttl, now.Add(-time.Millisecond)); err == nil { 176 | t.Error("failed to catch invalid timestamp") 177 | } 178 | }) 179 | t.Run("Malformed_Get", func(t *testing.T) { 180 | k := "invalidGet" 181 | r.Set(k, "malformed") 182 | if _, err := s.Get(k); err == nil { 183 | t.Error("failed to catch malformed value") 184 | } 185 | }) 186 | } 187 | -------------------------------------------------------------------------------- /internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/nomasters/hashmap/pkg/payload" 8 | ) 9 | 10 | // Engine is the enum type for StorageEngine 11 | type Engine uint8 12 | 13 | // Enum types for Storage Engine 14 | const ( 15 | _ Engine = iota 16 | MemoryEngine 17 | RedisEngine 18 | ) 19 | 20 | var ( 21 | minTTL = payload.MaxSubmitWindow*2 + time.Second // to prevent replay attacks against SubmitWindow 22 | maxTTL = payload.MaxTTL 23 | ) 24 | 25 | var ( 26 | errInvalidTimestamp = errors.New("storage: invalid timestamp") 27 | errInvalidStorage = errors.New("invalid storage engine") 28 | ) 29 | 30 | // Getter is an interface that wraps around the standard Get method. 31 | type Getter interface { 32 | Get(key string) ([]byte, error) 33 | } 34 | 35 | // Setter is an interface that wraps around the standard Set method. To Prevent replay attacks, the ttl 36 | // set on key should never be less than the minimumTTL, which is 2x the payload.MaxSubmitWindow. This prevents a specific 37 | // type of replay attack in which a short-lived TTL is set, below the SubmitWindow horizon, and a slightly older message, 38 | // also within the SubmitWindow time horizon is replayed and accepted due to no previous key being in the system. Timestamps 39 | // outside of this window should be automatically forward-looking, but a proper Set method should always check that the submitted 40 | // timestamp is greater than the existing one, if an existing one is found. 41 | type Setter interface { 42 | Set(key string, value []byte, ttl time.Duration, timestamp time.Time) error 43 | } 44 | 45 | // Closer is an interface that wraps around the standard Close method. 46 | type Closer interface { 47 | Close() error 48 | } 49 | 50 | // GetSetCloser is the interface that groups the basic Get, Set and Close methods. 51 | type GetSetCloser interface { 52 | Getter 53 | Setter 54 | Closer 55 | } 56 | 57 | // options is us to store Storage related Options 58 | type options struct { 59 | engine Engine 60 | redis []RedisOption 61 | } 62 | 63 | // Option is used for special Settings in Storage 64 | type Option func(*options) 65 | 66 | // parseOptions takes a arbitrary number of Option funcs and returns a options struct 67 | func parseOptions(opts ...Option) options { 68 | var o options 69 | for _, option := range opts { 70 | option(&o) 71 | } 72 | return o 73 | } 74 | 75 | // New is a helper function that takes an arbitrary number of options and returns a GetSetCloser interface 76 | func New(opts ...Option) (GetSetCloser, error) { 77 | o := parseOptions(opts...) 78 | switch o.engine { 79 | case MemoryEngine: 80 | return NewMemoryStore(), nil 81 | case RedisEngine: 82 | return NewRedisStore(o.redis...), nil 83 | default: 84 | return nil, errInvalidStorage 85 | } 86 | } 87 | 88 | // WithEngine takes an Engine and returns an Option. 89 | func WithEngine(e Engine) Option { 90 | return func(o *options) { 91 | o.engine = e 92 | } 93 | } 94 | 95 | // WithRedisOptions takes an arbitrary number of RedisOption and returns a Option 96 | func WithRedisOptions(opts ...RedisOption) Option { 97 | return func(o *options) { 98 | o.redis = opts 99 | } 100 | } 101 | 102 | // safeTTL ensures that a submitted TTL is no less than 2x the SubmitWindow duration, to prevent replay attacks 103 | // it also ensures that the maxTTL is no greater than allowed. 104 | func safeTTL(ttl time.Duration) time.Duration { 105 | if ttl <= minTTL { 106 | return minTTL 107 | } 108 | if ttl > maxTTL { 109 | return maxTTL 110 | } 111 | return ttl 112 | } 113 | -------------------------------------------------------------------------------- /internal/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewStorage(t *testing.T) { 9 | t.Parallel() 10 | 11 | t.Run("memory storage", func(t *testing.T) { 12 | t.Parallel() 13 | 14 | if _, err := New(WithEngine(MemoryEngine)); err != nil { 15 | t.Error(err) 16 | } 17 | }) 18 | 19 | t.Run("redis storage", func(t *testing.T) { 20 | t.Parallel() 21 | 22 | if _, err := New( 23 | WithEngine(RedisEngine), 24 | WithRedisOptions(WithRedisEndpoint("")), 25 | ); err != nil { 26 | t.Error(err) 27 | } 28 | }) 29 | 30 | t.Run("invalid storage engine", func(t *testing.T) { 31 | t.Parallel() 32 | 33 | if _, err := New(WithEngine(0)); err == nil { 34 | t.Error("failed to error with invalid engine") 35 | } 36 | }) 37 | 38 | t.Run("safeTTL", func(t *testing.T) { 39 | t.Parallel() 40 | 41 | tests := []struct { 42 | timestamp time.Duration 43 | expected time.Duration 44 | message string 45 | }{ 46 | { 47 | timestamp: time.Second, 48 | expected: minTTL, 49 | message: "failed to enforce minTTL", 50 | }, 51 | { 52 | timestamp: maxTTL + time.Hour, 53 | expected: maxTTL, 54 | message: "failed to enforce maxTTL", 55 | }, 56 | { 57 | timestamp: minTTL + time.Minute, 58 | expected: minTTL + time.Minute, 59 | message: "failed to allow valid", 60 | }, 61 | } 62 | 63 | for _, test := range tests { 64 | t.Log(test.timestamp) 65 | t.Log(safeTTL(test.timestamp)) 66 | t.Log(test.expected) 67 | if safeTTL(test.timestamp) != test.expected { 68 | t.Error(test.message) 69 | } 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | // For more information, please refer to 25 | 26 | package main 27 | 28 | import "github.com/nomasters/hashmap/cmd" 29 | 30 | func main() { 31 | cmd.Execute() 32 | } 33 | 34 | /* 35 | 36 | This is a description of the CLI tool generally. 37 | 38 | 39 | hashmap version 40 | 41 | hashmap generate keys //default of ed25519,xmss_SHA2_10_256 and output current dir 42 | hashmap generate keys --types=ed25519,xmss_SHA2_10_256 43 | hashmap generate keys --output=/path 44 | 45 | hashmap generate payload --message="hello, world" --ttl=5s --timestamp=now --keys=/path 46 | 47 | $ hashmap analyze < payload 48 | - output as TOML by default, allow out 49 | Pubkey Hash: HASH 50 | version: v1 51 | timestamp: 1562017791651859000 52 | ttl: 86400 53 | sig bundles: 54 | sig1: 55 | alg: nacl_sign 56 | pub: HASH 57 | sig: HASH 58 | sig2: 59 | alg: xmss_sha2_10_256 60 | pub: HASH 61 | sig: HASH 62 | 63 | data: HASH 64 | 65 | Valid Payload: TRUE 66 | Valid Version: TRUE 67 | Valid Timestamp: TRUE 68 | Within Submission Window: FALSE 69 | Expired TTL: FALSE 70 | Valid Data Size: TRUE 71 | Valid Signatures: TRUE 72 | Verify sig1: TRUE 73 | Verify sig2: TRUE 74 | 75 | 76 | hashmap --ge 77 | 78 | 79 | - hashmap 80 | 81 | 82 | */ 83 | -------------------------------------------------------------------------------- /pkg/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/pkg/.DS_Store -------------------------------------------------------------------------------- /pkg/payload/json.go: -------------------------------------------------------------------------------- 1 | package payload 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | ) 7 | 8 | // Unmarshal takes encoded json and returns a Payload and error 9 | func Unmarshal(b []byte) (Payload, error) { 10 | var p Payload 11 | err := json.Unmarshal(b, &p) 12 | return p, err 13 | } 14 | 15 | // Marshal takes a payload and returns encoded JSON and error 16 | func Marshal(p Payload) ([]byte, error) { 17 | return json.Marshal(p) 18 | } 19 | 20 | // UnmarshalJSON unmarshals base64 encoded strings into Bytes 21 | func (b *Bytes) UnmarshalJSON(d []byte) error { 22 | var s string 23 | if err := json.Unmarshal(d, &s); err != nil { 24 | return err 25 | } 26 | data, err := base64.StdEncoding.DecodeString(s) 27 | if err != nil { 28 | return err 29 | } 30 | *b = data 31 | return nil 32 | } 33 | 34 | // MarshalJSON takes Bytes and returns a base64 encoded string 35 | func (b Bytes) MarshalJSON() ([]byte, error) { 36 | s := base64.StdEncoding.EncodeToString(b) 37 | return json.Marshal(s) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/payload/json_test.go: -------------------------------------------------------------------------------- 1 | package payload 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "testing" 7 | "time" 8 | 9 | sig "github.com/nomasters/hashmap/pkg/sig" 10 | ) 11 | 12 | func TestJSONMarshall(t *testing.T) { 13 | t.Parallel() 14 | 15 | m := []byte("sign me, plz.") 16 | s := []sig.Signer{sig.GenNaclSign()} 17 | d, err := Generate(m, s, WithTTL(5*time.Second)) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | blob, err := json.Marshal(d) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | t.Log(string(blob)) 26 | } 27 | 28 | func TestJSONUnmarshall(t *testing.T) { 29 | t.Parallel() 30 | valid := ` 31 | { 32 | "version":1, 33 | "timestamp":"2020-05-11T05:56:43.839934-06:00", 34 | "ttl":86400000000000, 35 | "sig_bundles":[ 36 | { 37 | "alg":1, 38 | "pub":"jpZdClX/d44twVle7gbVc61Ai4Tv8xSvHRAlhagbWq4=", 39 | "sig":"zi5awXZr85U6ecR/ffAuHFzeNmLWEkOW0as6GMJqhYLbLDn+dovqGJSoeJmbFJEUqK4+Q6gMPrXmi+ngXz/mCw==" 40 | } 41 | ], 42 | "data":"c2lnbiBtZSwgcGx6Lg==" 43 | }` 44 | invalidString := ` 45 | { 46 | "version":1, 47 | "timestamp":"2020-05-11T05:56:43.839934-06:00", 48 | "ttl":86400000000000, 49 | "sig_bundles":[ 50 | { 51 | "alg":1, 52 | "pub":"jpZdClX/d44twVle7gbVc61Ai4Tv8xSvHRAlhagbWq4=", 53 | "sig":"zi5awXZr85U6ecR/ffAuHFzeNmLWEkOW0as6GMJqhYLbLDn+dovqGJSoeJmbFJEUqK4+Q6gMPrXmi+ngXz/mCw==" 54 | } 55 | ], 56 | "data":"bad_string" 57 | }` 58 | invalidType := ` 59 | { 60 | "version":1, 61 | "timestamp":"2020-05-11T05:56:43.839934-06:00", 62 | "ttl":86400000000000, 63 | "sig_bundles":[ 64 | { 65 | "alg":1, 66 | "pub":"jpZdClX/d44twVle7gbVc61Ai4Tv8xSvHRAlhagbWq4=", 67 | "sig":"zi5awXZr85U6ecR/ffAuHFzeNmLWEkOW0as6GMJqhYLbLDn+dovqGJSoeJmbFJEUqK4+Q6gMPrXmi+ngXz/mCw==" 68 | } 69 | ], 70 | "data":1 71 | }` 72 | 73 | var p Payload 74 | if err := json.Unmarshal([]byte(valid), &p); err != nil { 75 | t.Error(err) 76 | } 77 | if err := json.Unmarshal([]byte(invalidString), &p); err == nil { 78 | t.Error("invalid pub encoding not caught") 79 | } 80 | if err := json.Unmarshal([]byte(invalidType), &p); err == nil { 81 | t.Error("invalid type encoding not caught") 82 | } 83 | t.Log(p.TTL) 84 | } 85 | 86 | func TestMarshal(t *testing.T) { 87 | t.Parallel() 88 | var signers []sig.Signer 89 | signers = append(signers, sig.GenNaclSign()) 90 | message := []byte("hello, world") 91 | 92 | t.Run("Normal Operation", func(t *testing.T) { 93 | p, err := Generate(message, signers) 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | if _, err := Marshal(p); err != nil { 98 | t.Error(err) 99 | } 100 | }) 101 | 102 | t.Run("Invalid Timestamp", func(t *testing.T) { 103 | bad := time.Unix(-99999999999, 0) 104 | p, err := Generate(message, signers, WithTimestamp(bad)) 105 | if err != nil { 106 | t.Error(err) 107 | } 108 | if _, err := Marshal(p); err == nil { 109 | t.Error("failed to catch invalid timestamp") 110 | } 111 | }) 112 | 113 | } 114 | 115 | func TestUnmarshal(t *testing.T) { 116 | t.Parallel() 117 | t.Run("Normal Operation", func(t *testing.T) { 118 | var signers []sig.Signer 119 | signers = append(signers, sig.GenNaclSign()) 120 | message := []byte("hello, world") 121 | p, err := Generate(message, signers) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | encoded, err := Marshal(p) 126 | if err != nil { 127 | t.Error(err) 128 | } 129 | if _, err := Unmarshal(encoded); err != nil { 130 | t.Error(err) 131 | } 132 | }) 133 | t.Run("invalid protobuf", func(t *testing.T) { 134 | invalidPayload, _ := hex.DecodeString("ffffffffffffff") 135 | // this is a valid payload protobuf with a malformed (by hand) timestamp to force out of range errors 136 | badTimestamp, _ := hex.DecodeString("12070880cdfdffaf071a080880aef188221001") 137 | // this is a valid payload protobuf with a malformed (by hand) ttl to force out of range errors 138 | badTTL, _ := hex.DecodeString("12070880cdfdefaf071a0808fffffff8221001") 139 | 140 | testTable := []struct { 141 | bytes []byte 142 | err string 143 | }{ 144 | { 145 | bytes: invalidPayload, 146 | err: "failed to catch malformed payload bytes", 147 | }, 148 | { 149 | bytes: badTimestamp, 150 | err: "failed to catch malformed timestamp", 151 | }, 152 | { 153 | bytes: badTTL, 154 | err: "failed to catch malformed ttl", 155 | }, 156 | } 157 | 158 | for _, test := range testTable { 159 | if _, err := Unmarshal(test.bytes); err == nil { 160 | t.Error(test.err) 161 | } 162 | } 163 | }) 164 | } 165 | -------------------------------------------------------------------------------- /pkg/payload/payload.go: -------------------------------------------------------------------------------- 1 | package payload 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "time" 8 | 9 | sig "github.com/nomasters/hashmap/pkg/sig" 10 | sigutil "github.com/nomasters/hashmap/pkg/sig/sigutil" 11 | ) 12 | 13 | // Version type is used for setting the hashmap implementation version. 14 | type Version uint16 15 | 16 | // Bytes is a byte slice with special json encoding properties 17 | type Bytes []byte 18 | 19 | const ( 20 | // V0 is deprecated and should be deemed invalid 21 | V0 Version = iota 22 | // V1 is the current version of the payload spec 23 | V1 24 | ) 25 | 26 | const ( 27 | // DefaultTTL is set to 24 hours 28 | DefaultTTL = 24 * time.Hour 29 | ) 30 | 31 | var ( 32 | defaultVersion = V1 33 | ) 34 | 35 | // Payload holds all information related to a Hashmap Payload that will be handled 36 | // for signing and validation. This struct is used by both client and server and 37 | // includes all necessary methods for encoding, decoding, signing, an verifying itself. 38 | type Payload struct { 39 | Version Version `json:"version"` 40 | Timestamp time.Time `json:"timestamp"` 41 | TTL time.Duration `json:"ttl"` 42 | SigBundles []sig.Bundle `json:"sig_bundles"` 43 | Data Bytes `json:"data"` 44 | } 45 | 46 | // Option is used for interacting with Context when setting options for Generate and Verify 47 | type Option func(*options) 48 | 49 | // options contains private fields used for Option 50 | type options struct { 51 | version Version 52 | timestamp time.Time 53 | ttl time.Duration 54 | validate validateContext 55 | } 56 | 57 | // WithVersion takes a Version and returns an Option 58 | func WithVersion(v Version) Option { 59 | return func(o *options) { 60 | o.version = v 61 | } 62 | } 63 | 64 | // WithTimestamp takes a time.Time and returns an Option 65 | func WithTimestamp(t time.Time) Option { 66 | return func(o *options) { 67 | o.timestamp = t 68 | } 69 | } 70 | 71 | // WithTTL takes a time.Duration and returns an Option 72 | func WithTTL(d time.Duration) Option { 73 | return func(o *options) { 74 | o.ttl = d 75 | } 76 | } 77 | 78 | // parseOptions takes a arbitrary number of Option funcs and returns a Context with defaults 79 | // for version, timestamp, and ttl, and validate rules. 80 | func parseOptions(opts ...Option) options { 81 | now := time.Now() 82 | 83 | o := options{ 84 | version: defaultVersion, 85 | timestamp: now, 86 | ttl: DefaultTTL, 87 | validate: validateContext{ 88 | ttl: true, 89 | expiration: true, 90 | payloadSize: true, 91 | dataSize: true, 92 | version: true, 93 | submitTime: false, 94 | futureTime: true, 95 | referenceTime: now, 96 | }, 97 | } 98 | for _, opt := range opts { 99 | opt(&o) 100 | } 101 | return o 102 | } 103 | 104 | // Generate takes a message, signers, and a set of options and returns a payload or error. 105 | // This function defaults to time.Now() and the default TTL of 24 hours. Generate Requires 106 | // at least one signer, but can sign with many signers. Sort order is important though, The unique 107 | // order of the signers pubkeys are what is responsible for generating the endpoint hash. 108 | func Generate(message []byte, signers []sig.Signer, opts ...Option) (Payload, error) { 109 | if len(signers) == 0 { 110 | return Payload{}, errors.New("Generate must have at least one signer") 111 | } 112 | o := parseOptions(opts...) 113 | p := Payload{ 114 | Version: o.version, 115 | Timestamp: o.timestamp, 116 | TTL: o.ttl, 117 | Data: message, 118 | } 119 | 120 | sigBundles, err := sigutil.SignAll(p.SigningBytes(), signers) 121 | if err != nil { 122 | return Payload{}, err 123 | } 124 | p.SigBundles = sigBundles 125 | 126 | return p, nil 127 | } 128 | 129 | // SigningBytes returns a byte slice of version|timestamp|ttl|len|data used as 130 | // the message to be signed by a Signer. 131 | func (p Payload) SigningBytes() []byte { 132 | j := [][]byte{ 133 | uint64ToBytes(uint64(p.Version)), 134 | uint64ToBytes(uint64(p.Timestamp.UnixNano())), 135 | uint64ToBytes(uint64(p.TTL.Nanoseconds())), 136 | uint64ToBytes(uint64(len(p.Data))), 137 | p.Data, 138 | } 139 | return bytes.Join(j, []byte{}) 140 | } 141 | 142 | // PubKeyBytes returns a byte slice of all pubkeys concatenated in the index 143 | // order of the slice of sig.Bundles. This is intended to be used with a hash 144 | // function to derive the unique endpoint for a payload on hashmap server. 145 | func (p Payload) PubKeyBytes() []byte { 146 | return sigutil.BundlePubKeys(p.SigBundles) 147 | } 148 | 149 | // PubKeyHash returns a byte slice of the blake2b-512 hash of PubKeyBytes 150 | func (p Payload) PubKeyHash() []byte { 151 | return sigutil.BundleHash(p.SigBundles) 152 | } 153 | 154 | // Endpoint returns a url-safe base64 encoded endpoint string of PubKeyHash 155 | func (p Payload) Endpoint() string { 156 | return sigutil.EncodedBundleHash(p.SigBundles) 157 | } 158 | 159 | // uint64ToBytes converts uint64 numbers into a byte slice in Big Endian format 160 | func uint64ToBytes(t uint64) []byte { 161 | b := make([]byte, 8) 162 | binary.BigEndian.PutUint64(b, t) 163 | return b 164 | } 165 | -------------------------------------------------------------------------------- /pkg/payload/payload_test.go: -------------------------------------------------------------------------------- 1 | package payload 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | "time" 7 | 8 | sig "github.com/nomasters/hashmap/pkg/sig" 9 | blake2b "golang.org/x/crypto/blake2b" 10 | ) 11 | 12 | func TestGenerate(t *testing.T) { 13 | t.Parallel() 14 | 15 | var signers []sig.Signer 16 | m := []byte("hello, world") 17 | 18 | t.Run("Normal Operation", func(t *testing.T) { 19 | // single sig 20 | s1 := append(signers, sig.GenNaclSign()) 21 | if _, err := Generate(m, s1); err != nil { 22 | t.Error(err) 23 | } 24 | // multisig 25 | s2 := append(signers, sig.GenNaclSign(), sig.GenNaclSign()) 26 | if _, err := Generate(m, s2); err != nil { 27 | t.Error(err) 28 | } 29 | }) 30 | t.Run("With Options", func(t *testing.T) { 31 | s := append(signers, sig.GenNaclSign()) 32 | ttl := 5 * time.Second 33 | timestamp := time.Now().Add(5 * time.Minute) 34 | version := Version(2) 35 | 36 | p, err := Generate(m, s, 37 | WithTTL(ttl), 38 | WithTimestamp(timestamp), 39 | WithVersion(version), 40 | ) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | if p.TTL != ttl { 45 | t.Error("ttl mismatch") 46 | } 47 | if p.Timestamp != timestamp { 48 | t.Error("timestamp mismatch") 49 | } 50 | if p.Version != version { 51 | t.Error("version mismatch") 52 | } 53 | 54 | }) 55 | t.Run("Empty Signers", func(t *testing.T) { 56 | if _, err := Generate(m, signers); err == nil { 57 | t.Error("should reject on missing signers") 58 | } 59 | }) 60 | t.Run("Malformed Signer", func(t *testing.T) { 61 | bad := &sig.NaClSign{PrivateKey: [64]byte{}} 62 | s := append(signers, bad) 63 | if _, err := Generate(m, s); err == nil { 64 | t.Error("should reject a malformed signer") 65 | } 66 | }) 67 | 68 | } 69 | 70 | func TestPayloadCoreMethods(t *testing.T) { 71 | t.Parallel() 72 | 73 | // PubKeyBytes tests that the PubKeyBytes method properly generates a concat of 74 | // all public keys in proper order 75 | t.Run("PubKeyBytes", func(t *testing.T) { 76 | var signers []sig.Signer 77 | sig1, sig2, sig3 := sig.GenNaclSign(), sig.GenNaclSign(), sig.GenNaclSign() 78 | signers = append(signers, sig1, sig2, sig3) 79 | message := []byte("") 80 | p, err := Generate(message, signers) 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | var naclSigns []*sig.NaClSign 85 | var refConcat []byte 86 | naclSigns = append(naclSigns, sig1, sig2, sig3) 87 | for _, s := range naclSigns { 88 | pubkeyBytes := s.PrivateKey[32:] 89 | refConcat = append(refConcat, pubkeyBytes...) 90 | } 91 | if !bytes.Equal(refConcat, p.PubKeyBytes()) { 92 | t.Error("concatenated pubkey bytes are invalid") 93 | } 94 | }) 95 | t.Run("PubKeyHash", func(t *testing.T) { 96 | var signers []sig.Signer 97 | sig1 := sig.GenNaclSign() 98 | signers = append(signers, sig1) 99 | message := []byte("") 100 | p, err := Generate(message, signers) 101 | if err != nil { 102 | t.Error(err) 103 | } 104 | b := blake2b.Sum512(sig1.PrivateKey[32:]) 105 | if !bytes.Equal(b[:], p.PubKeyHash()) { 106 | t.Error("pubkey hash bytes are invalid") 107 | } 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /pkg/payload/verify.go: -------------------------------------------------------------------------------- 1 | package payload 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | sigutil "github.com/nomasters/hashmap/pkg/sig/sigutil" 9 | ) 10 | 11 | const ( 12 | // MaxSigBundleCount is the upper limit of signatures 13 | // allowed for a single payload 14 | MaxSigBundleCount = 4 15 | // MaxPayloadSize is intended to by used by a LimitedReader 16 | // to enforce a strict upper limit on payload size 17 | MaxPayloadSize = 128 * 1024 // 128 KB 18 | // MaxMessageSize Payload.Data 19 | MaxMessageSize = 512 // bytes 20 | // MaxSubmitWindow is the time drift allow between a submission 21 | // to hashmap server and the time reflected on a signed payload 22 | MaxSubmitWindow = 5 * time.Second 23 | // MinTTL is the minimum value of a TTL for a payload 24 | MinTTL = 0 * time.Second 25 | // MaxTTL is the maximum value of a TTL for a payload 26 | MaxTTL = 24 * 7 * time.Hour // 1 week 27 | ) 28 | 29 | // validateContext is used for interacting with options 30 | type validateContext struct { 31 | endpoint string 32 | ttl bool 33 | expiration bool 34 | payloadSize bool 35 | dataSize bool 36 | version bool 37 | submitTime bool 38 | futureTime bool 39 | referenceTime time.Time 40 | } 41 | 42 | // WithValidateEndpoint sets the endpoint string for options.validate.endpoint and is 43 | // used for the Verify method. endpoint defaults to and empty string. 44 | func WithValidateEndpoint(e string) Option { 45 | return func(o *options) { 46 | o.validate.endpoint = e 47 | } 48 | } 49 | 50 | // WithReferenceTime sets time for options.validate.referenceTime and is used for 51 | // the Verify method. referenceTime defaults to time.Now 52 | func WithReferenceTime(t time.Time) Option { 53 | return func(o *options) { 54 | o.validate.referenceTime = t 55 | } 56 | } 57 | 58 | // WithServerMode sets options.validate.submitTime boolean. Defaults to false. 59 | // Setting to false will skip validation when using the payload Verify method 60 | func WithServerMode(b bool) Option { 61 | return func(o *options) { 62 | o.validate.submitTime = b 63 | } 64 | } 65 | 66 | // WithValidateTTL sets options.validate.ttl boolean. Defaults to true. 67 | // Setting to false will skip validation when using the payload Verify method 68 | func WithValidateTTL(b bool) Option { 69 | return func(o *options) { 70 | o.validate.ttl = b 71 | } 72 | } 73 | 74 | // WithValidateExpiration sets options.validate.expiration boolean. Defaults to true. 75 | // Setting to false will skip validation when using the payload Verify method 76 | func WithValidateExpiration(b bool) Option { 77 | return func(o *options) { 78 | o.validate.expiration = b 79 | } 80 | } 81 | 82 | // WithValidateFuture sets options.validate.futureTime boolean. Defaults to true. 83 | // Setting to false will skip validation when using the payload Verify method 84 | func WithValidateFuture(b bool) Option { 85 | return func(o *options) { 86 | o.validate.futureTime = b 87 | } 88 | } 89 | 90 | // WithValidateDataSize sets options.validate.dataSize boolean. Defaults to true. 91 | // Setting to false will skip validation when using the payload Verify method 92 | func WithValidateDataSize(b bool) Option { 93 | return func(o *options) { 94 | o.validate.dataSize = b 95 | } 96 | } 97 | 98 | // WithValidatePayloadSize sets options.validate.payloadSize boolean. Defaults to true. 99 | // Setting to false will skip validation when using the payload Verify method 100 | func WithValidatePayloadSize(b bool) Option { 101 | return func(o *options) { 102 | o.validate.payloadSize = b 103 | } 104 | } 105 | 106 | // WithValidateVersion sets options.validate.version boolean. Defaults to true. 107 | // Setting to false will skip validation when using the payload Verify method 108 | func WithValidateVersion(b bool) Option { 109 | return func(o *options) { 110 | o.validate.version = b 111 | } 112 | } 113 | 114 | // Verify method takes a set of options and implements the Verify function 115 | func (p Payload) Verify(options ...Option) error { 116 | return verify(p, options...) 117 | } 118 | 119 | // verify takes a payload and set of options and validates 120 | // and verifies the payload. By default Verify runs in client mode 121 | // which means Verification passes without verifying the submitWindow. 122 | // Any host planning to store hashmap payloads should run WithServerMode 123 | func verify(p Payload, options ...Option) error { 124 | 125 | if err := validate(p, options...); err != nil { 126 | return fmt.Errorf("validation error: %v", err) 127 | } 128 | 129 | if ok := p.VerifySignatures(); !ok { 130 | return errors.New("failed signature verification") 131 | } 132 | 133 | return nil 134 | } 135 | 136 | // validate takes a payload and set of options and validates 137 | // the payload itself. This ensures it meets size and version 138 | // requirements 139 | func validate(p Payload, opts ...Option) error { 140 | o := parseOptions(opts...) 141 | 142 | if o.validate.endpoint != "" { 143 | if !p.ValidEndpoint(o.validate.endpoint) { 144 | return errors.New("invalid endpoint") 145 | } 146 | } 147 | 148 | if o.validate.payloadSize { 149 | if !p.ValidPayloadSize() { 150 | return errors.New("MaxPayloadSize exceeded") 151 | } 152 | } 153 | if o.validate.dataSize { 154 | if !p.ValidDataSize() { 155 | return errors.New("MaxMessageSize exceeded") 156 | } 157 | } 158 | if o.validate.version { 159 | if !p.ValidVersion() { 160 | return errors.New("invalid payload version") 161 | } 162 | } 163 | if o.validate.expiration { 164 | if p.IsExpired(o.validate.referenceTime) { 165 | return errors.New("payload ttl is expired") 166 | } 167 | } 168 | if o.validate.ttl { 169 | if !p.ValidTTL() { 170 | return errors.New("invalid payload ttl") 171 | } 172 | } 173 | if o.validate.futureTime { 174 | if p.IsInFuture(o.validate.referenceTime) { 175 | return errors.New("payload timestamp is too far in the future") 176 | } 177 | } 178 | if o.validate.submitTime { 179 | if !p.WithinSubmitWindow(o.validate.referenceTime) { 180 | return errors.New("timestamp is outside of submit window") 181 | } 182 | } 183 | return nil 184 | } 185 | 186 | // ValidEndpoint takes a string and attempts to match the URL safe 187 | // base64 string encoded PubKeyHash and returns a boolean 188 | func (p Payload) ValidEndpoint(e string) bool { 189 | return e == p.Endpoint() 190 | } 191 | 192 | // ValidTTL checks that a TTL falls within an acceptable range. 193 | func (p Payload) ValidTTL() bool { 194 | return !(p.TTL < MinTTL || p.TTL > MaxTTL) 195 | } 196 | 197 | // IsExpired checks the reference time t against the timestamp and 198 | // TTL of a payload and returns a boolean value on whether 199 | // or not the TTL has been exceeded 200 | func (p Payload) IsExpired(t time.Time) bool { 201 | return t.Sub(p.Timestamp) > p.TTL 202 | } 203 | 204 | // IsInFuture checks if the payload timestamp is too far into the future based 205 | // on the reference time t plus the MaxSubmitWindow. 206 | func (p Payload) IsInFuture(t time.Time) bool { 207 | return p.Timestamp.UnixNano() > t.Add(MaxSubmitWindow).UnixNano() 208 | } 209 | 210 | // ValidVersion returns whether version is supported by Hashmap 211 | // Currently only V1 is supported. 212 | func (p Payload) ValidVersion() bool { 213 | switch p.Version { 214 | case V1: 215 | return true 216 | } 217 | return false 218 | } 219 | 220 | // ValidDataSize checks that the length of Payload.Data is less than or equal 221 | // to the MaxMessageSize and returns a boolean value. 222 | func (p Payload) ValidDataSize() bool { 223 | return len(p.Data) <= MaxMessageSize 224 | } 225 | 226 | // ValidPayloadSize checks that the wire protocol bytes are less than or equal 227 | // to the MaxPayloadSize allowed and returns a boolean value. 228 | func (p Payload) ValidPayloadSize() bool { 229 | b, err := Marshal(p) 230 | if err != nil { 231 | return false 232 | } 233 | if len(b) > MaxPayloadSize { 234 | return false 235 | } 236 | return true 237 | } 238 | 239 | // WithinSubmitWindow checks reference time t against the payload timestamp, 240 | // validates that it exists within the MaxSubmitWindow and returns a boolean. 241 | func (p Payload) WithinSubmitWindow(t time.Time) bool { 242 | diff := t.Sub(p.Timestamp) 243 | 244 | // get absolute value of time difference 245 | if diff.Seconds() < 0 { 246 | diff = -diff 247 | } 248 | return diff <= MaxSubmitWindow 249 | } 250 | 251 | // VerifySignatures checks all signatures in the sigBundles. If all signatures 252 | // are valid, it returns `true`. 253 | func (p Payload) VerifySignatures() bool { 254 | return sigutil.VerifyAll(p.SigningBytes(), p.SigBundles) 255 | } 256 | -------------------------------------------------------------------------------- /pkg/payload/verify_test.go: -------------------------------------------------------------------------------- 1 | package payload 2 | 3 | import ( 4 | "encoding/base64" 5 | "testing" 6 | "time" 7 | 8 | sig "github.com/nomasters/hashmap/pkg/sig" 9 | ) 10 | 11 | func TestVerify(t *testing.T) { 12 | t.Parallel() 13 | 14 | var signers []sig.Signer 15 | signers = append(signers, sig.GenNaclSign()) 16 | message := []byte("hello, world") 17 | now := time.Now() 18 | p, _ := Generate(message, signers, WithTimestamp(now)) 19 | 20 | t.Run("Normal Operation", func(t *testing.T) { 21 | if err := p.Verify(); err != nil { 22 | t.Error(err) 23 | } 24 | }) 25 | t.Run("Failed Validation", func(t *testing.T) { 26 | err := p.Verify( 27 | WithReferenceTime(now.Add(15*time.Second)), 28 | WithServerMode(true), 29 | ) 30 | if err == nil { 31 | t.Error("Verify did not catch invalid payload") 32 | } 33 | }) 34 | t.Run("Failed Verification", func(t *testing.T) { 35 | p.SigBundles[0].Sig = []byte("bad_bytes") 36 | err := p.Verify() 37 | if err == nil { 38 | t.Error("Verify did not catch invalid signature") 39 | } 40 | }) 41 | } 42 | 43 | func TestPayloadMethods(t *testing.T) { 44 | t.Parallel() 45 | var signers []sig.Signer 46 | signers = append(signers, sig.GenNaclSign()) 47 | now := time.Now() 48 | message := []byte("hello, world") 49 | t.Run("validPayloadSize Bad Marshal", func(t *testing.T) { 50 | bad := time.Unix(-99999999999, 0) 51 | p, err := Generate(message, signers, WithTimestamp(bad)) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | if p.ValidPayloadSize() { 56 | t.Error("failed to catch marshalling error") 57 | } 58 | }) 59 | t.Run("withinSubmitWindow negative diff", func(t *testing.T) { 60 | p, err := Generate(message, signers, WithTimestamp(now)) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | if !p.WithinSubmitWindow(now.Add(-3 * time.Second)) { 65 | t.Fail() 66 | } 67 | if p.WithinSubmitWindow(now.Add(-10 * time.Second)) { 68 | t.Fail() 69 | } 70 | }) 71 | } 72 | 73 | func TestValidate(t *testing.T) { 74 | t.Parallel() 75 | var signers []sig.Signer 76 | s1 := sig.GenNaclSign() 77 | signers = append(signers, s1) 78 | now := time.Now() 79 | message := []byte("hello, world") 80 | p, _ := Generate(message, signers, WithTimestamp(now)) 81 | 82 | t.Run("Normal Operation", func(t *testing.T) { 83 | if err := validate(p); err != nil { 84 | t.Error(err) 85 | } 86 | }) 87 | t.Run("Endpoint", func(t *testing.T) { 88 | p, _ := Generate(message, signers, WithTimestamp(now)) 89 | e := base64.URLEncoding.EncodeToString(p.PubKeyHash()) 90 | if err := validate(p, WithValidateEndpoint(e)); err != nil { 91 | t.Error(err) 92 | } 93 | if err := validate(p, WithValidateEndpoint("BAD_ENDPOINT")); err == nil { 94 | t.Error("validate did not catch MaxMessageSize") 95 | } 96 | if err := validate(p, WithValidateEndpoint("")); err != nil { 97 | t.Error(err) 98 | } 99 | }) 100 | t.Run("Data Size", func(t *testing.T) { 101 | message := make([]byte, MaxMessageSize+1) 102 | p, _ := Generate(message, signers, WithTimestamp(now)) 103 | if err := validate(p, WithValidateDataSize(true)); err == nil { 104 | t.Error("validate did not catch MaxMessageSize") 105 | } 106 | if err := validate(p, WithValidateDataSize(false)); err != nil { 107 | t.Error(err) 108 | } 109 | }) 110 | t.Run("Payload Size", func(t *testing.T) { 111 | message := make([]byte, MaxPayloadSize+1) 112 | p, _ := Generate(message, signers, WithTimestamp(now)) 113 | if err := validate(p, 114 | WithValidatePayloadSize(true), 115 | WithValidateDataSize(false)); err == nil { 116 | t.Error("validate did not catch MaxPayloadSize") 117 | } 118 | if err := validate(p, 119 | WithValidatePayloadSize(false), 120 | WithValidateDataSize(false)); err != nil { 121 | t.Error(err) 122 | } 123 | }) 124 | t.Run("Version", func(t *testing.T) { 125 | p, _ := Generate(message, signers, 126 | WithTimestamp(now), 127 | WithVersion(Version(3))) 128 | if err := validate(p, WithValidateVersion(true)); err == nil { 129 | t.Error("validate did not catch bad version") 130 | } 131 | if err := validate(p, WithValidateVersion(false)); err != nil { 132 | t.Error(err) 133 | } 134 | }) 135 | t.Run("Future", func(t *testing.T) { 136 | p, _ := Generate(message, signers, WithTimestamp(now.Add(MaxSubmitWindow+1))) 137 | if err := validate(p, 138 | WithValidateFuture(true), 139 | WithReferenceTime(now)); err == nil { 140 | t.Error("validate did not catch timestamp in future") 141 | } 142 | if err := validate(p, 143 | WithValidateFuture(false), 144 | WithReferenceTime(now)); err != nil { 145 | t.Error(err) 146 | } 147 | }) 148 | t.Run("TTL", func(t *testing.T) { 149 | p, _ := Generate(message, signers, WithTTL(MaxTTL+1)) 150 | if err := validate(p, WithValidateTTL(true)); err == nil { 151 | t.Error("validate did not catch bad ttl") 152 | } 153 | if err := validate(p, WithValidateTTL(false)); err != nil { 154 | t.Error(err) 155 | } 156 | }) 157 | t.Run("expiration", func(t *testing.T) { 158 | p, _ := Generate( 159 | message, 160 | signers, 161 | WithTTL(10*time.Second), 162 | WithTimestamp(now.Add(-11*time.Second))) 163 | if err := validate(p, WithValidateExpiration(true)); err == nil { 164 | t.Error("validate did not catch expired ttl") 165 | } 166 | if err := validate(p, WithValidateExpiration(false)); err != nil { 167 | t.Error(err) 168 | } 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /pkg/sig/json.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | ) 7 | 8 | // UnmarshalJSON unmarshals base64 encoded strings into Bytes 9 | func (b *Bytes) UnmarshalJSON(d []byte) error { 10 | var s string 11 | if err := json.Unmarshal(d, &s); err != nil { 12 | return err 13 | } 14 | data, err := base64.StdEncoding.DecodeString(s) 15 | if err != nil { 16 | return err 17 | } 18 | *b = data 19 | return nil 20 | } 21 | 22 | // UnmarshalJSON unmarshals base64 encoded strings into Bytes 23 | func (b Bytes) MarshalJSON() ([]byte, error) { 24 | s := base64.StdEncoding.EncodeToString(b) 25 | return json.Marshal(s) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/sig/json_test.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestJSONEncoding(t *testing.T) { 9 | t.Parallel() 10 | s := GenNaclSign() 11 | m := []byte("sign me, plz.") 12 | b, _ := s.Sign(m) 13 | if _, err := json.Marshal(b); err != nil { 14 | t.Error(err) 15 | } 16 | } 17 | 18 | func TestUnmarshall(t *testing.T) { 19 | t.Parallel() 20 | valid := ` 21 | { 22 | "alg":1, 23 | "pub":"qOQp8p/ZheaTHhlJ90TQoHBNKQ7BXGC2FHOjNHfGH0M=", 24 | "sig":"4xb4Zgr9lGN3imfiGezH36fby6cktqrpRL6m5yluqSn83r+HMONhQ5722BDgF5Cb4xX9GVUiWo6I/zFOeGlTBQ==" 25 | }` 26 | invalidString := ` 27 | { 28 | "alg":1, 29 | "pub":"invalid", 30 | "sig":"4xb4Zgr9lGN3imfiGezH36fby6cktqrpRL6m5yluqSn83r+HMONhQ5722BDgF5Cb4xX9GVUiWo6I/zFOeGlTBQ==" 31 | }` 32 | invalidType := ` 33 | { 34 | "alg":1, 35 | "pub":1, 36 | "sig":"4xb4Zgr9lGN3imfiGezH36fby6cktqrpRL6m5yluqSn83r+HMONhQ5722BDgF5Cb4xX9GVUiWo6I/zFOeGlTBQ==" 37 | }` 38 | 39 | var b Bundle 40 | if err := json.Unmarshal([]byte(valid), &b); err != nil { 41 | t.Error(err) 42 | } 43 | if err := json.Unmarshal([]byte(invalidString), &b); err == nil { 44 | t.Error("invalid pub encoding not caught") 45 | } 46 | if err := json.Unmarshal([]byte(invalidType), &b); err == nil { 47 | t.Error("invalid type encoding not caught") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/sig/nacl_sign.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | 7 | "golang.org/x/crypto/nacl/sign" 8 | ) 9 | 10 | // NaClSign holds a pointer to a 64 byte array used by NaCl Sign. It implements the 11 | // Signer interface. 12 | type NaClSign struct { 13 | PrivateKey [64]byte 14 | } 15 | 16 | // GenNaclSign returns a randomly generated ed25519 private key in a NaClSign 17 | func GenNaclSign() *NaClSign { 18 | _, k, _ := sign.GenerateKey(rand.Reader) 19 | return NewNaClSign(k[:]) 20 | } 21 | 22 | // NewNaClSign takes a ed25519 private key as a byte array and returns a NaClSign 23 | func NewNaClSign(privateKey []byte) *NaClSign { 24 | var pk [64]byte 25 | copy(pk[:], privateKey) 26 | return &NaClSign{ 27 | PrivateKey: pk, 28 | } 29 | } 30 | 31 | // Sign takes a message and returns a Bundle signed with a private key using NaCl Sign. 32 | func (s *NaClSign) Sign(message []byte) (Bundle, error) { 33 | pub := make([]byte, 32) 34 | copy(pub, s.PrivateKey[32:]) 35 | 36 | sig := sign.Sign(nil, message, &s.PrivateKey) 37 | 38 | b := Bundle{ 39 | Alg: AlgNaClSign, 40 | Pub: pub, 41 | Sig: sig[:sign.Overhead], 42 | } 43 | 44 | if ok := VerifyNaclSign(message, b); !ok { 45 | return Bundle{}, errors.New("verification sanity check failed on sign") 46 | } 47 | 48 | return b, nil 49 | } 50 | 51 | // VerifyNaclSign takes a message and a Bundle and bool indicating if the message 52 | // is verified ny the signature. 53 | func VerifyNaclSign(msg []byte, b Bundle) bool { 54 | sig := append(b.Sig, msg...) 55 | var pubkey [32]byte 56 | copy(pubkey[:], b.Pub) 57 | _, ok := sign.Open(nil, sig, &pubkey) 58 | return ok 59 | } 60 | -------------------------------------------------------------------------------- /pkg/sig/nacl_sign_test.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestSign(t *testing.T) { 9 | t.Parallel() 10 | 11 | m := []byte("sign me, plz.") 12 | s := GenNaclSign() 13 | 14 | t.Run("normal", func(t *testing.T) { 15 | if _, err := s.Sign(m); err != nil { 16 | t.Error(err) 17 | } 18 | }) 19 | t.Run("malformed", func(t *testing.T) { 20 | badKey := bytes.Repeat([]byte{5}, 64) 21 | copy(s.PrivateKey[:], badKey) 22 | if _, err := s.Sign(m); err == nil { 23 | t.Error("malformed signature not caught") 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/sig/sig.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | // Alg type is used for setting the Algorithm in a Signature Set 4 | type Alg uint16 5 | 6 | // Bytes is a byte slice with special json encoding properties 7 | type Bytes []byte 8 | 9 | const ( 10 | // 0 value skipped to handle empty Payload validation 11 | _ Alg = iota 12 | // AlgNaClSign is meant for Nacl Sign implementations 13 | AlgNaClSign 14 | // AlgXMSS10 is meant for xmss sha2_10_256 15 | AlgXMSS10 16 | ) 17 | 18 | // Bundle is used to encapsulate an Algorithm implementation, A Public Key, and a Signature. 19 | // A Bundle is designed to be used to verify the integrity of the Payload. 20 | type Bundle struct { 21 | Alg Alg `json:"alg"` 22 | Pub Bytes `json:"pub"` 23 | Sig Bytes `json:"sig"` 24 | } 25 | 26 | // Signer is an interface for signing messages and generating a SigSet. 27 | type Signer interface { 28 | Sign(message []byte) (Bundle, error) 29 | } 30 | 31 | // Verify takes a message and a signature Bundle and attempts to verify 32 | // the bundle based on bundle's implemented Alg, the sig, and the pubkey. 33 | // Verify returns a simple true or false. 34 | func Verify(message []byte, bundle Bundle) bool { 35 | switch bundle.Alg { 36 | case AlgNaClSign: 37 | return VerifyNaclSign(message, bundle) 38 | case AlgXMSS10: 39 | return VerifyXMSS10(message, bundle) 40 | } 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /pkg/sig/sig_test.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestVerify(t *testing.T) { 8 | t.Parallel() 9 | 10 | m := []byte("sign me, plz.") 11 | 12 | t.Run("naclSign", func(t *testing.T) { 13 | t.Parallel() 14 | s := GenNaclSign() 15 | b, _ := s.Sign(m) 16 | if ok := Verify(m, b); !ok { 17 | t.Error("verification failed") 18 | } 19 | }) 20 | t.Run("XMSS10", func(t *testing.T) { 21 | t.Parallel() 22 | s := GenXMSS10() 23 | b, _ := s.Sign(m) 24 | if ok := Verify(m, b); !ok { 25 | t.Error("verification failed") 26 | } 27 | }) 28 | t.Run("invalidAlg", func(t *testing.T) { 29 | t.Parallel() 30 | b := Bundle{ 31 | Alg: 0, 32 | } 33 | if ok := Verify(m, b); ok { 34 | t.Error("verification should fail") 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/sig/sigutil/sigutil.go: -------------------------------------------------------------------------------- 1 | package sigutil 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/gob" 7 | 8 | sig "github.com/nomasters/hashmap/pkg/sig" 9 | blake2b "golang.org/x/crypto/blake2b" 10 | ) 11 | 12 | func init() { 13 | gob.Register(&sig.NaClSign{}) 14 | gob.Register(&sig.XMSS10{}) 15 | } 16 | 17 | // NewDefaultSigners returns a slice of sig.Signer that 18 | // includes an ed25519 sig 19 | func NewDefaultSigners() (s []sig.Signer) { 20 | return append(s, sig.GenNaclSign()) 21 | } 22 | 23 | // NewExperimentalSigners returns a slice of sig.Signer that 24 | // includes an ed25519 sig and an XMSS sig 25 | func NewExperimentalSigners() (s []sig.Signer) { 26 | return append(s, sig.GenNaclSign(), sig.GenXMSS10()) 27 | } 28 | 29 | // Encode takes a slice of sig.Signer and returns a gob 30 | // encoded byte slice and an error 31 | func Encode(s []sig.Signer) ([]byte, error) { 32 | var buffer bytes.Buffer 33 | err := gob.NewEncoder(&buffer).Encode(s) 34 | return buffer.Bytes(), err 35 | } 36 | 37 | // Decode takes a gob encoded byte slice and returns 38 | // a []sig.Signer and an error. 39 | func Decode(b []byte) (s []sig.Signer, err error) { 40 | var buffer bytes.Buffer 41 | buffer.Write(b) 42 | err = gob.NewDecoder(&buffer).Decode(&s) 43 | return s, err 44 | } 45 | 46 | // SignAll is a helper function for quickly signing a byteslice with a group 47 | // of signers. It takes a slice of sig.Signers and the bytes for signing and 48 | // returns a slice of sig.Bundles and an error 49 | func SignAll(message []byte, signers []sig.Signer) ([]sig.Bundle, error) { 50 | var sigBundles []sig.Bundle 51 | for _, s := range signers { 52 | bundle, err := s.Sign(message) 53 | if err != nil { 54 | return nil, err 55 | } 56 | sigBundles = append(sigBundles, bundle) 57 | } 58 | return sigBundles, nil 59 | } 60 | 61 | // VerifyAll takes message bytes and a slice of sig.Bundles and returns a boolean 62 | // value of true if all signatures verify, otherwise it returns false 63 | func VerifyAll(message []byte, bundles []sig.Bundle) bool { 64 | for _, bundle := range bundles { 65 | if !sig.Verify(message, bundle) { 66 | return false 67 | } 68 | } 69 | return true 70 | } 71 | 72 | // BundlePubKeys returns a byte slice of all pubkeys concatenated in the index 73 | // order of the slice of sig.Bundles. 74 | func BundlePubKeys(bundles []sig.Bundle) []byte { 75 | var o []byte 76 | for _, b := range bundles { 77 | o = append(o, b.Pub...) 78 | } 79 | return o 80 | } 81 | 82 | // BundleHash returns a byte slice of the blake2b-512 hash of PubKeys 83 | func BundleHash(bundles []sig.Bundle) []byte { 84 | b := blake2b.Sum512(BundlePubKeys(bundles)) 85 | return b[:] 86 | } 87 | 88 | // EncodedBundleHash returns a url-safe base64 encoded endpoint string of PubKeyHash 89 | func EncodedBundleHash(bundles []sig.Bundle) string { 90 | return base64.URLEncoding.EncodeToString(BundleHash(bundles)) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/sig/sigutil/sigutil_test.go: -------------------------------------------------------------------------------- 1 | package sigutil 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | sig "github.com/nomasters/hashmap/pkg/sig" 8 | // sig "github.com/nomasters/hashmap/pkg/sig" 9 | ) 10 | 11 | func TestEncodeDecode(t *testing.T) { 12 | t.Parallel() 13 | 14 | s := NewDefaultSigners() 15 | b, err := Encode(s) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | 20 | o, err := Decode(b) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | m := []byte("hello, world.") 25 | sb, err := s[0].Sign(m) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | 30 | ob, err := o[0].Sign(m) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | if !bytes.Equal(sb.Sig, ob.Sig) { 36 | t.Log("source: ", sb.Sig) 37 | t.Log("decoded: ", ob.Sig) 38 | t.Error("Decode signature mismatch") 39 | } 40 | } 41 | 42 | func TestSignAll(t *testing.T) { 43 | t.Parallel() 44 | s := NewDefaultSigners() 45 | m := []byte("hello, world") 46 | if _, err := SignAll(m, s); err != nil { 47 | t.Error(err) 48 | } 49 | } 50 | 51 | func TestVerifyAll(t *testing.T) { 52 | t.Parallel() 53 | s := NewDefaultSigners() 54 | m := []byte("hello, world") 55 | b, err := s[0].Sign(m) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | if !VerifyAll(m, []sig.Bundle{b}) { 60 | t.Error("VerifyAll failed for", b) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/sig/xmss.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | xmss "github.com/danielhavir/go-xmss" 8 | ) 9 | 10 | // XMSS10 holds a pointer to a 132 byte array used by XMSS. It implements the 11 | // Signer interface. 12 | type XMSS10 struct { 13 | m sync.RWMutex 14 | PrivateKey [132]byte 15 | } 16 | 17 | // GenXMSS10 returns a randomly generated XMSS SHA2_10_256 private key in a NaClSign 18 | func GenXMSS10() *XMSS10 { 19 | k, _ := xmss.GenerateXMSSKeypair(xmss.SHA2_10_256) 20 | return NewXMSS10((*k)[:]) 21 | } 22 | 23 | // NewXMSS10 takes an XMSS private key as a byte array and returns a NaClSign 24 | func NewXMSS10(privateKey []byte) *XMSS10 { 25 | var pk [132]byte 26 | copy(pk[:], privateKey) 27 | return &XMSS10{ 28 | PrivateKey: pk, 29 | } 30 | } 31 | 32 | // Sign takes a message and returns a Bundle signed with a private key using XMSS SHA2_10_256. 33 | func (s *XMSS10) Sign(message []byte) (Bundle, error) { 34 | s.m.Lock() 35 | prv := xmss.PrivateXMSS(s.PrivateKey[:]) 36 | pub := make([]byte, 64) 37 | copy(pub[:32], prv[100:]) 38 | copy(pub[32:], prv[68:100]) 39 | 40 | prm := xmss.SHA2_10_256 41 | sig := *prv.Sign(prm, message) 42 | copy(s.PrivateKey[:], prv) 43 | s.m.Unlock() 44 | 45 | b := Bundle{ 46 | Alg: AlgXMSS10, 47 | Pub: pub, 48 | Sig: Bytes(sig[:prm.SignBytes()]), 49 | } 50 | 51 | if ok := VerifyXMSS10(message, b); !ok { 52 | return Bundle{}, errors.New("verification sanity check failed on sign") 53 | } 54 | 55 | return b, nil 56 | } 57 | 58 | // VerifyXMSS10 takes a message and a Bundle and bool indicating if the message 59 | // is verified ny the signature. 60 | func VerifyXMSS10(msg []byte, b Bundle) bool { 61 | prm := xmss.SHA2_10_256 62 | sig := append(b.Sig, msg...) 63 | m := make([]byte, prm.SignBytes()+len(msg)) 64 | return xmss.Verify(prm, m, sig, []byte(b.Pub)) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/sig/xmss_test.go: -------------------------------------------------------------------------------- 1 | package sig 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestXMSS10(t *testing.T) { 9 | t.Parallel() 10 | m := []byte("sign me, plz.") 11 | t.Run("normal", func(t *testing.T) { 12 | t.Parallel() 13 | s := GenXMSS10() 14 | pre := s.PrivateKey[3] 15 | if _, err := s.Sign(m); err != nil { 16 | t.Error(err) 17 | } 18 | post := s.PrivateKey[3] 19 | if pre >= post { 20 | t.Error("counter state invalid") 21 | } 22 | }) 23 | t.Run("malformed", func(t *testing.T) { 24 | t.Parallel() 25 | s := GenXMSS10() 26 | badKey := bytes.Repeat([]byte{5}, 100) 27 | copy(s.PrivateKey[15:], badKey) 28 | if _, err := s.Sign(m); err == nil { 29 | t.Error("malformed signature not caught") 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /test/testdata/hashmap_ed25519.keyset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/test/testdata/hashmap_ed25519.keyset -------------------------------------------------------------------------------- /test/testdata/valid_payload_expired.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "timestamp": "2020-05-11T06:33:54.662306-06:00", 4 | "ttl": 5000000000, 5 | "sig_bundles": [ 6 | { 7 | "alg": 1, 8 | "pub": "RMoWDk+Yei20xNFw+DfFGQbXVd5KTHBhUOz9WSHvCKk=", 9 | "sig": "zkUcaHI+Q/JvnyRJdfzV9kgmgkusr7IxyPNhx9LZUMv/6j9hE83ZhlAyGWbfr7lKaPk43JX8a7DN3Ea6A2Y+Dw==" 10 | } 11 | ], 12 | "data": "c2lnbiBtZSwgcGx6Lg==" 13 | } -------------------------------------------------------------------------------- /x/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/.DS_Store -------------------------------------------------------------------------------- /x/README.md: -------------------------------------------------------------------------------- 1 | # Expirimental 2 | 3 | This is an experimental projects directory. Used for official experiments on hashmap. -------------------------------------------------------------------------------- /x/flag-exp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | flag "github.com/spf13/pflag" 6 | ) 7 | 8 | func main() { 9 | var signingTypes []string 10 | flag.StringSliceVarP(&signingTypes, "signing-type","t", []string{}, "declares the signing type for keygen") 11 | flag.Parse() 12 | fmt.Println(signingTypes) 13 | } -------------------------------------------------------------------------------- /x/request_demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/nomasters/hashmap/pkg/payload" 11 | "github.com/nomasters/hashmap/pkg/sig" 12 | ) 13 | 14 | func main() { 15 | s := []sig.Signer{sig.GenNaclSign()} 16 | m := []byte("hello, world") 17 | p, err := payload.Generate(m, s) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | pb, err := payload.Marshal(p) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | baseURL := "http://localhost:3000" 26 | contentType := "application/protobuf" 27 | resp, err := http.Post(baseURL, contentType, bytes.NewReader(pb)) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | fmt.Println(resp) 32 | fmt.Println("successful post for: ", p.Endpoint()) 33 | 34 | resp2, err := http.Get(fmt.Sprintf("%v/%v", baseURL, p.Endpoint())) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | pbResp, err := ioutil.ReadAll(resp2.Body) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | pResp, err := payload.Unmarshal(pbResp) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | fmt.Println("get is successful for: ", pResp.Endpoint()) 47 | fmt.Println("payload response data: ", string(pResp.Data)) 48 | } 49 | -------------------------------------------------------------------------------- /x/server_tinker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nomasters/hashmap/internal/server" 5 | ) 6 | 7 | func main() { 8 | server.Run() 9 | 10 | } 11 | -------------------------------------------------------------------------------- /x/webapp-demo/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | public/bundle.* 4 | -------------------------------------------------------------------------------- /x/webapp-demo/Makefile: -------------------------------------------------------------------------------- 1 | # This make file steamlines the embeded webapp build process 2 | 3 | WASM_SOURCE=wasm/main.go 4 | WASM_OUT=assets/hashmap.wasm 5 | BUILD_DIR=./public 6 | 7 | all: build-wasm build-webapp statik 8 | 9 | build-wasm: 10 | GOOS=js GOARCH=wasm go build -o ${BUILD_DIR}/${WASM_OUT} ${WASM_SOURCE} 11 | 12 | build-webapp: build-wasm 13 | npm install 14 | npm run build 15 | 16 | statik: build-webapp 17 | statik -src=${BUILD_DIR} -------------------------------------------------------------------------------- /x/webapp-demo/README.md: -------------------------------------------------------------------------------- 1 | *Psst — looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)* 2 | 3 | --- 4 | 5 | # svelte app 6 | 7 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. 8 | 9 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): 10 | 11 | ```bash 12 | npx degit sveltejs/template svelte-app 13 | cd svelte-app 14 | ``` 15 | 16 | *Note that you will need to have [Node.js](https://nodejs.org) installed.* 17 | 18 | 19 | ## Get started 20 | 21 | Install the dependencies... 22 | 23 | ```bash 24 | cd svelte-app 25 | npm install 26 | ``` 27 | 28 | ...then start [Rollup](https://rollupjs.org): 29 | 30 | ```bash 31 | npm run dev 32 | ``` 33 | 34 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. 35 | 36 | 37 | ## Deploying to the web 38 | 39 | ### With [now](https://zeit.co/now) 40 | 41 | Install `now` if you haven't already: 42 | 43 | ```bash 44 | npm install -g now 45 | ``` 46 | 47 | Then, from within your project folder: 48 | 49 | ```bash 50 | cd public 51 | now 52 | ``` 53 | 54 | As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon. 55 | 56 | ### With [surge](https://surge.sh/) 57 | 58 | Install `surge` if you haven't already: 59 | 60 | ```bash 61 | npm install -g surge 62 | ``` 63 | 64 | Then, from within your project folder: 65 | 66 | ```bash 67 | npm run build 68 | surge public 69 | ``` 70 | -------------------------------------------------------------------------------- /x/webapp-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hashmap-payload-viewer", 3 | "version": "0.0.1", 4 | "devDependencies": { 5 | "normalize.css": "8.0.1", 6 | "rollup": "1.19.4", 7 | "rollup-plugin-commonjs": "10.0.2", 8 | "rollup-plugin-node-resolve": "5.2.0", 9 | "rollup-plugin-postcss": "2.0.3", 10 | "rollup-plugin-svelte": "5.1.0", 11 | "rollup-plugin-terser": "5.1.1", 12 | "milligram": "1.3.0", 13 | "svelte": "3.8.1", 14 | "svelte-routing": "1.3.0" 15 | }, 16 | "dependencies": {}, 17 | "scripts": { 18 | "build": "rollup -c" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /x/webapp-demo/public/assets/fonts/hack-regular-subset.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/assets/fonts/hack-regular-subset.woff2 -------------------------------------------------------------------------------- /x/webapp-demo/public/assets/fonts/roboto-v20-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/assets/fonts/roboto-v20-latin-300.woff2 -------------------------------------------------------------------------------- /x/webapp-demo/public/assets/fonts/roboto-v20-latin-300italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/assets/fonts/roboto-v20-latin-300italic.woff2 -------------------------------------------------------------------------------- /x/webapp-demo/public/assets/fonts/roboto-v20-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/assets/fonts/roboto-v20-latin-700.woff2 -------------------------------------------------------------------------------- /x/webapp-demo/public/assets/fonts/roboto-v20-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/assets/fonts/roboto-v20-latin-700italic.woff2 -------------------------------------------------------------------------------- /x/webapp-demo/public/assets/hashmap.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/assets/hashmap.wasm -------------------------------------------------------------------------------- /x/webapp-demo/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/favicon.png -------------------------------------------------------------------------------- /x/webapp-demo/public/hashmap.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/hashmap.wasm -------------------------------------------------------------------------------- /x/webapp-demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hashmap Payload Viewer 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /x/webapp-demo/public/roboto-v20-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/public/roboto-v20-latin-regular.woff2 -------------------------------------------------------------------------------- /x/webapp-demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from "rollup-plugin-svelte"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import commonjs from "rollup-plugin-commonjs"; 4 | import postcss from "rollup-plugin-postcss"; 5 | import { terser } from "rollup-plugin-terser"; 6 | 7 | export default { 8 | input: "src/main.js", 9 | output: { 10 | // sourcemap: true, 11 | format: "iife", 12 | name: "app", 13 | file: "public/assets/bundle.js" 14 | }, 15 | plugins: [ 16 | postcss({ 17 | extensions: [".css"] 18 | }), 19 | svelte({ dev: false }), 20 | resolve({ 21 | browser: true, 22 | dedupe: importee => 23 | importee === "svelte" || importee.startsWith("svelte/") 24 | }), 25 | commonjs(), 26 | terser() 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /x/webapp-demo/server/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomasters/hashmap/6a2676faf106ad3769b216d1c7649e1bc7873d9a/x/webapp-demo/server/main -------------------------------------------------------------------------------- /x/webapp-demo/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | _ "github.com/nomasters/hashmap/x/webapp-demo/statik" 9 | "github.com/rakyll/statik/fs" 10 | ) 11 | 12 | // TODO: 13 | // move assets to /assets 14 | // / or /:hash should resolve to /index.html /assets should resolve assets 15 | 16 | // Before buildling, run go generate. 17 | // Then, run the main program and visit http://localhost:8080/public/hello.txt 18 | func main() { 19 | statikFS, err := fs.New() 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | fmt.Println("hello, world") 25 | 26 | http.Handle("/", http.StripPrefix("/", http.FileServer(statikFS))) 27 | http.ListenAndServe(":8080", nil) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /x/webapp-demo/src/App.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 16 | 17 | 18 |

Hello {name}!

19 | hello, world 20 | 21 |
hello, world
22 | 23 | 24 | 28 |
29 | 30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /x/webapp-demo/src/WasmExec.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
34 | 37 |
38 | -------------------------------------------------------------------------------- /x/webapp-demo/src/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #212121; 3 | color: #f8f8f2; 4 | } 5 | 6 | code { 7 | font-family: Hack, monospace; 8 | } 9 | 10 | pre { 11 | font-family: Hack, monospace; 12 | } 13 | 14 | /* hack font */ 15 | @font-face { 16 | font-family: "Hack"; 17 | font-style: normal; 18 | font-weight: 400; 19 | src: url("assets/fonts/hack-regular-subset.woff2") format("woff2"); 20 | } 21 | 22 | /* roboto font */ 23 | @font-face { 24 | font-family: "Roboto"; 25 | font-style: normal; 26 | font-weight: 300; 27 | src: url("assets/fonts/roboto-v20-latin-300.woff2") format("woff2"); 28 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 29 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 30 | U+FEFF, U+FFFD; 31 | } 32 | @font-face { 33 | font-family: "Roboto"; 34 | font-style: italic; 35 | font-weight: 300; 36 | src: url("assets/fonts/roboto-v20-latin-300italic.woff2") format("woff2"); 37 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 38 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 39 | U+FEFF, U+FFFD; 40 | } 41 | @font-face { 42 | font-family: "Roboto"; 43 | font-style: normal; 44 | font-weight: 700; 45 | src: url("assets/fonts/roboto-v20-latin-700.woff2") format("woff2"); 46 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 47 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 48 | U+FEFF, U+FFFD; 49 | } 50 | @font-face { 51 | font-family: "Roboto"; 52 | font-style: italic; 53 | font-weight: 700; 54 | src: url("assets/fonts/roboto-v20-latin-700italic.woff2") format("woff2"); 55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 56 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 57 | U+FEFF, U+FFFD; 58 | } 59 | -------------------------------------------------------------------------------- /x/webapp-demo/src/main.js: -------------------------------------------------------------------------------- 1 | import "./wasm_exec.js"; 2 | import "../node_modules/normalize.css/normalize.css"; 3 | import "../node_modules/milligram/dist/milligram.min.css"; 4 | import "./custom.css"; 5 | import App from "./App.svelte"; 6 | 7 | const app = new App({ 8 | target: document.body, 9 | props: { 10 | name: "nathan" 11 | } 12 | }); 13 | 14 | export default app; 15 | -------------------------------------------------------------------------------- /x/webapp-demo/src/routes/About.svelte: -------------------------------------------------------------------------------- 1 |

ABOUT

2 | -------------------------------------------------------------------------------- /x/webapp-demo/src/routes/Blog.svelte: -------------------------------------------------------------------------------- 1 |

BLOG

2 | -------------------------------------------------------------------------------- /x/webapp-demo/src/routes/Get.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

the hash is: {hash}

6 | -------------------------------------------------------------------------------- /x/webapp-demo/src/routes/Home.svelte: -------------------------------------------------------------------------------- 1 |

HOME

2 | -------------------------------------------------------------------------------- /x/webapp-demo/src/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | if (typeof global !== "undefined") { 7 | // global already exists 8 | } else if (typeof window !== "undefined") { 9 | window.global = window; 10 | } else if (typeof self !== "undefined") { 11 | self.global = self; 12 | } else { 13 | throw new Error( 14 | "cannot export Go (neither global, window nor self is defined)" 15 | ); 16 | } 17 | let outputBuf = ""; 18 | global.fs = { 19 | constants: { 20 | O_WRONLY: -1, 21 | O_RDWR: -1, 22 | O_CREAT: -1, 23 | O_TRUNC: -1, 24 | O_APPEND: -1, 25 | O_EXCL: -1 26 | }, // unused 27 | writeSync(fd, buf) { 28 | outputBuf += decoder.decode(buf); 29 | const nl = outputBuf.lastIndexOf("\n"); 30 | if (nl != -1) { 31 | console.log(outputBuf.substr(0, nl)); 32 | outputBuf = outputBuf.substr(nl + 1); 33 | } 34 | return buf.length; 35 | }, 36 | write(fd, buf, offset, length, position, callback) { 37 | if (offset !== 0 || length !== buf.length || position !== null) { 38 | throw new Error("not implemented"); 39 | } 40 | const n = this.writeSync(fd, buf); 41 | callback(null, n); 42 | }, 43 | open(path, flags, mode, callback) { 44 | const err = new Error("not implemented"); 45 | err.code = "ENOSYS"; 46 | callback(err); 47 | }, 48 | read(fd, buffer, offset, length, position, callback) { 49 | const err = new Error("not implemented"); 50 | err.code = "ENOSYS"; 51 | callback(err); 52 | }, 53 | fsync(fd, callback) { 54 | callback(null); 55 | } 56 | }; 57 | 58 | const encoder = new TextEncoder("utf-8"); 59 | const decoder = new TextDecoder("utf-8"); 60 | 61 | global.Go = class { 62 | constructor() { 63 | this.argv = ["js"]; 64 | this.env = {}; 65 | this.exit = code => { 66 | if (code !== 0) { 67 | console.warn("exit code:", code); 68 | } 69 | }; 70 | this._exitPromise = new Promise(resolve => { 71 | this._resolveExitPromise = resolve; 72 | }); 73 | this._pendingEvent = null; 74 | this._scheduledTimeouts = new Map(); 75 | this._nextCallbackTimeoutID = 1; 76 | 77 | const mem = () => { 78 | // The buffer may change when requesting more memory. 79 | return new DataView(this._inst.exports.mem.buffer); 80 | }; 81 | 82 | const setInt64 = (addr, v) => { 83 | mem().setUint32(addr + 0, v, true); 84 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); 85 | }; 86 | 87 | const getInt64 = addr => { 88 | const low = mem().getUint32(addr + 0, true); 89 | const high = mem().getInt32(addr + 4, true); 90 | return low + high * 4294967296; 91 | }; 92 | 93 | const loadValue = addr => { 94 | const f = mem().getFloat64(addr, true); 95 | if (f === 0) { 96 | return undefined; 97 | } 98 | if (!isNaN(f)) { 99 | return f; 100 | } 101 | 102 | const id = mem().getUint32(addr, true); 103 | return this._values[id]; 104 | }; 105 | 106 | const storeValue = (addr, v) => { 107 | const nanHead = 0x7ff80000; 108 | 109 | if (typeof v === "number") { 110 | if (isNaN(v)) { 111 | mem().setUint32(addr + 4, nanHead, true); 112 | mem().setUint32(addr, 0, true); 113 | return; 114 | } 115 | if (v === 0) { 116 | mem().setUint32(addr + 4, nanHead, true); 117 | mem().setUint32(addr, 1, true); 118 | return; 119 | } 120 | mem().setFloat64(addr, v, true); 121 | return; 122 | } 123 | 124 | switch (v) { 125 | case undefined: 126 | mem().setFloat64(addr, 0, true); 127 | return; 128 | case null: 129 | mem().setUint32(addr + 4, nanHead, true); 130 | mem().setUint32(addr, 2, true); 131 | return; 132 | case true: 133 | mem().setUint32(addr + 4, nanHead, true); 134 | mem().setUint32(addr, 3, true); 135 | return; 136 | case false: 137 | mem().setUint32(addr + 4, nanHead, true); 138 | mem().setUint32(addr, 4, true); 139 | return; 140 | } 141 | 142 | let ref = this._refs.get(v); 143 | if (ref === undefined) { 144 | ref = this._values.length; 145 | this._values.push(v); 146 | this._refs.set(v, ref); 147 | } 148 | let typeFlag = 0; 149 | switch (typeof v) { 150 | case "string": 151 | typeFlag = 1; 152 | break; 153 | case "symbol": 154 | typeFlag = 2; 155 | break; 156 | case "function": 157 | typeFlag = 3; 158 | break; 159 | } 160 | mem().setUint32(addr + 4, nanHead | typeFlag, true); 161 | mem().setUint32(addr, ref, true); 162 | }; 163 | 164 | const loadSlice = addr => { 165 | const array = getInt64(addr + 0); 166 | const len = getInt64(addr + 8); 167 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 168 | }; 169 | 170 | const loadSliceOfValues = addr => { 171 | const array = getInt64(addr + 0); 172 | const len = getInt64(addr + 8); 173 | const a = new Array(len); 174 | for (let i = 0; i < len; i++) { 175 | a[i] = loadValue(array + i * 8); 176 | } 177 | return a; 178 | }; 179 | 180 | const loadString = addr => { 181 | const saddr = getInt64(addr + 0); 182 | const len = getInt64(addr + 8); 183 | return decoder.decode( 184 | new DataView(this._inst.exports.mem.buffer, saddr, len) 185 | ); 186 | }; 187 | 188 | const timeOrigin = Date.now() - performance.now(); 189 | this.importObject = { 190 | go: { 191 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 192 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 193 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 194 | // This changes the SP, thus we have to update the SP used by the imported function. 195 | 196 | // func wasmExit(code int32) 197 | "runtime.wasmExit": sp => { 198 | const code = mem().getInt32(sp + 8, true); 199 | this.exited = true; 200 | delete this._inst; 201 | delete this._values; 202 | delete this._refs; 203 | this.exit(code); 204 | }, 205 | 206 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 207 | "runtime.wasmWrite": sp => { 208 | const fd = getInt64(sp + 8); 209 | const p = getInt64(sp + 16); 210 | const n = mem().getInt32(sp + 24, true); 211 | fs.writeSync( 212 | fd, 213 | new Uint8Array(this._inst.exports.mem.buffer, p, n) 214 | ); 215 | }, 216 | 217 | // func nanotime() int64 218 | "runtime.nanotime": sp => { 219 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 220 | }, 221 | 222 | // func walltime() (sec int64, nsec int32) 223 | "runtime.walltime": sp => { 224 | const msec = new Date().getTime(); 225 | setInt64(sp + 8, msec / 1000); 226 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); 227 | }, 228 | 229 | // func scheduleTimeoutEvent(delay int64) int32 230 | "runtime.scheduleTimeoutEvent": sp => { 231 | const id = this._nextCallbackTimeoutID; 232 | this._nextCallbackTimeoutID++; 233 | this._scheduledTimeouts.set( 234 | id, 235 | setTimeout( 236 | () => { 237 | this._resume(); 238 | }, 239 | getInt64(sp + 8) + 1 // setTimeout has been seen to fire up to 1 millisecond early 240 | ) 241 | ); 242 | mem().setInt32(sp + 16, id, true); 243 | }, 244 | 245 | // func clearTimeoutEvent(id int32) 246 | "runtime.clearTimeoutEvent": sp => { 247 | const id = mem().getInt32(sp + 8, true); 248 | clearTimeout(this._scheduledTimeouts.get(id)); 249 | this._scheduledTimeouts.delete(id); 250 | }, 251 | 252 | // func getRandomData(r []byte) 253 | "runtime.getRandomData": sp => { 254 | crypto.getRandomValues(loadSlice(sp + 8)); 255 | }, 256 | 257 | // func stringVal(value string) ref 258 | "syscall/js.stringVal": sp => { 259 | storeValue(sp + 24, loadString(sp + 8)); 260 | }, 261 | 262 | // func valueGet(v ref, p string) ref 263 | "syscall/js.valueGet": sp => { 264 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 265 | sp = this._inst.exports.getsp(); // see comment above 266 | storeValue(sp + 32, result); 267 | }, 268 | 269 | // func valueSet(v ref, p string, x ref) 270 | "syscall/js.valueSet": sp => { 271 | Reflect.set( 272 | loadValue(sp + 8), 273 | loadString(sp + 16), 274 | loadValue(sp + 32) 275 | ); 276 | }, 277 | 278 | // func valueIndex(v ref, i int) ref 279 | "syscall/js.valueIndex": sp => { 280 | storeValue( 281 | sp + 24, 282 | Reflect.get(loadValue(sp + 8), getInt64(sp + 16)) 283 | ); 284 | }, 285 | 286 | // valueSetIndex(v ref, i int, x ref) 287 | "syscall/js.valueSetIndex": sp => { 288 | Reflect.set( 289 | loadValue(sp + 8), 290 | getInt64(sp + 16), 291 | loadValue(sp + 24) 292 | ); 293 | }, 294 | 295 | // func valueCall(v ref, m string, args []ref) (ref, bool) 296 | "syscall/js.valueCall": sp => { 297 | try { 298 | const v = loadValue(sp + 8); 299 | const m = Reflect.get(v, loadString(sp + 16)); 300 | const args = loadSliceOfValues(sp + 32); 301 | const result = Reflect.apply(m, v, args); 302 | sp = this._inst.exports.getsp(); // see comment above 303 | storeValue(sp + 56, result); 304 | mem().setUint8(sp + 64, 1); 305 | } catch (err) { 306 | storeValue(sp + 56, err); 307 | mem().setUint8(sp + 64, 0); 308 | } 309 | }, 310 | 311 | // func valueInvoke(v ref, args []ref) (ref, bool) 312 | "syscall/js.valueInvoke": sp => { 313 | try { 314 | const v = loadValue(sp + 8); 315 | const args = loadSliceOfValues(sp + 16); 316 | const result = Reflect.apply(v, undefined, args); 317 | sp = this._inst.exports.getsp(); // see comment above 318 | storeValue(sp + 40, result); 319 | mem().setUint8(sp + 48, 1); 320 | } catch (err) { 321 | storeValue(sp + 40, err); 322 | mem().setUint8(sp + 48, 0); 323 | } 324 | }, 325 | 326 | // func valueNew(v ref, args []ref) (ref, bool) 327 | "syscall/js.valueNew": sp => { 328 | try { 329 | const v = loadValue(sp + 8); 330 | const args = loadSliceOfValues(sp + 16); 331 | const result = Reflect.construct(v, args); 332 | sp = this._inst.exports.getsp(); // see comment above 333 | storeValue(sp + 40, result); 334 | mem().setUint8(sp + 48, 1); 335 | } catch (err) { 336 | storeValue(sp + 40, err); 337 | mem().setUint8(sp + 48, 0); 338 | } 339 | }, 340 | 341 | // func valueLength(v ref) int 342 | "syscall/js.valueLength": sp => { 343 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 344 | }, 345 | 346 | // valuePrepareString(v ref) (ref, int) 347 | "syscall/js.valuePrepareString": sp => { 348 | const str = encoder.encode(String(loadValue(sp + 8))); 349 | storeValue(sp + 16, str); 350 | setInt64(sp + 24, str.length); 351 | }, 352 | 353 | // valueLoadString(v ref, b []byte) 354 | "syscall/js.valueLoadString": sp => { 355 | const str = loadValue(sp + 8); 356 | loadSlice(sp + 16).set(str); 357 | }, 358 | 359 | // func valueInstanceOf(v ref, t ref) bool 360 | "syscall/js.valueInstanceOf": sp => { 361 | mem().setUint8( 362 | sp + 24, 363 | loadValue(sp + 8) instanceof loadValue(sp + 16) 364 | ); 365 | }, 366 | 367 | debug: value => { 368 | console.log(value); 369 | } 370 | } 371 | }; 372 | } 373 | 374 | async run(instance) { 375 | this._inst = instance; 376 | this._values = [ 377 | // TODO: garbage collection 378 | NaN, 379 | 0, 380 | null, 381 | true, 382 | false, 383 | global, 384 | this._inst.exports.mem, 385 | this 386 | ]; 387 | this._refs = new Map(); 388 | this.exited = false; 389 | 390 | const mem = new DataView(this._inst.exports.mem.buffer); 391 | 392 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 393 | let offset = 4096; 394 | 395 | const strPtr = str => { 396 | let ptr = offset; 397 | new Uint8Array(mem.buffer, offset, str.length + 1).set( 398 | encoder.encode(str + "\0") 399 | ); 400 | offset += str.length + (8 - (str.length % 8)); 401 | return ptr; 402 | }; 403 | 404 | const argc = this.argv.length; 405 | 406 | const argvPtrs = []; 407 | this.argv.forEach(arg => { 408 | argvPtrs.push(strPtr(arg)); 409 | }); 410 | 411 | const keys = Object.keys(this.env).sort(); 412 | argvPtrs.push(keys.length); 413 | keys.forEach(key => { 414 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 415 | }); 416 | 417 | const argv = offset; 418 | argvPtrs.forEach(ptr => { 419 | mem.setUint32(offset, ptr, true); 420 | mem.setUint32(offset + 4, 0, true); 421 | offset += 8; 422 | }); 423 | 424 | this._inst.exports.run(argc, argv); 425 | if (this.exited) { 426 | this._resolveExitPromise(); 427 | } 428 | await this._exitPromise; 429 | } 430 | 431 | _resume() { 432 | if (this.exited) { 433 | throw new Error("Go program has already exited"); 434 | } 435 | this._inst.exports.resume(); 436 | if (this.exited) { 437 | this._resolveExitPromise(); 438 | } 439 | } 440 | 441 | _makeFuncWrapper(id) { 442 | const go = this; 443 | return function() { 444 | const event = { id: id, this: this, args: arguments }; 445 | go._pendingEvent = event; 446 | go._resume(); 447 | return event.result; 448 | }; 449 | } 450 | }; 451 | })(); 452 | -------------------------------------------------------------------------------- /x/webapp-demo/wasm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("hello wasm") 9 | } 10 | -------------------------------------------------------------------------------- /x/xmss-test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "log" 7 | 8 | xmss "github.com/danielhavir/go-xmss" 9 | ) 10 | 11 | func main() { 12 | prm := xmss.SHA2_10_256 13 | prv, pub := xmss.GenerateXMSSKeypair(prm) 14 | fmt.Printf("%x\n", *prv) 15 | fmt.Printf("%x\n", *pub) 16 | fmt.Println(len(*prv)) 17 | fmt.Println(len(*pub)) 18 | fmt.Println("") 19 | 20 | fmt.Printf("%x%x\n", (*prv)[100:], (*prv)[68:100]) 21 | var p [64]byte 22 | copy(p[:32], (*prv)[100:]) 23 | copy(p[32:], (*prv)[68:100]) 24 | fmt.Printf("%x\n", p) 25 | // msg := []byte("hello, world") 26 | 27 | // for i := 1022; i < 1023; i++ { 28 | // log.Println("verifying counter:", i) 29 | // bumpCounter(uint64(i), prv) 30 | // signAndVerify(prm, prv, pub, msg) 31 | // } 32 | } 33 | 34 | func signAndVerify(prm *xmss.Params, prv *xmss.PrivateXMSS, pub *xmss.PublicXMSS, msg []byte) { 35 | sig := *prv.Sign(prm, msg) 36 | m := make([]byte, prm.SignBytes()+len(msg)) 37 | 38 | fmt.Println(sig) 39 | if !xmss.Verify(prm, m, sig, *pub) { 40 | log.Fatal("failed to verify") 41 | } 42 | log.Println(string(m)[prm.SignBytes():]) 43 | } 44 | 45 | func bumpCounter(i uint64, prv *xmss.PrivateXMSS) { 46 | pb := []byte(*prv) 47 | copy(pb[:4], uint64ToBytes(i)[4:]) 48 | copy(*prv, pb) 49 | } 50 | 51 | func uint64ToBytes(t uint64) []byte { 52 | b := make([]byte, 8) 53 | binary.BigEndian.PutUint64(b, t) 54 | return b 55 | } 56 | --------------------------------------------------------------------------------