signedstrings

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 17, 2023 License: MIT Imports: 7 Imported by: 0

README

Go HMAC-SHA256 signing

Go reference zero dependencies under 150 LOC great coverage Go report card

Why?

Doing a HMAC-SHA256 is trivial:

func hmacSHA256(message, key []byte) string {
    var hash [sha256.Size]byte
    alg := hmac.New(sha256.New, key)
    alg.Write(message)
    alg.Sum(hash[:0])
    return hex.EncodeToString(hash[:])
}

Sometimes, though, you also want:

  • key rotation support;
  • parsing of the keys;
  • parsing of the signed messages (strings.CutLast would be so nice to have);
  • sanity checks to avoid signing something with an empty or short key due to misconfiguration;
  • maybe even adding a prefix to identify the tokens (for security leak prevention, log sanitization and input sanity checking purposes);

...all without littering your code with these uninteresting details. That's where signedstrings comes in, a tiny utility library.

IMPORTANT: signedstrings does NOT add a timestamp or a random nonce, and will always return the same string given the same inputs. This will enable replay attacks in certain use cases. As a professional, you are expected to know what you're doing when using security primitives, HMAC-SHA256 included. If you don't, you REALLY should not be writing security-sensitive code, sorry.

Usage

Install:

go get github.com/andreyvit/signedstrings

Use:

conf := &signedstrings.Configuration{
    Prefixes: []string{"MYAPPTOKEN-"}, // optional!
}
flag.Var(&conf.Keys, "signing-keys", "key(s) for signed tokens") // or envflag.Var

...
signed := conf.Sign("foo")
// MYAPPTOKEN-foo-1c54...7a1e

...
data, err := conf.Validate(signed)
// data == "foo"
// errors: signedstrings.Invalid, signedstrings.InvalidSig

IMPORTANT: signedstrings does NOT add a timestamp or a random nonce, and will always return the same string given the same inputs. This will enable replay attacks in certain use cases. As a professional, you are expected to know what you're doing when using security primitives, HMAC-SHA256 included. If you don't, you REALLY should not be writing security-sensitive code, sorry.

Generating Keys & Choosing Key Length

I recommend 64-byte fully random keys. Generate via openssl rand -hex 64. This is the maximum key size supported by HMAC-SHA256; anything longer will be hashed down to a 32-byte key, so don't use longer keys.

StackOverflow says 32 bytes are enough, though, if you prefer shorter keys.

IMPORTANT: Note the fully random part. The key should come from a cryptographically secure random number generator like crypto/rand or openssl rand. Depending on your use case, you might get away with using a non-random key, but in that case, please make sure you know what you are doing. Ditto for other key lengths.

As a secure default, this library will panic when encountering keys shorter than 32 bytes. If you truly want to use shorter keys, adjust the minimum key length:

signedstrings.MinKeyLen = 4  // live dangerously

Key & Prefix Rotation

You can specify multiple keys and multiple prefixes in your configuration, they will all be accepted for validation. The first key and the first prefix are used to sign new messages. This means you can add a new key or a new prefix while continuing to support prior ones.

Again, note that it's the first key and the first prefix that is used for new signatures. You are supposed to insert keys to the front of the list.

Contributing

This library is feature-complete, but you can always contribute:

  • bug fixes
  • better documentation and examples
  • more tests
  • better ways to handle things internally

We recommend modd (go install github.com/cortesi/modd/cmd/modd@latest) for continuous testing during development.

MIT license

Copyright (c) 2023 Andrey Tarantsov. Published under the terms of the MIT license.

Documentation

Overview

Example (Plain)
package main

import (
	"encoding/hex"
	"fmt"

	"github.com/andreyvit/signedstrings"
)

var exampleKey = must(hex.DecodeString("d850af431064164d9a73891fa0a257ba91e5cb18a67de07d3507b8ccdc8781c2"))

func main() {
	conf := signedstrings.Configuration{
		Keys: [][]byte{exampleKey},
		Sep:  " :: ",
	}

	fmt.Println(conf.Sign("some text to sign"))

	print(conf.Validate("some text to sign :: 2f9a0cb84617f6e394a22068504f59ba3e7903c4dc1fd995cc4a940ffeef90d8"))

	print(conf.Validate(" :: "))
	print(conf.Validate("some text to sign"))
	print(conf.Validate("some text to sign :: 1111111111111111111111111111111111111111111111111111111111111111"))

}

func print(v any, err error) {
	if err != nil {
		fmt.Println("err: " + err.Error())
	} else {
		fmt.Println(v)
	}
}

func must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}
Output:

some text to sign :: 2f9a0cb84617f6e394a22068504f59ba3e7903c4dc1fd995cc4a940ffeef90d8
some text to sign
err: invalid string
err: invalid string
err: invalid signature
Example (Token)
package main

import (
	"encoding/hex"
	"fmt"

	"github.com/andreyvit/signedstrings"
)

var exampleKey = must(hex.DecodeString("d850af431064164d9a73891fa0a257ba91e5cb18a67de07d3507b8ccdc8781c2"))

func main() {
	conf := signedstrings.Configuration{
		Keys:     [][]byte{exampleKey},
		Prefixes: []string{"TOKEN-"},
	}

	fmt.Println(conf.Sign("foo"))

	print(conf.Validate("TOKEN-foo-4bc019e2218479926f27694a281b8b2af30f86f5f522d0bbde31ab19bc730f39"))

	print(conf.Validate(""))
	print(conf.Validate("foo-4bc019e2218479926f27694a281b8b2af30f86f5f522d0bbde31ab19bc730f39"))
	print(conf.Validate("TOKEN-foo-1111111111111111111111111111111111111111111111111111111111111111"))
}

func print(v any, err error) {
	if err != nil {
		fmt.Println("err: " + err.Error())
	} else {
		fmt.Println(v)
	}
}

func must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}
Output:

TOKEN-foo-4bc019e2218479926f27694a281b8b2af30f86f5f522d0bbde31ab19bc730f39
foo
err: invalid string
err: invalid string
err: invalid signature

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// Invalid is the error returned for incorrectly formatted messages.
	Invalid = errors.New("invalid string")
	// InvalidSig is the error returned for correctly formatted messages that
	// fail signature validation (i.e. have been corrupted or tampered with).
	InvalidSig = errors.New("invalid signature")
)
View Source
var MinKeyLen = 32

Minimum acceptable length of secure **fully random** keys. Note that this is a variable, so you can adjust it if desired.

Functions

This section is empty.

Types

type Configuration

type Configuration struct {
	// Keys are accepted when validating signatures. The first key is the one used
	// when signing new messages. Multiple valid keys allow for key rotation.
	Keys Keys

	// Prefixes are added in front of the tokens to help identify them.
	// The first one is used for new tokens. Others are accepted when
	// validating tokens to allow prefix changes.
	//
	// An empty prefix is a valid choice. Omitting this field is the same as
	// specifying a single empty prefix.
	Prefixes []string

	// Sep is the separator between the data and the signature, a cosmetic choice.
	// Defaults to a dash.
	Sep string
}

func (*Configuration) Sign

func (conf *Configuration) Sign(data string) string

Sign signs the given string (and adds a configured prefix if any).

func (*Configuration) Validate

func (conf *Configuration) Validate(signed string) (string, error)

Validate verifies the signature on the given string, and returns the original value if the signature is valid.

type Keys

type Keys [][]byte

Keys is a convenience type for a list of []byte keys. Can be used with flag.Var and its compatibles. Defines a sensible String().

Example
package main

import (
	"flag"
	"fmt"

	"github.com/andreyvit/signedstrings"
)

func main() {
	var keys signedstrings.Keys
	flags := flag.NewFlagSet("", flag.PanicOnError)
	flags.Var(&keys, "keys", "explanation")
	flags.Parse([]string{"-keys", "d850af431064164d9a73891fa0a257ba91e5cb18a67de07d3507b8ccdc8781c2,65ce238cb1b11d17a00c94c875394f500b05abd24c276a01691bdf9ce00d213c"})
	fmt.Println(keys)
}
Output:

d850af431064164d9a73891fa0a257ba91e5cb18a67de07d3507b8ccdc8781c2 65ce238cb1b11d17a00c94c875394f500b05abd24c276a01691bdf9ce00d213c

func ParseKeys

func ParseKeys(s string) (Keys, error)

ParseKeys parses a comma or whitespace-separated list of hex-encoded keys.

Example
package main

import (
	"encoding/hex"
	"fmt"

	"github.com/andreyvit/signedstrings"
)

func main() {
	// WARNING: use longer keys, these are very short, for demonstration only
	keys, err := signedstrings.ParseKeys("d850af431064164d9a73891fa0a257ba91e5cb18a67de07d3507b8ccdc8781c2 65ce238cb1b11d17a00c94c875394f500b05abd24c276a01691bdf9ce00d213c,,,283d54389c394ed33ba4146eff7b4133f7e393cb905d089a06798456a1cb7dcd")
	if err != nil {
		panic(err)
	}

	fmt.Println(hex.EncodeToString(keys[0]))
	fmt.Println(hex.EncodeToString(keys[1]))
	fmt.Println(hex.EncodeToString(keys[2]))

	print(signedstrings.ParseKeys("zzz"))
	print(signedstrings.ParseKeys("d850"))

}

func print(v any, err error) {
	if err != nil {
		fmt.Println("err: " + err.Error())
	} else {
		fmt.Println(v)
	}
}
Output:

d850af431064164d9a73891fa0a257ba91e5cb18a67de07d3507b8ccdc8781c2
65ce238cb1b11d17a00c94c875394f500b05abd24c276a01691bdf9ce00d213c
283d54389c394ed33ba4146eff7b4133f7e393cb905d089a06798456a1cb7dcd
err: encoding/hex: invalid byte: U+007A 'z'
err: 2-byte key is too short, need at least 32 bytes

func (Keys) Get

func (v Keys) Get() interface{}

func (*Keys) Set

func (v *Keys) Set(raw string) (err error)

func (Keys) String

func (v Keys) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL