ssp

package module
v0.0.0-...-d6bf5a2 Latest Latest
Warning

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

Go to latest
Published: Jan 2, 2024 License: MIT Imports: 20 Imported by: 0

README

sqrl-ssp

SQRL is a identiy managment system that is meant to replace usernames and passwords for online account authentication. It requires a user to have a SQRL client that securely manages their identity. The server interacts with the SQRL client to authenticate a user (similar but more secure than a username/password challenge). Once a user's identity is established, a session should be established if the desired behavior is for a user to remain "logged in". This is typically a session cookie or authentication token.

This implements the public parts of the SQRL authentication server API as specified here: https://www.grc.com/sqrl/sspapi.htm. This library is meant to be pluggable into a broader infrastructure to handle whatever type of session management you desire. It also allows pluggable storage options and scales horizontally.

This project is still very much a work in-progress. All the endpoints log a ton of debugging information.

Documentation Go Report Card

Integration

The ssp.SqrlSspAPI struct is a configurable server that exposes http.HandlerFuncs that implement the SSP API. The main one is the ssp.SqrlSspAPI.Cli handler which directly handles communication from the SQRL client. These endpoints can be configured to run as a standalone service or as part of a larger API structure. There are several required pieces of configuration that must be provided to integrate SQRL into broader user management.

Authenticator

The basis of the SSP API is to manage SQRL identities. The goal of this library is to manage these identities and allow for loosly coupling an identity to a "user". This is similar in concept to a user having a username and password which may be changed for a given user. A SQRL idenity can be associated with a user, and at a later time that identity may be disabled or removed from a user, or a new identity may be associated with that user. These actions are supported by the ssp.Authenticator interface.

Hoard and AuthStore

The SSP API has requirements for storage exposed by the Hoard and AuthStore interfaces. Because an extended pun is always fun, a Hoard stores Nuts. Nuts are SQRL's cryptographic nonces. A Hoard also has stores pending auth information associated with the Nut. These are ephemperal and have an expiration so are best stored in a in-memory store like Redis or memcached. The AuthStore saves the SQRL identity information and should be a durable database like PostgreSQL or MariaDB. Both are interfaces so any storage should be able to be plugged in. The ssp package provides map-backed implementations for both which are NOT recommended for production use.

I've written a Redis-backed Hoard implementation at github.com/sqrldev/server-go-ssp-redishoard I've written a GORM-backed (GORM supports several different database backends) AuthStore implementation at github.com/sqrldev/server-go-ssp-gormauthstore

Trees

Trees produce Nuts. There are several ways to produce a secure nonce. GRC reccommends an in-memory counter-based nonce, but the design does not easily scale horizontally. Multiple servers could produce the same nonce if they are not externally coordinated (like through a globally consistent counter like a PostgreSQL sequence.) The ssp package provides ssp.GrcTree as an implementation of this, but I reccommend using ssp.RandomTree if you're using multiple servers.

API

This package only implements the public parts of the SSP API intentionally. The callbacks provided by the Authenticator interface should allow integration with any auth system; includig embedding in a larger existing auth service or aloowing the SSP service to stand alone and send requests to another authorization and/or user management service.

I've also made some convenient additions to the standard API.

/nut.sqrl

In addition to the nut, this endpoint returns a "pag" parameter that must be used by the web browser (or other user-agent) to poll the /pag.sqrl endpoint. For security, it's required to tie the nut to the original requestor so that another casual observer of the QR code cannot hijack authentication. The GRC server does this implicitly through browser cookies. The pagnut makes this explicit and is not tied to cookies to make it more friendly to API-only usage. The pag value must be kept secret at the user-agent to ensure security.

I've also added an "exp" parameter which is the expiration in seconds of the nut. This may be used to refresh the nut/png to prevent users from failing to authenticate due to using a stale nut.

I also support a JSON version of the response that can be accessed by adding "Accept: application/json" header to the request. By default it always returns application/x-www-form-urlencoded as per the GRC spec.

/png.sqrl

Normally, a "nut" parameter which comes from the /nut.sqrl endpoint is required to produce a valid QR code. I've added some additional functionality to allow this to be one-step. Calling /png.sqrl with no parameters will return a new QR code with the nut values as headers:

Sqrl-Nut
Sqrl-Pag
Sqrl-Exp

If a user-agent has easy access to the headers of the image request, this is a good way to get everything in one call. These headers are NOT included if the nut parameter is provided as it is assumed the caller has already gotten them from the /nut.sqrl endpoint.

/pag.sqrl

This endpoint requires sending both the "nut" and "pag" parameters. See section for /nut.sqrl. Otherwise it follows the GRC spec and returns a redirect URL that should authorize the user.

I also support a JSON version of the response that can be accessed by adding "Accept: application/json" header to the request. The response body is an object with a single "url" parameter.

Documentation

Overview

Package ssp implements the SQRL server-side protocol (SSP). The SqrlSspApi is a stateful server object that manages SQRL identities. The /cli.sqrl exposed at Cli is the only endpoint that is required to operate in conjunction with the SQRL client. This endpoint is required to be served over https.

While it's possible that this code can be run within a web server that terminates TLS itself, the expectation is that it is served from behind a load balancer or reverse proxy. While I attempt to reconstruct the host and paths from the request and standard forwarding headers, this can be unreliable and it's best to confgure the HostOverride and RootPath

Index

Constants

View Source
const (
	// the identity has been seen before
	TIFIDMatch = 0x1
	// the previous identity is a known identity
	TIFPreviousIDMatch = 0x2
	// the IP address of the current request and the original Nut request match
	TIFIPMatched = 0x4
	// the SQRL account is disabled
	TIFSQRLDisabled = 0x8
	// the ClientBody.Cmd is not recognized
	TIFFunctionNotSupported = 0x10
	// used for all the random server errors like failures to connect to datastores
	TIFTransientError = 0x20
	// the specific ClientBody.Cmd could not be completed for any reason
	TIFCommandFailed = 0x40
	// the client sent bad or unrecognized data or signature validation failed
	TIFClientFailure = 0x80
	// The owner of the Nut doesn't match this request
	TIFBadIDAssociation = 0x100
	// The IDK has been rekeyed to a newer one
	TIFIdentitySuperseded = 0x200
)

TIF bitflags

View Source
const SqrlScheme = "sqrl"

SqrlScheme is sqrl

Variables

View Source
var ErrNotFound = fmt.Errorf("Not Found")

ErrNotFound specific error returned if a Hoard or identity isn't found. This is to differentiate from more serious errors at the storage level

Sqrl64 is a shortcut base64.RawURLEncoding encoding which is used pervasively throughout the SQRL protocol

View Source
var TIFDesc = map[uint32]string{
	TIFIDMatch:              "ID Matched",
	TIFPreviousIDMatch:      "Previous ID Matched",
	TIFIPMatched:            "IP Matched",
	TIFSQRLDisabled:         "Identity disabled",
	TIFFunctionNotSupported: "Command not recognized",
	TIFTransientError:       "Server Error",
	TIFCommandFailed:        "Command failed",
	TIFClientFailure:        "Bad client request",
	TIFBadIDAssociation:     "Mismatch of nut to idk",
	TIFIdentitySuperseded:   "Identity superseded by newer one",
}

TIFDesc description of the TIF bits

Functions

func ParseSqrlQuery

func ParseSqrlQuery(query string) (params map[string]string, err error)

ParseSqrlQuery copied from go's url.ParseQuery with some modifications. The format is CRLF separated "key=value" pairs

Types

type Ask

type Ask struct {
	Message string `json:"message"`
	Button1 string `json:"button1,omitempty"`
	URL1    string `json:"url1,omitempty"`
	Button2 string `json:"button2,omitempty"`
	URL2    string `json:"url2,omitempty"`
}

Ask holds optional response to queries that will be shown to the user from the SQRL client

func ParseAsk

func ParseAsk(askString string) *Ask

ParseAsk parses the special Ask format

func (*Ask) Encode

func (a *Ask) Encode() string

Encode creates the tilde and semicolon separated ask format

type AuthStore

type AuthStore interface {
	FindIdentity(idk string) (*SqrlIdentity, error)
	SaveIdentity(identity *SqrlIdentity) error
	DeleteIdentity(idk string) error
}

AuthStore stores SQRL identities

type Authenticator

type Authenticator interface {
	// Called when a SQRL identity has been successfully authenticated. It
	// should return a URL that will finish authentication to create a
	// logged in session. This is also called for a new user.
	// If an error occurs this should return an error
	// page redirection
	AuthenticateIdentity(identity *SqrlIdentity) string
	// When an identity is rekeyed, it's necessary to swap the identity
	// associated with a given user. This callback happens when a user
	// wishes to swap their previous identity for a new one.
	SwapIdentities(previousIdentity, newIdentity *SqrlIdentity) error
	// This denotes an identity is now removed and this identity
	// should be disassociated with a user. This does not necessarily
	// mean the user should be deleted though. The SQRL spec mentions
	// being able to re-associate another identity at a later time (possibly
	// during the same login session)
	RemoveIdentity(identity *SqrlIdentity) error
	// Send an ask response back to the SQRL client.
	// Since this is triggered on query and not ident,
	// the identity may only contain Idk. Ask responses
	// will be included as part of the SqrlIdentity sent via
	// AuthenticateIdentity
	AskResponse(identity *SqrlIdentity) *Ask
}

Authenticator interface to allow user management triggered by SQRL authentication events.

type CliRequest

type CliRequest struct {
	Client        *ClientBody `json:"client"`
	ClientEncoded string      `json:"clientEncoded"`
	Server        string      `json:"server"`
	Ids           string      `json:"ids"`
	Pids          string      `json:"pids"`
	Urs           string      `json:"urs"`

	IPAddress string // saved here for reference
}

CliRequest holds the data sent from the SQRL client to the /cli.sqrl endpoint

func ParseCliRequest

func ParseCliRequest(r *http.Request) (*CliRequest, error)

ParseCliRequest parses and validates the request. The CliRequest can be trusted if no error is returned as the signatures have been checked.

func (*CliRequest) Encode

func (cr *CliRequest) Encode() string

Encode creates the form encoded POST body from CliRequest

func (*CliRequest) Identity

func (cr *CliRequest) Identity() *SqrlIdentity

Identity creates an identity from a request

func (*CliRequest) IsAuthCommand

func (cr *CliRequest) IsAuthCommand() bool

IsAuthCommand is a command that authenticates (ident, enable)

func (*CliRequest) SigningString

func (cr *CliRequest) SigningString() []byte

SigningString creates the string that is signed by ids, pids and urs

func (*CliRequest) UpdateIdentity

func (cr *CliRequest) UpdateIdentity(identity *SqrlIdentity) bool

UpdateIdentity updates identity from request

func (*CliRequest) ValidateLastResponse

func (cr *CliRequest) ValidateLastResponse(lastRepsonse []byte) bool

ValidateLastResponse checks to make sure the response on this request matches a stored on that's passed in.

func (*CliRequest) VerifyPidsSignature

func (cr *CliRequest) VerifyPidsSignature() error

VerifyPidsSignature verifies the pids signature against the pidk in the ClientBody

func (*CliRequest) VerifySignature

func (cr *CliRequest) VerifySignature() error

VerifySignature verifies the ids signature against the idk in the ClientBody. It also calls VerifyPidsSignature if necessary.

func (*CliRequest) VerifyUrs

func (cr *CliRequest) VerifyUrs(vuk string) error

VerifyUrs validates a urs signature against a passed in vuk. This call will fail if the urs doesn't exist because it is required for several operations. Don't call this if you don't need it.

type CliResponse

type CliResponse struct {
	Version []int
	Nut     Nut
	TIF     uint32
	Qry     string
	URL     string
	Sin     string
	Suk     string
	Ask     *Ask
	Can     string

	// HoardCache is not serialized but the encoded response is saved here
	// so we can check it in the next request
	HoardCache *HoardCache
}

CliResponse encodes a response to the SQRL client As specified https://www.grc.com/sqrl/semantics.htm

func NewCliResponse

func NewCliResponse(nut Nut, qry string) *CliResponse

NewCliResponse creates a minimal valid CliResponse object

func ParseCliResponse

func ParseCliResponse(body []byte) (*CliResponse, error)

ParseCliResponse parses a server response

func (*CliResponse) ClearIDMatch

func (cr *CliResponse) ClearIDMatch() *CliResponse

ClearIDMatch set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) ClearPreviousIDMatch

func (cr *CliResponse) ClearPreviousIDMatch() *CliResponse

ClearPreviousIDMatch clears the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) Encode

func (cr *CliResponse) Encode() []byte

Encode writes the response as the CRNL format and encodes it using Sqrl64 encoding.

func (*CliResponse) WithBadIDAssociation

func (cr *CliResponse) WithBadIDAssociation() *CliResponse

WithBadIDAssociation set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithClientFailure

func (cr *CliResponse) WithClientFailure() *CliResponse

WithClientFailure set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithCommandFailed

func (cr *CliResponse) WithCommandFailed() *CliResponse

WithCommandFailed set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithFunctionNotSupported

func (cr *CliResponse) WithFunctionNotSupported() *CliResponse

WithFunctionNotSupported set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithIDMatch

func (cr *CliResponse) WithIDMatch() *CliResponse

WithIDMatch set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithIPMatch

func (cr *CliResponse) WithIPMatch() *CliResponse

WithIPMatch set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithIdentitySuperseded

func (cr *CliResponse) WithIdentitySuperseded() *CliResponse

func (*CliResponse) WithPreviousIDMatch

func (cr *CliResponse) WithPreviousIDMatch() *CliResponse

WithPreviousIDMatch set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithSQRLDisabled

func (cr *CliResponse) WithSQRLDisabled() *CliResponse

WithSQRLDisabled set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

func (*CliResponse) WithTransientError

func (cr *CliResponse) WithTransientError() *CliResponse

WithTransientError set the appropriate TIF bits on this response. Returns the object for easier chaining (not immutability).

type ClientBody

type ClientBody struct {
	Version []int           `json:"version"`
	Cmd     string          `json:"cmd"`
	Opt     map[string]bool `json:"opt"`
	Suk     string          `json:"suk"`  // Sqrl64.Encoded
	Vuk     string          `json:"vuk"`  // Sqrl64.Encoded
	Pidk    string          `json:"pidk"` // Sqrl64.Encoded
	Idk     string          `json:"idk"`  // Sqrl64.Encoded
	// valid values are 0,1,2; -1 means no value
	Btn int `json:"btn"`
}

ClientBody holds the internal structure of the request "client" parameter; see https://www.grc.com/sqrl/protocol.htm in the section "The content of the “client” parameter." This is owned by a ClientRequest and probably shouldn't be used on it's own.

func ClientBodyFromParams

func ClientBodyFromParams(params map[string]string) (*ClientBody, error)

ClientBodyFromParams creates ClientBody from the output of ParseSqrlQuery

func (*ClientBody) Encode

func (cb *ClientBody) Encode() []byte

Encode returns the ClientBody encoded in Sqrl64

func (*ClientBody) PidkPublicKey

func (cb *ClientBody) PidkPublicKey() (ed25519.PublicKey, error)

PidkPublicKey decodes and validates the Pidk as a ed25519.PublicKey

func (*ClientBody) PublicKey

func (cb *ClientBody) PublicKey() (ed25519.PublicKey, error)

PublicKey decodes and validates the Idk as a ed25519.PublicKey

type GrcTree

type GrcTree struct {
	// contains filtered or unexported fields
}

GrcTree Creates a 64-bit nut based on the GRC spec using a monotonic counter and blowfish cipher

func NewGrcTree

func NewGrcTree(counterInit uint64, blowfishKey []byte) (*GrcTree, error)

NewGrcTree takes an initial counter value (in the case of reboot) and a blowfish key (use a max key of random 56 bytes) https://godoc.org/golang.org/x/crypto/blowfish

func (*GrcTree) Nut

func (gt *GrcTree) Nut() (Nut, error)

Nut Create a nut based on the GRC spec.

type Hoard

type Hoard interface {
	Get(nut Nut) (*HoardCache, error)
	GetAndDelete(nut Nut) (*HoardCache, error)
	Save(nut Nut, value *HoardCache, expiration time.Duration) error
}

Hoard stores Nuts for later use

type HoardCache

type HoardCache struct {
	State        string        `json:"state"`
	RemoteIP     string        `json:"remoteIP"`
	OriginalNut  Nut           `json:"originalNut"`
	PagNut       Nut           `json:"pagNut"`
	LastRequest  *CliRequest   `json:"lastRequest"`
	Identity     *SqrlIdentity `json:"identity"`
	LastResponse []byte        `json:"lastResponse"`
}

HoardCache is the state associated with a Nut

type MapAuthStore

type MapAuthStore struct {
	// contains filtered or unexported fields
}

MapAuthStore stores identities in a map in the process. Great for testing but you should probably use some database to store these in a production environment.

func NewMapAuthStore

func NewMapAuthStore() *MapAuthStore

NewMapAuthStore inits the internal map

func (*MapAuthStore) DeleteIdentity

func (m *MapAuthStore) DeleteIdentity(idk string) error

DeleteIdentity implements AuthStore

func (*MapAuthStore) FindIdentity

func (m *MapAuthStore) FindIdentity(idk string) (*SqrlIdentity, error)

FindIdentity implements AuthStore

func (*MapAuthStore) SaveIdentity

func (m *MapAuthStore) SaveIdentity(identity *SqrlIdentity) error

SaveIdentity implements AuthStore

type MapHoard

type MapHoard struct {
	// contains filtered or unexported fields
}

MapHoard implements a Hoard that is backed by an in-memory map

func NewMapHoard

func NewMapHoard() *MapHoard

NewMapHoard creates a new MapHoard

func (*MapHoard) Get

func (mh *MapHoard) Get(nut Nut) (*HoardCache, error)

Get implements Hoard

func (*MapHoard) GetAndDelete

func (mh *MapHoard) GetAndDelete(nut Nut) (*HoardCache, error)

GetAndDelete implements Hoard

func (*MapHoard) Save

func (mh *MapHoard) Save(nut Nut, value *HoardCache, expiration time.Duration) error

Save implements Hoard

type Nut

type Nut string

Nut is a cryptographic nonce used by SQRL

type RandomTree

type RandomTree struct {
	// contains filtered or unexported fields
}

RandomTree produces random nuts

func NewRandomTree

func NewRandomTree(byteSize int) (*RandomTree, error)

NewRandomTree takes a bytesize between 8 and 20 Shorter nuts are preferred; but if you think your deployment would require more bits to be unique you can create larger ones

func (*RandomTree) Nut

func (rt *RandomTree) Nut() (Nut, error)

Nut Create a pure random nut

type SqrlIdentity

type SqrlIdentity struct {
	Idk      string `json:"idk" sql:"primary_key"`
	Suk      string `json:"suk"`
	Vuk      string `json:"vuk"`
	Pidk     string `json:"pidk"` // TODO do we need to keep track of Pidk?
	SQRLOnly bool   `json:"sqrlOnly"`
	Hardlock bool   `json:"hardlock"`
	Disabled bool   `json:"disabled"`
	Rekeyed  string `json:"rekeyed"` // If this Idk has been rekeyed, this links to the new ID
	// Btn is filled in if the request includes a button press response from an
	// ask. -1 if there's no value.
	Btn int `json:"-" sql:"-"`
}

SqrlIdentity holds all the info about a valid SQRL identity

type SqrlSspAPI

type SqrlSspAPI struct {
	NutExpiration time.Duration

	// set to the hostname for serving SQRL urls; this can include a port if necessary
	HostOverride string
	// if the SQRL endpoints are not at the root of the host, then this overrides the path where they are hosted
	RootPath      string
	Authenticator Authenticator
	// contains filtered or unexported fields
}

SqrlSspAPI implements the endpoitns outlined here https://www.grc.com/sqrl/sspapi.htm

func NewSqrlSspAPI

func NewSqrlSspAPI(tree Tree, hoard Hoard, authenticator Authenticator, authStore AuthStore) *SqrlSspAPI

NewSqrlSspAPI takes a Tree implementation that produces Nuts. If set to nil, a the API defaults to NewRandomTree(8). Also needs a Hoard to store a retrieve Nuts

func (*SqrlSspAPI) Cli

func (api *SqrlSspAPI) Cli(w http.ResponseWriter, r *http.Request)

Cli implements the /cli.sqrl endpoint

func (*SqrlSspAPI) HTTPSRoot

func (api *SqrlSspAPI) HTTPSRoot(r *http.Request) *url.URL

HTTPSRoot returns the best guess at the https root URL for this server

func (*SqrlSspAPI) Host

func (api *SqrlSspAPI) Host(r *http.Request) string

Host gets the host in order of preference: SqrlSspAPI.HostOverride, header X-Forwarded-Host, Request.Host

func (*SqrlSspAPI) Nut

func (api *SqrlSspAPI) Nut(w http.ResponseWriter, r *http.Request)

Nut implements the /nut.sqrl endpoint TODO sin, ask and 1-9 params

func (*SqrlSspAPI) NutExpirationSeconds

func (api *SqrlSspAPI) NutExpirationSeconds() int

NutExpirationSeconds has a self-explanatory name

func (*SqrlSspAPI) PNG

func (api *SqrlSspAPI) PNG(w http.ResponseWriter, r *http.Request)

PNG implements the /png.sqrl endpoint

func (*SqrlSspAPI) Pag

func (api *SqrlSspAPI) Pag(w http.ResponseWriter, r *http.Request)

Pag implements the /pag.sqrl endpoint

func (*SqrlSspAPI) RemoteIP

func (api *SqrlSspAPI) RemoteIP(r *http.Request) string

RemoteIP gets the remote IP as a string from a request It prefers the X-Forwarded-For header since it's likely this server will be behind a load balancer

type Tree

type Tree interface {
	Nut() (Nut, error)
}

A Tree produces Nuts :)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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