diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..bd0668c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +--- +github: +- canterberry diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..29cc9a0 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +golang 1.17.6 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8c515f --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2022 Twuni + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3caee53 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# Pinki + +Pinki helps developers ship software with authenticity. + +Use it anywhere you would use `gpg` to sign and verify things. + +## Features + + * Easy to use + * Portable, standalone binary + * Anonymous -- a key is just a key, nothing more + * Doesn't touch your filesystem + * Reads and writes standard PEM-wrapped ASN.1 (compatible with X.509, GPG) + +## Installing + +### Precompiled Binaries + +Visit [Releases](https://releases.twuni.dev/pinki/latest/) to download a precompiled binary for your system. + +#### Verifying Binaries + +All releases are signed using the following [release signing key](https://releases.twuni.dev/verify.pem): + +```pem +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEdDOWMNxI5f88Yck8WNcPsxDOwMbzoU/Y +cZhfoR+gwGi0wRoSscWA1xy1BQTG6PNrQlvLJbfm2vAIAImnyMmhoKS3hwcO6F+5 +4QjLZQJAQHZ6G7c842gYRSnwLLQ2GIvj +-----END PUBLIC KEY----- +``` + +Each release binary has a corresponding file with an **.asc** prefix containing the signature for that file. + +Here's an example of how you could use Pinki to verify itself. + +> :bulb: In practice, you probably want to use another tool to verify Pinki itself the first time you download +> it. Once you have a genuine copy of `pinki`, then you can use it to verify updates to itself. + +```sh +# Download Pinki for Linux (64-bit) +$ curl -sSL -o pinki https://releases.twuni.dev/pinki/latest/linux-amd64/pinki + +# Use Pinki to verify itself +$ ./pinki verify "$(curl -sSL https://releases.twuni.dev/verify.pem)" "$(curl -sSL https://releases.twuni.dev/pinki/latest/linux-amd64/pinki.asc)" < pinki +``` + +If you get an output of `OK`, the signature is valid. + +### Building from source + +Already have `go`? Clone this repo and run `go build`. + +## Usage + +Pinki is designed to make it easy for you to do one of two things: + + * **Sign** your software so other people can verify its authenticity, or + + * **Verify** the authenticity of software you are using when the developers + are using Pinki. + +### Signing your software with Pinki + +First, you'll need a private key. To create a new key with the +recommended (default) options: + +```sh +$ pinki key create +-----BEGIN PRIVATE KEY----- +............................................................... +............................................................... +............................................................... +-----END PRIVATE KEY----- +``` + +Save the output somewhere safe. Put it in your password manager, +vault, or whatever you are using to keep sensitive information +safe. + +Once you have a private key, you will need to *export* that in a +way that is safe for people to verify your signatures: + +```sh +$ pinki key export < /path/to/your-pinki-private-key +-----BEGIN PUBLIC KEY----- +............................................................... +............................................................... +-----END PUBLIC KEY----- +``` + +Publish this public key somewhere that anyone you want to be able +to verify your signatures is able to access it. You can commit it +to your source code repo, publish it to your website, etc. + +> :bulb: The **public key** is not sensitive! You can safely share +> it with anyone. + +Now that you have a private key, you're ready to sign your first thing! + +```sh +$ pinki sign "$(cat /path/to/your-pinki-private-key)" < /path/to/your-thing-1.2.3.tar.gz +-----BEGIN SIGNATURE----- +............................................................... +............................................................... +-----END SIGNATURE----- +``` + +Publish that signature any way you like. Conventionally, you might want to +publish it as a file with the same name as the thing you've signed, but with +a **.sig** suffix. So **foo-1.0.tgz** would have its signature in +**foo-1.0.tgz.sig**. The choice is up to you. + +### Verifying a signature with Pinki + +To verify a signature, you'll need three things: + + * The thing that was signed (e.g: **foo-1.2.3.tgz**) + * The signature (e.g: **foo-1.2.3.tgz.asc**) + * The public key of the signer (e.g: **foomaker-signing-key.pem**) + +Check the release notes or installation/verification documentation of the +thing you're trying to verify for more details on where to find these things. + +Once you have them, here's how you verify the thing is authentic! + +```sh +$ pinki verify "$(cat /path/to/signing-key)" "$(cat /path/to/signature)" < /path/to/thing-that-was-signed +OK +``` + +The command will exit with status code 0 and print "OK" on success. +Otherwise, it will exit with status code 1 and print an error message. diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..1d154fd --- /dev/null +++ b/cli.go @@ -0,0 +1,43 @@ +package main + +import ( + "io" +) + +func cli(args []string, in io.Reader, out io.Writer) error { + if len(args) < 1 { + return help(out) + } + + switch args[0] { + case "help": + return help(out) + case "key": + if len(args) < 2 { + return help(out) + } + + switch args[1] { + case "create": + return createPrivateKey(out) + case "export": + return exportPublicKey(in, out) + default: + return help(out) + } + case "sign": + if len(args) < 2 { + return help(out) + } + + return sign(args[1], in, out) + case "verify": + if len(args) < 3 { + return help(out) + } + + return verify(args[1], args[2], in, out) + } + + return help(out) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..07793cd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/twuni/pinki + +go 1.17 diff --git a/help.go b/help.go new file mode 100644 index 0000000..c79b1fe --- /dev/null +++ b/help.go @@ -0,0 +1,28 @@ +package main + +import ( + "errors" + "io" +) + +func help(out io.Writer) error { + return errors.New(`USAGE: pinki + +EXAMPLES + + $ pinki help + Display this help message. + + $ pinki key create > private.pem + Generate a new private key and write the result to "private.pem". + + $ pinki key export < private.pem > public.pem + Extract the public key from "private.pem" and write the result to "public.pem". + + $ pinki sign "$(cat private.pem)" < package-1.0.0.tgz > package-1.0.0.tgz.asc + Sign "package-1.0.0.tgz" using the private key from "private.pem" and write the result to "package-1.0.0.tgz.asc". + + $ pinki verify "$(cat public.pem)" "$(cat package-1.0.0.tgz.asc)" < package-1.0.0.tgz + Verify the signature in "package-1.0.0.tgz.asc" of "package-1.0.0.tgz" using the public key from "public.pem". +`) +} diff --git a/key_create.go b/key_create.go new file mode 100644 index 0000000..12d1e7b --- /dev/null +++ b/key_create.go @@ -0,0 +1,31 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "io" +) + +func createPrivateKey(out io.Writer) error { + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + + if err != nil { + return err + } + + der, err := x509.MarshalPKCS8PrivateKey(key) + + if err != nil { + return err + } + + block := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: der, + } + + return pem.Encode(out, block) +} diff --git a/key_export.go b/key_export.go new file mode 100644 index 0000000..65b5cd4 --- /dev/null +++ b/key_export.go @@ -0,0 +1,28 @@ +package main + +import ( + "crypto/x509" + "encoding/pem" + "io" +) + +func exportPublicKey(in io.Reader, out io.Writer) error { + key, err := readPrivateKey(in) + + if err != nil { + return err + } + + der, err := x509.MarshalPKIXPublicKey(key.Public()) + + if err != nil { + return err + } + + block := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: der, + } + + return pem.Encode(out, block) +} diff --git a/key_help.go b/key_help.go new file mode 100644 index 0000000..4defd11 --- /dev/null +++ b/key_help.go @@ -0,0 +1,10 @@ +package main + +import ( + "io" +) + +func keyHelp(out io.Writer) error { + out.Write([]byte("USAGE: pinki key create|export|help\n")) + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..306b26c --- /dev/null +++ b/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + err := cli(os.Args[1:], os.Stdin, os.Stdout) + + if err != nil { + fmt.Printf("%s\n", err) + os.Exit(1) + } + + os.Exit(0) +} diff --git a/read_private_key.go b/read_private_key.go new file mode 100644 index 0000000..0881030 --- /dev/null +++ b/read_private_key.go @@ -0,0 +1,35 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" + "io" +) + +const ( + PrivateKey = "PRIVATE KEY" +) + +func readPrivateKey(in io.Reader) (*ecdsa.PrivateKey, error) { + buffer, err := io.ReadAll(in) + + if err != nil { + return nil, err + } + + block, _ := pem.Decode(buffer) + + if block == nil || block.Type != PrivateKey { + return nil, errors.New("Expected a PEM-encoded private key") + } + + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + + if err != nil { + return nil, err + } + + return key.(*ecdsa.PrivateKey), nil +} diff --git a/read_public_key.go b/read_public_key.go new file mode 100644 index 0000000..b29fd23 --- /dev/null +++ b/read_public_key.go @@ -0,0 +1,35 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "errors" + "io" +) + +const ( + PublicKey = "PUBLIC KEY" +) + +func readPublicKey(in io.Reader) (*ecdsa.PublicKey, error) { + buffer, err := io.ReadAll(in) + + if err != nil { + return nil, err + } + + block, _ := pem.Decode(buffer) + + if block == nil || block.Type != PublicKey { + return nil, errors.New("Expected a PEM-encoded public key") + } + + key, err := x509.ParsePKIXPublicKey(block.Bytes) + + if err != nil { + return nil, err + } + + return key.(*ecdsa.PublicKey), nil +} diff --git a/read_signature.go b/read_signature.go new file mode 100644 index 0000000..43efa09 --- /dev/null +++ b/read_signature.go @@ -0,0 +1,27 @@ +package main + +import ( + "encoding/pem" + "errors" + "io" +) + +const ( + Signature = "SIGNATURE" +) + +func readSignature(in io.Reader) ([]byte, error) { + buffer, err := io.ReadAll(in) + + if err != nil { + return nil, err + } + + block, _ := pem.Decode(buffer) + + if block == nil || block.Type != Signature { + return nil, errors.New("Expected a PEM-encoded signature") + } + + return block.Bytes, nil +} diff --git a/sign.go b/sign.go new file mode 100644 index 0000000..8a49da8 --- /dev/null +++ b/sign.go @@ -0,0 +1,39 @@ +package main + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "encoding/pem" + "io" +) + +func sign(privateKeyAsString string, in io.Reader, out io.Writer) error { + privateKey, err := readPrivateKey(bytes.NewBufferString(privateKeyAsString)) + + if err != nil { + return err + } + + digest := sha256.New() + + _, err = io.Copy(digest, in) + + if err != nil { + return err + } + + signature, err := ecdsa.SignASN1(rand.Reader, privateKey, digest.Sum(nil)) + + if err != nil { + return err + } + + block := &pem.Block{ + Type: "SIGNATURE", + Bytes: signature, + } + + return pem.Encode(out, block) +} diff --git a/verify.go b/verify.go new file mode 100644 index 0000000..7bac692 --- /dev/null +++ b/verify.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "crypto/ecdsa" + "crypto/sha256" + "errors" + "io" +) + +func verify(publicKeyAsString string, signatureAsString string, in io.Reader, out io.Writer) error { + publicKey, err := readPublicKey(bytes.NewBufferString(publicKeyAsString)) + + if err != nil { + return err + } + + signature, err := readSignature(bytes.NewBufferString(signatureAsString)) + + if err != nil { + return err + } + + digest := sha256.New() + + _, err = io.Copy(digest, in) + + if err != nil { + return err + } + + isVerified := ecdsa.VerifyASN1(publicKey, digest.Sum(nil), signature) + + if !isVerified { + return errors.New("Bad Signature") + } + + _, err = out.Write([]byte("OK\n")) + + return err +}