runes

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2023 License: MIT Imports: 12 Imported by: 0

README

go-runes

Runes for authentication (like macaroons only simpler) ported to Go. The original Python implementation is found here

I'm by no means an expert in cryptography so please, if you see something bad, PRs are welcome.

What are Runes?

Runes are like cookies for authorization but extra restrictions can be added by clients and shared with others. Those runes can then still be authenticated by the server.

Rune Language

A rune is a series of restrictions; you have to pass all of them (so appending a new one always makes the rune less powerful). Each restriction is one or more alternatives ("cmd=foo OR cmd=bar"), any one of which can pass.

The form of each alternative is a simple string:

ALTERNATIVE := FIELDNAME CONDITION VALUE

FIELDNAME contains only UTF-8 characters, exclusive of ! " # $ % & ' ( ) * +, - . / : ; ? @ [ \ ] ^ _ ` { | } ~ (C's ispunct()). These can appear inside a VALUE, but &, | and \\ must be escaped with \ (escaping is legal for any character, but unnecessary).

CONDITION is one of the following values:

  • !: Pass if field is missing (value ignored)
  • =: Pass if exists and exactly equals
  • /: Pass if exists and is not exactly equal
  • ^: Pass if exists and begins with
  • $: Pass if exists and ends with
  • ~: Pass if exists and contains
  • <: Pass if exists, is a valid integer (may be signed), and numerically less than
  • >: Pass if exists, is a valid integer (may be signed), and numerically greater than
  • }: Pass if exists and lexicograpically greater than (or longer)
  • {: Pass if exists and lexicograpically less than (or shorter)
  • #: Always pass: no condition, this is a comment.

Grouping using ( and ) may be added in future.

A restriction is a group of alternatives separated by |; restrictions are separated by &. e.g.

cmd=foo | cmd=bar
& subcmd! | subcmd{get

The first requires cmd be present, and to be foo or bar. The second requires that subcmd is not present, or is lexicographically less than get. Both must be true for authorization to succeed.

Rune Authorization

A run also comes with a SHA-256 authentication code. This is generated as SHA-256 of the following bytestream:

  1. The secret (less than 56 bytes, known only to the server which issued it).
  2. For every restriction:
    1. Pad the stream as per SHA-256 (i.e. append 0x80, then zeroes, then the big-endian 64-bit bitcount so far, such that it's a multiple of 64 bytes).
    2. Append the restriction.

By using the same padding scheme as SHA-256 usually uses to end the data, we have the property that we can initialize the SHA-256 function with the result from any prior restriction, and continue.

The server can validate the rune authorization by repeating this procedure and checking the result.

Rune Encoding

Runes are encoded as base64, starting with the 256-bit SHA256 authentication code, the followed by one or more restrictions separated by &.

Not because base64 is good, but because it's familiar to Web people; we use RFC3548 with + and / replaced by - and _ to make it URL safe.

(There's also a string encoding which is easier to read and debug).

Best Practices

It's usually worth including an id in each rune you hand out so that you can blacklist particular runes in future (your other option is to change your master secret, but that revokes all runes). Because this appears in all runes, using the empty fieldname (''), and a simple counter reduces overall size, but you could use a UUID.

This is made trivial by the unique_id parameter to Rune() and MasterRune(): it adds such an empty field with the unique id (which the default evaluator will ignore unless you handle it explicitly).

You may also include version number, to allow future runes to have different interpretations: this appends '-[version]' in the '' field: the default handler will fail any cookie that has a version field (for safe forward compatibility).

The rune unmarshalling code ensures that if an empty parameter exists, it's the first one, and it's of a valid form.

API Example

make a rune

package main

import (
    "log"

    "github.com/TheRebelOfBabylon/go-runes"
)

func main() {
    secret := make([]byte, 16)

    // make a top level rune with no id
    masterRune, err := runes.NewMasterRune(secret, "", "")
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("masterrune=%s", masterRune.encode())
}

add a restriction

package main

import (
    "log"

    "github.com/TheRebelOfBabylon/go-runes"
)

func main() {
    ...
    restr, err := runes.RestrictionFromString("f1=1")
    if err != nil {
        log.Fatal(err)
    }
    if err = masterRune.AddRestriction(restr); err != nil {
        log.Fatal(err)
    }
}

parse a rune and test its authenticity

package main

import (
    "log"

    "github.com/TheRebelOfBabylon/go-runes"
)

func main() {
    ...
    newRune, err := runes.RuneFromEncodedString(masterRune.encode())
    if err != nil {
        log.Println(err)
    }
    // check if it's legit
    if err = masterRune.Check(newRune.encode(), map[string]runes.Test{"f1": {"1", runes.StandardTestFunc}}); err != nil {
        log.Println(err)
    }
}

make a custom test function for your runes

package main

import (
    "log"

    "github.com/TheRebelOfBabylon/go-runes"
)

func main() {
    ...
    var customTestFunc runes.TestFunc = func(alt *runes.Alternative, v interface{}) error {
        // do some interesting, custom authentication. Maybe rate limiting
        return nil
    }
    // check if it passes our custom test func
    if err = masterRune.Check(newRune.encode(), map[string]runes.Test{"f1": {"1", customTestFunc}}); err != nil {
        log.Println(err)
    }
}

Author

The original author of the idea and the creator of the original implementation is Rusty Russell.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	Punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"

	ErrInvalidField          = errors.New("field not valid")
	ErrInvalidCondition      = errors.New("condition not valid")
	ErrMissingField          = errors.New("missing field in test")
	ErrFieldIsPresent        = errors.New("field is present")
	ErrForbiddenValue        = errors.New("forbidden value")
	ErrInvalidValuePrefix    = errors.New("value has invalid perfix")
	ErrInvalidValueSuffix    = errors.New("value has invalid suffix")
	ErrValueDoesntContain    = errors.New("value does not contain substring")
	ErrValueTooLarge         = errors.New("value too large")
	ErrValueTooSmall         = errors.New("value too small")
	ErrWrongLexicOrder       = errors.New("wrong lexicographical order")
	ErrNoRestrictions        = errors.New("no restrictions")
	ErrNoOperator            = errors.New("restriction contains no operator")
	ErrIdFieldHasAlts        = errors.New("unique_id field can't have alternatives")
	ErrExtraChars            = errors.New("restriction has extra ending characters")
	ErrInvalidRunePrefix     = errors.New("rune strings must start with 64 hex digits then '-'")
	ErrSecretTooLarge        = errors.New("secret too large")
	ErrCondValueTypeMismatch = errors.New("condition and test value type mismatch")
	ErrUnauthorizedRune      = errors.New("unauthorized rune")
	ErrInvalidUniqueIdCond   = errors.New("unique_id condition must be '='")
	ErrIdUknownVersion       = errors.New("unique_id unknown version")
	ErrIdHasHyphens          = errors.New("hyphen not allowed in unique_id")
	ErrIdFieldForbidden      = errors.New("unique_id fiield not valid here")
)

Functions

func EndShastream

func EndShastream(length int) []byte

EndShastream simulates a SHA-256 ending pad

Types

type Alternative

type Alternative struct {
	Field     string
	Value     string
	Condition string
}

func NewAlternative

func NewAlternative(field, cond, value string, allowIdField bool) (*Alternative, error)

NewAlternative creates a new Alternative

func (*Alternative) String

func (a *Alternative) String() string

String formats the alternative into a string

func (*Alternative) Test

func (a *Alternative) Test(tests map[string]Test) error

Test Checks if the alternative passes the given tests

type Restriction

type Restriction []*Alternative

func RestrictionFromString

func RestrictionFromString(encodedString string, allowIdField bool) (Restriction, error)

RestrictionFromString creates restrictions from an escaped string

func UniqueIdRestriction

func UniqueIdRestriction(id, version string) (Restriction, error)

UniqueIdRestriction creates a unique Id restriction

func (Restriction) String

func (r Restriction) String() string

String formats the restriction into a string

func (Restriction) Test

func (r Restriction) Test(tests map[string]Test) error

Test performs the given tests on the restrictions

type Rune

type Rune struct {
	Restrictions []Restriction
	// contains filtered or unexported fields
}

func NewMasterRune

func NewMasterRune(secret []byte, id, version string) (*Rune, error)

NewMasterRune creates a new master rune

func NewRuneFromAuthbase

func NewRuneFromAuthbase(authbase []byte, uniqueId, version string, restrictions []Restriction) (*Rune, error)

NewRuneFromAuthbase creates a new rune from a given authbase and list of restrictions

func RuneFromAuthcode

func RuneFromAuthcode(authcode []byte, restrictions []Restriction) (*Rune, error)

RuneFromAuthcode parses a rune from a given authcode and a list of restrictions

func RuneFromEncodedString

func RuneFromEncodedString(encodedString string) (*Rune, error)

RuneFromEncodedString parses a rune from an encoded rune string

func RuneFromString

func RuneFromString(runeString string) (*Rune, error)

RuneFromString parses a rune from a rune string

func (*Rune) AddRestriction

func (r *Rune) AddRestriction(restriction Restriction) error

AddRestrictions adds new restrictions to the rune

func (*Rune) AreRestrictionsMet

func (r *Rune) AreRestrictionsMet(tests map[string]Test) error

AreRestrictionsMet tests the rune restrictions. If any fail, returns an error

func (*Rune) Authcode

func (r *Rune) Authcode() []byte

Authcode returns the SHA256 of the authbase

func (*Rune) Check

func (r *Rune) Check(encodedRune string, tests map[string]Test) error

Check checks if a given rune is authorized by the parent rune and passes the given tests

func (*Rune) Clone added in v1.0.1

func (r *Rune) Clone() (*Rune, error)

Clone creates a copy of the current rune

func (*Rune) Encode

func (r *Rune) Encode() string

Encode returns the base64 encoded rune

func (*Rune) IsRuneAuthorized

func (r *Rune) IsRuneAuthorized(otherRune *Rune) bool

IsRuneAuthorized checks whether or not a given rune has been authorized by the master rune

func (*Rune) String

func (r *Rune) String() string

String returns the string encoded version of the rune

type Test

type Test struct {
	Value    interface{}
	TestFunc TestFunc
}

Test is a struct made for passing a value and a test function for Restriction testing

type TestFunc

type TestFunc func(alt *Alternative, v interface{}) error

TestFunc is a type for creating custom tests for restrictions

var StandardTestFunc TestFunc = func(alt *Alternative, v interface{}) error {
	switch value := v.(type) {
	case string:
		switch alt.Condition {
		case "!":
			return fmt.Errorf("%s: %w", alt.Field, ErrFieldIsPresent)
		case "=":
			if alt.Value != value {
				return fmt.Errorf("%s: %w", alt.Value, ErrForbiddenValue)
			}
		case "/":
			if alt.Value == value {
				return fmt.Errorf("%s: %w", alt.Value, ErrForbiddenValue)
			}
		case "^":
			if !strings.HasPrefix(value, alt.Value) {
				return fmt.Errorf("%s: %w", alt.Value, ErrInvalidValuePrefix)
			}
		case "$":
			if !strings.HasSuffix(value, alt.Value) {
				return fmt.Errorf("%s: %w", alt.Value, ErrInvalidValueSuffix)
			}
		case "~":
			if !strings.Contains(value, alt.Value) {
				return fmt.Errorf("%s: %w", alt.Value, ErrValueDoesntContain)
			}
		case "{":
			if !(value < alt.Value) {
				return fmt.Errorf("%s: %w", alt.Value, ErrWrongLexicOrder)
			}
		case "}":
			if !(value > alt.Value) {
				return fmt.Errorf("%s: %w", alt.Value, ErrWrongLexicOrder)
			}
		case "<":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			vInt, err := strconv.ParseInt(value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if !(vInt < valueAsInt) {
				return fmt.Errorf("%s: %w", alt.Value, ErrValueTooLarge)
			}
		case ">":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			vInt, err := strconv.ParseInt(value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if !(vInt > valueAsInt) {
				return fmt.Errorf("%s: %w", alt.Value, ErrValueTooSmall)
			}
		default:
			return fmt.Errorf("%s & %T: %w", alt.Condition, value, ErrCondValueTypeMismatch)
		}
	case int:
		switch alt.Condition {
		case "=":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if int64(value) != valueAsInt {
				return fmt.Errorf("%s: %w", alt.Value, ErrForbiddenValue)
			}
		case "/":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if int64(value) == valueAsInt {
				return fmt.Errorf("%s: %w", alt.Value, ErrForbiddenValue)
			}
		case "<":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if !(int64(value) < valueAsInt) {
				return fmt.Errorf("%s: %w", alt.Value, ErrValueTooLarge)
			}
		case ">":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if !(int64(value) > valueAsInt) {
				return fmt.Errorf("%s: %w", alt.Value, ErrValueTooSmall)
			}
		default:
			return fmt.Errorf("%s & %T: %w", alt.Condition, value, ErrCondValueTypeMismatch)
		}
	case int64:
		switch alt.Condition {
		case "=":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if value != valueAsInt {
				return fmt.Errorf("%s: %w", alt.Value, ErrForbiddenValue)
			}
		case "/":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if value == valueAsInt {
				return fmt.Errorf("%s: %w", alt.Value, ErrForbiddenValue)
			}
		case "<":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if !(value < valueAsInt) {
				return fmt.Errorf("%s: %w", alt.Value, ErrValueTooLarge)
			}
		case ">":
			valueAsInt, err := strconv.ParseInt(alt.Value, 10, 64)
			if err != nil {
				return fmt.Errorf("%s: %w", alt.Value, err)
			}
			if !(value > valueAsInt) {
				return fmt.Errorf("%s: %w", alt.Value, ErrValueTooSmall)
			}
		default:
			return fmt.Errorf("%s & %T: %w", alt.Condition, value, ErrCondValueTypeMismatch)
		}
	}
	return nil
}

TODO - Finish covering all base type cases

Jump to

Keyboard shortcuts

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