git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/tools/zign/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "encoding/hex" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "os" 10 "strings" 11 12 "git.sr.ht/~pingoo/stdx/cobra" 13 "git.sr.ht/~pingoo/stdx/crypto" 14 "git.sr.ht/~pingoo/stdx/filex" 15 "git.sr.ht/~pingoo/stdx/zign" 16 "golang.org/x/term" 17 ) 18 19 const ( 20 defaultPrivateKeyFile = "zign.private" 21 defaultPulicKeyFile = "zign.public" 22 ) 23 24 var ( 25 signManifestOutpout string 26 signCmdPasswordStdin bool 27 28 version = fmt.Sprintf("%d.0.0", zign.Version1) 29 ) 30 31 func init() { 32 rootCmd.AddCommand(initCmd) 33 34 signCmd.Flags().StringVarP(&signManifestOutpout, "output", "o", zign.DefaultManifestFilename, "Output file for signature (default: zign.json)") 35 signCmd.Flags().BoolVar(&signCmdPasswordStdin, "password-stdin", false, "Read password from stdin") 36 rootCmd.AddCommand(signCmd) 37 38 rootCmd.AddCommand(verifyCmd) 39 } 40 41 func main() { 42 err := rootCmd.Execute() 43 if err != nil { 44 fmt.Fprintln(os.Stdout, err.Error()) 45 os.Exit(1) 46 } 47 } 48 49 var rootCmd = &cobra.Command{ 50 Use: "zign", 51 Short: "Sign and verify files", 52 Version: version, 53 SilenceUsage: true, 54 SilenceErrors: true, 55 RunE: func(cmd *cobra.Command, args []string) (err error) { 56 return cmd.Help() 57 }, 58 } 59 60 var initCmd = &cobra.Command{ 61 Use: "init", 62 Short: "Initialize a keypair to sign files (zign.private, zign.public)", 63 RunE: func(cmd *cobra.Command, args []string) (err error) { 64 fmt.Print("password: ") 65 password, err := term.ReadPassword(int(os.Stdin.Fd())) 66 if err != nil { 67 err = fmt.Errorf("zign: error reading password: %w", err) 68 return 69 } 70 defer crypto.Zeroize(password) 71 72 fmt.Print("\nverify password: ") 73 passwordVerification, err := term.ReadPassword(int(os.Stdin.Fd())) 74 if err != nil { 75 err = fmt.Errorf("zign: error reading password: %w", err) 76 return 77 } 78 defer crypto.Zeroize(passwordVerification) 79 80 if crypto.ConstantTimeCompare(password, passwordVerification) == false { 81 err = errors.New("zign: passwords don't match") 82 return 83 } 84 85 fmt.Println("") 86 87 privateKey, publicKey, err := zign.Init(password) 88 if err != nil { 89 return 90 } 91 92 privateKey += "\n" 93 94 err = os.WriteFile(defaultPrivateKeyFile, []byte(privateKey), 0600) 95 if err != nil { 96 err = fmt.Errorf("zign: writing private key file: %w", err) 97 return 98 } 99 100 publicKey += "\n" 101 102 err = os.WriteFile(defaultPulicKeyFile, []byte(publicKey), 0600) 103 if err != nil { 104 err = fmt.Errorf("zign: writing public key file: %w", err) 105 return 106 } 107 108 return 109 }, 110 } 111 112 var signCmd = &cobra.Command{ 113 Use: "sign [-o project_version_zign.json] zign.private file1 file2 file3...", 114 Short: "Sign files using the given private key", 115 Args: cobra.MinimumNArgs(2), 116 RunE: func(cmd *cobra.Command, args []string) (err error) { 117 var password []byte 118 119 if len(args) < 2 { 120 return cmd.Help() 121 } 122 123 if signCmdPasswordStdin { 124 stdinReader := bufio.NewReader(os.Stdin) 125 password, _, err = stdinReader.ReadLine() 126 if err != nil { 127 err = fmt.Errorf("zign: error reading password from stdin: %w", err) 128 return 129 } 130 } else { 131 fmt.Print("password: ") 132 password, err = term.ReadPassword(int(os.Stdin.Fd())) 133 if err != nil { 134 err = fmt.Errorf("zign: error reading password: %w", err) 135 return 136 } 137 138 fmt.Println("") 139 } 140 defer crypto.Zeroize(password) 141 142 encodedPrivateKeyBytes, err := os.ReadFile(defaultPrivateKeyFile) 143 if err != nil { 144 err = fmt.Errorf("zign: reading private key file: %w", err) 145 return 146 } 147 148 encodedPrivateKey := strings.TrimSpace(string(encodedPrivateKeyBytes)) 149 150 files := args[1:] 151 signInput := make([]zign.SignInput, len(files)) 152 153 for index, file := range files { 154 fileExists := false 155 var fileHandle *os.File 156 157 fileExists, err = filex.Exists(file) 158 if err != nil { 159 err = fmt.Errorf("checking if file exists (%s): %w", file, err) 160 return 161 } 162 163 if !fileExists { 164 err = fmt.Errorf("file does not exist: %s", file) 165 return 166 } 167 168 // we don't need to close the files as the program is short lived... 169 fileHandle, err = os.Open(file) 170 if err != nil { 171 err = fmt.Errorf("opening file (%s): %w", file, err) 172 return 173 } 174 fileSignInput := zign.SignInput{ 175 Reader: fileHandle, 176 } 177 signInput[index] = fileSignInput 178 } 179 180 signOutput, err := zign.SignMany(encodedPrivateKey, string(password), signInput) 181 if err != nil { 182 return 183 } 184 185 manifest := zign.GenerateManifest(signOutput) 186 manifestJson, err := manifest.ToJson() 187 if err != nil { 188 return 189 } 190 191 err = os.WriteFile(signManifestOutpout, manifestJson, 0644) 192 if err != nil { 193 err = fmt.Errorf("zign: writing manifest (%s): %w", signManifestOutpout, err) 194 return 195 } 196 197 return 198 }, 199 } 200 201 var verifyCmd = &cobra.Command{ 202 Use: "verify [base64_encoded_public_key] project_version_zign.json", 203 Short: "Verify files using the given public key", 204 Args: cobra.ExactArgs(2), 205 RunE: func(cmd *cobra.Command, args []string) (err error) { 206 var manifest zign.Manifest 207 if len(args) != 2 { 208 return cmd.Help() 209 } 210 211 manifestJson, err := os.ReadFile(args[1]) 212 if err != nil { 213 return 214 } 215 216 err = json.Unmarshal(manifestJson, &manifest) 217 if err != nil { 218 err = fmt.Errorf("parsing manifest: %w", err) 219 return 220 } 221 222 if manifest.Version != zign.Version1 { 223 err = fmt.Errorf("zign: manifest version superior to %s are not supported", zign.Version1) 224 return 225 } 226 227 verifyInput := make([]zign.VerifyInput, 0, len(manifest.Files)) 228 verifiedFiles := make([]string, 0, len(manifest.Files)) 229 230 for _, file := range manifest.Files { 231 fileExists := false 232 var fileHandle *os.File 233 var hash []byte 234 235 fileExists, err = filex.Exists(file.Filename) 236 if err != nil { 237 err = fmt.Errorf("checking if file exists (%s): %w", file.Filename, err) 238 return 239 } 240 241 if !fileExists { 242 continue 243 } 244 245 // we don't need to close the files as the program is short lived... 246 fileHandle, err = os.Open(file.Filename) 247 if err != nil { 248 err = fmt.Errorf("opening file (%s): %w", file.Filename, err) 249 return 250 } 251 252 hash, err = hex.DecodeString(file.HashBlake3) 253 if err != nil { 254 err = fmt.Errorf("decoding blake3 hash for file %s: %w", file.Filename, err) 255 return 256 } 257 258 fileVerifyInput := zign.VerifyInput{ 259 Reader: fileHandle, 260 HashBlake3: hash, 261 Signature: file.Signature, 262 } 263 verifyInput = append(verifyInput, fileVerifyInput) 264 verifiedFiles = append(verifiedFiles, file.Filename) 265 } 266 267 base64EncodedPublicKey := strings.TrimSpace(args[0]) 268 err = zign.VerifyMany(base64EncodedPublicKey, verifyInput) 269 if err != nil { 270 return 271 } 272 273 if len(verifiedFiles) == 0 { 274 fmt.Println("No file to verify") 275 return 276 } 277 278 for _, file := range verifiedFiles { 279 fmt.Println("✓", file) 280 } 281 282 return 283 }, 284 }