Commit 9cd4485b authored by ale's avatar ale

Initial commit

parents
Copyright (c) 2017 Autistici/Inventati <info@autistici.org>
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.
package client
import (
"bytes"
"context"
"io"
"net/textproto"
"git.autistici.org/id/auth"
)
var defaultSocket = "/run/auth/socket"
type Client struct {
socketPath string
codec auth.Codec
}
func New(socketPath string) *Client {
return &Client{
socketPath: socketPath,
codec: auth.DefaultCodec,
}
}
func (c *Client) Authenticate(ctx context.Context, req *auth.Request) (*auth.Response, error) {
conn, err := textproto.Dial("unix", c.socketPath)
if err != nil {
return nil, err
}
defer conn.Close()
var resp auth.Response
done := make(chan error)
go func() {
defer close(done)
// Write the command to a buffer.
var buf bytes.Buffer
io.WriteString(&buf, "auth ")
buf.Write(c.codec.Encode(req))
if err := conn.PrintfLine(buf.String()); err != nil {
done <- err
return
}
// Read the response.
line, err := conn.ReadLineBytes()
if err != nil {
done <- err
return
}
if err := c.codec.Decode(line, &resp); err != nil {
done <- err
return
}
done <- nil
}()
// Wait for the call to terminate, or the context to time out,
// whichever happens first.
select {
case err := <-done:
return &resp, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"git.autistici.org/id/auth/server"
"github.com/coreos/go-systemd/daemon"
)
var (
configPath = flag.String("config", "/etc/authserver/config.yml", "configuration `file`")
socketPath = flag.String("socket", "/run/auth/socket", "`path` to the UNIX socket to listen on")
systemdSocketActivation = flag.Bool("systemd-socket", false, "use SystemD socket activation")
)
func usage() {
fmt.Fprintf(os.Stderr, `authserv - a simple authentication server
Usage: authserv [<OPTIONS>]
Known options:
`)
flag.PrintDefaults()
}
func main() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
syscall.Umask(007)
config, err := server.LoadConfig(*configPath)
if err != nil {
log.Fatalf("could not load configuration: %v", err)
}
authSrv, err := server.NewServer(config, nil)
if err != nil {
log.Fatalf("configuration error: %v", err)
}
var sockSrv *server.SocketServer
if *systemdSocketActivation {
sockSrv, err = server.NewSystemdSocketServer(authSrv)
} else {
sockSrv, err = server.NewSocketServer(*socketPath, authSrv)
}
if err != nil {
log.Fatalf("error: %v", err)
}
sigCh := make(chan os.Signal, 1)
go func() {
<-sigCh
log.Printf("terminating")
sockSrv.Close()
}()
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
log.Printf("starting")
daemon.SdNotify(false, "READY=1")
sockSrv.Serve()
}
package auth
import (
"bytes"
"encoding/base64"
"fmt"
"io"
)
// Codec serializes simple key/value maps on the wire. Requirements
// for a codec: it must transfer maps, it must be trivial to implement
// in both C, Go and Python. It must not have messy external
// dependencies if possible.
type Codec interface {
Encode(interface{}) []byte
Decode([]byte, interface{}) error
}
type mapEncodable interface {
EncodeToMap(map[string]string, string)
}
type mapDecodable interface {
DecodeFromMap(map[string]string, string)
}
type kvCodec struct{}
var DefaultCodec = &kvCodec{}
//var charsToEscape = "\" \r\n"
func shouldEscapeString(s string) bool {
for _, r := range s {
if r < 32 || r > 127 || r == '"' || r == ' ' || r == '\r' || r == '\n' {
return true
}
}
return false
}
func (c kvCodec) encodeMap(obj map[string]string) []byte {
var buf bytes.Buffer
first := true
for key, value := range obj {
if first {
first = false
} else {
io.WriteString(&buf, " ")
}
fmt.Fprintf(&buf, "%s=", key)
if shouldEscapeString(value) {
w := base64.NewEncoder(base64.RawURLEncoding, &buf)
io.WriteString(w, value)
w.Close()
} else {
fmt.Fprintf(&buf, "\"%s\"", value)
}
}
return buf.Bytes()
}
func (c kvCodec) Encode(obj interface{}) []byte {
var m map[string]string
switch t := obj.(type) {
case map[string]string:
m = t
case mapEncodable:
m = make(map[string]string)
t.EncodeToMap(m, "")
default:
// TODO: Error
}
return c.encodeMap(m)
}
type inputScanner struct {
b []byte
pos int
}
func (i *inputScanner) next() (byte, bool) {
if i.pos >= len(i.b) {
return 0, true
}
value := i.b[i.pos]
i.pos++
return value, false
}
func (i *inputScanner) ungetc() {
if i.pos > 0 {
i.pos--
}
}
func (i *inputScanner) skipWhitespace() {
for {
c, eof := i.next()
if eof {
return
}
if c != ' ' {
break
}
}
i.ungetc()
}
func (i *inputScanner) parseUntil(sep byte) ([]byte, error) {
pos := i.pos
idx := bytes.IndexByte(i.b[pos:], sep)
if idx == -1 {
return nil, io.EOF
}
value := i.b[i.pos : i.pos+idx]
i.pos += idx + 1
return value, nil
}
func (i *inputScanner) parseUntilOrEOF(sep byte) []byte {
pos := i.pos
idx := bytes.IndexByte(i.b[pos:], sep)
if idx == -1 {
i.pos = len(i.b)
} else {
i.pos = pos + idx
}
return i.b[pos:i.pos]
}
func (i *inputScanner) parseKey() (string, error) {
i.skipWhitespace()
key, err := i.parseUntil('=')
if err != nil {
return "", err
}
return string(key), nil
}
func (i *inputScanner) parseQuotedString() (string, error) {
s, err := i.parseUntil('"')
if err != nil {
return "", err
}
return string(s), nil
}
func (i *inputScanner) parseBase64String() (string, error) {
data := i.parseUntilOrEOF(' ')
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
_, err := base64.RawURLEncoding.Decode(out, data)
if err != nil {
return "", err
}
return string(out), nil
}
func (i *inputScanner) parseValue() (string, error) {
c, eof := i.next()
if eof {
return "", io.EOF
}
if c == '"' {
return i.parseQuotedString()
}
i.ungetc()
return i.parseBase64String()
}
func (i *inputScanner) parseAssignment() (string, string, error) {
key, err := i.parseKey()
if err == io.EOF {
return "", "", nil
} else if err != nil {
return "", "", err
}
value, err := i.parseValue()
if err != nil {
return "", "", err
}
return key, value, nil
}
func (c kvCodec) decodeMap(b []byte, out map[string]string) (map[string]string, error) {
if out == nil {
out = make(map[string]string)
}
i := &inputScanner{b: b}
for {
key, value, err := i.parseAssignment()
if err != nil {
return nil, err
}
if key == "" {
break
}
out[key] = value
}
return out, nil
}
func (c kvCodec) Decode(b []byte, obj interface{}) error {
var m map[string]string
switch t := obj.(type) {
case map[string]string:
m = t
}
var err error
m, err = c.decodeMap(b, m)
if err != nil {
return err
}
switch t := obj.(type) {
case mapDecodable:
t.DecodeFromMap(m, "")
}
return nil
}
package auth
import (
"bytes"
"fmt"
"math/rand"
"reflect"
"testing"
)
func makeRandomString(n int) string {
var buf bytes.Buffer
for i := 0; i < n; i++ {
c := byte(rand.Intn(254) + 1)
buf.Write([]byte{c})
}
return buf.String()
}
func makeRandomMap() map[string]string {
m := make(map[string]string)
for i := 0; i < 7; i++ {
var value string
n := rand.Intn(10)
switch {
case n < 3:
value = "asdf \""
case n < 5:
value = makeRandomString(64)
default:
value = fmt.Sprintf("value%d", i+1)
}
m[fmt.Sprintf("key%d", i+1)] = value
}
return m
}
func TestCodec_UTF8(t *testing.T) {
c := &kvCodec{}
m := map[string]string{
"hello": "Hello, 世界",
}
b := c.Encode(m)
m2 := make(map[string]string)
err := c.Decode(b, m2)
if err != nil {
t.Fatal("Decode():", err)
}
if !reflect.DeepEqual(m, m2) {
t.Fatalf("results differ: %+v vs %+v", m, m2)
}
}
func TestCodec_Random(t *testing.T) {
c := &kvCodec{}
for i := 0; i < 1000; i++ {
m := makeRandomMap()
b := c.Encode(m)
//t.Logf("encoded: '%s'", string(b))
m2 := make(map[string]string)
err := c.Decode(b, m2)
if err != nil {
t.Fatalf("Decode(%s): %v", string(b), err)
}
if !reflect.DeepEqual(m, m2) {
t.Fatalf("decode results differ: %+v vs %+v", m, m2)
}
}
}
package auth
import (
"fmt"
"github.com/tstranex/u2f"
)
// DeviceInfo holds information about the client device. We use a
// simple persistent cookie to track the same client device across
// multiple session.
type DeviceInfo struct {
ID string
RemoteAddr string
RemoteZone string
Browser string
OS string
}
func (d *DeviceInfo) encodeToMap(m map[string]string, prefix string) {
m[prefix+"id"] = d.ID
m[prefix+"remote_addr"] = d.RemoteAddr
m[prefix+"remote_zone"] = d.RemoteZone
m[prefix+"browser"] = d.Browser
m[prefix+"os"] = d.OS
}
func decodeDeviceInfoFromMap(m map[string]string, prefix string) *DeviceInfo {
if _, ok := m[prefix+"id"]; !ok {
return nil
}
return &DeviceInfo{
ID: m[prefix+"id"],
RemoteAddr: m[prefix+"remote_addr"],
RemoteZone: m[prefix+"remote_zone"],
Browser: m[prefix+"browser"],
OS: m[prefix+"os"],
}
}
// Request to authenticate a user. It supports multiple methods for
// authentication including challenge-response 2FA.
type Request struct {
Service string
Username string
Password []byte
OTP string
U2FAppID string
U2FResponse *u2f.SignResponse
DeviceInfo *DeviceInfo
//Extra map[string]string
}
func (r *Request) EncodeToMap(m map[string]string, prefix string) {
m[prefix+"service"] = r.Service
m[prefix+"username"] = r.Username
m[prefix+"password"] = string(r.Password)
if r.OTP != "" {
m[prefix+"otp"] = r.OTP
}
if r.U2FAppID != "" {
m[prefix+"u2f_app_id"] = r.U2FAppID
}
if r.U2FResponse != nil {
encodeU2FResponseToMap(r.U2FResponse, m, prefix+"u2f_response.")
}
if r.DeviceInfo != nil {
r.DeviceInfo.encodeToMap(m, prefix+"device.")
}
}
func (r *Request) DecodeFromMap(m map[string]string, prefix string) {
r.Service = m[prefix+"service"]
r.Username = m[prefix+"username"]
r.Password = []byte(m[prefix+"password"])
r.OTP = m[prefix+"otp"]
r.U2FAppID = m[prefix+"u2f_app_id"]
r.U2FResponse = decodeU2FResponseFromMap(m, prefix+"u2f_response.")
r.DeviceInfo = decodeDeviceInfoFromMap(m, prefix+"device.")
}
// UserInfo contains optional user information.
type UserInfo struct {
Email string
Groups []string
}
func (u *UserInfo) EncodeToMap(m map[string]string, prefix string) {
if u.Email != "" {
m[prefix+"email"] = u.Email
}
for i, g := range u.Groups {
m[fmt.Sprintf("%sgroup.%d.", prefix, i)] = g
}
}
func decodeUserInfoFromMap(m map[string]string, prefix string) *UserInfo {
u := UserInfo{Email: m[prefix+"email"]}
i := 0
for {
s, ok := m[fmt.Sprintf("%sgroup.%d.", prefix, i)]
if !ok {
break
}
u.Groups = append(u.Groups, s)
i++
}
if u.Email == "" && len(u.Groups) == 0 {
return nil
}
return &u
}
// Response to an authentication request.
type Response struct {
Status Status
TFAMethod TFAMethod
U2FSignRequest *u2f.WebSignRequest
UserInfo *UserInfo
}
func (r *Response) EncodeToMap(m map[string]string, prefix string) {
m[prefix+"status"] = r.Status.String()
m[prefix+"2fa_method"] = string(r.TFAMethod)
if r.U2FSignRequest != nil {
// External type.
encodeU2FSignRequestToMap(r.U2FSignRequest, m, prefix+"u2f_req.")
}
if r.UserInfo != nil {
r.UserInfo.EncodeToMap(m, prefix+"user.")
}
}
func (r *Response) DecodeFromMap(m map[string]string, prefix string) {
r.Status = parseAuthStatus(m[prefix+"status"])
r.TFAMethod = TFAMethod(m[prefix+"2fa_method"])
r.U2FSignRequest = decodeU2FSignRequestFromMap(m, prefix+"u2f_req.")
r.UserInfo = decodeUserInfoFromMap(m, prefix+"user.")
}
// TFAMethod is a hint provided to the caller with the type of 2FA
// method that is available for authentication.
type TFAMethod string
const (
TFAMethodNone = ""
TFAMethodOTP = "otp"
TFAMethodU2F = "u2f"
)
// Status of an authentication request.
type Status int
const (
StatusOK = iota
StatusInsufficientCredentials
StatusError
)
func (s Status) String() string {
switch s {
case StatusOK:
return "ok"
case StatusInsufficientCredentials:
return "insufficient_credentials"
case StatusError:
return "error"
default:
return fmt.Sprintf("unknown(%d)", int(s))
}
}
func parseAuthStatus(s string) Status {
switch s {
case "ok":
return StatusOK
case "insufficient_credentials":
return StatusInsufficientCredentials
default:
return StatusError
}
}
func (s Status) Error() string {
return s.String()
}
// Miscellaneous serializers for external objects.
func encodeU2FResponseToMap(resp *u2f.SignResponse, m map[string]string, prefix string) {
m[prefix+"key_handle"] = resp.KeyHandle
m[prefix+"signature_data"] = resp.SignatureData
m[prefix+"client_data"] = resp.ClientData
}
func decodeU2FResponseFromMap(m map[string]string, prefix string) *u2f.SignResponse {
if _, ok := m[prefix+"key_handle"]; !ok {
return nil
}
return &u2f.SignResponse{
KeyHandle: m[prefix+"key_handle"],
SignatureData: m[prefix+"signature_data"],
ClientData: m[prefix+"client_data"],
}
}
func encodeU2FSignRequestToMap(req *u2f.WebSignRequest, m map[string]string, prefix string) {
m[prefix+"app_id"] = req.AppID
m[prefix+"challenge"] = req.Challenge
for i, key := range req.RegisteredKeys {
encodeU2FRegisteredKeyToMap(key, m, fmt.Sprintf("%sregistered_keys.%d.", prefix, i))
}
}
func encodeU2FRegisteredKeyToMap(key u2f.RegisteredKey, m map[string]string, prefix string) {
m[prefix+"version"] = key.Version
m[prefix+"key_handle"] = key.KeyHandle
m[prefix+"app_id"] = key.AppID
}
func decodeU2FSignRequestFromMap(m map[string]string, prefix string) *u2f.WebSignRequest {
if _, ok := m[prefix+"app_id"]; !ok {
return nil
}
req := u2f.WebSignRequest{
AppID: m[prefix+"app_id"],
Challenge: m[prefix+"challenge"],
}
i := 0
for {
key := decodeU2FRegisteredKeyFromMap(m, fmt.Sprintf("%sregistered_keys.%d.", prefix, i))
if key == nil {
break
}
req.RegisteredKeys = append(req.RegisteredKeys, *key)
i++
}
return &req
}
func decodeU2FRegisteredKeyFromMap(m map[string]string, prefix string) *u2f.RegisteredKey {
if _, ok := m[prefix+"version"]; !ok {