Skip to content
Snippets Groups Projects
Commit c05eedc7 authored by ale's avatar ale
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
package backend
import (
"context"
"errors"
"io/ioutil"
"log"
"strings"
"time"
"gopkg.in/ldap.v2"
)
type LDAPQueryConfig struct {
// SearchBase, SearchFilter and Scope define parameters for
// the LDAP search. The search should return a single object.
// SearchBase and SearchFilter can contain the string "%s",
// which will be replaced with the username before performing
// a query.
SearchBase string `yaml:"search_base"`
SearchFilter string `yaml:"search_filter"`
Scope string `yaml:"scope"`
// Attr is the LDAP attribute holding the encrypted user key.
Attr string `yaml:"attr"`
}
// Valid returns an error if the configuration is invalid.
func (c *LDAPQueryConfig) Valid() error {
if c.SearchBase == "" {
return errors.New("empty search_base")
}
if c.SearchFilter == "" {
return errors.New("empty search_filter")
}
if c.Scope != "one" && c.Scope != "sub" {
return errors.New("unknown scope")
}
if c.Attr == "" {
return errors.New("empty attr")
}
return nil
}
func (c *LDAPQueryConfig) searchRequest(username string) *ldap.SearchRequest {
u := ldap.EscapeFilter(username)
base := strings.Replace(c.SearchBase, "%s", u, -1)
filter := strings.Replace(c.SearchFilter, "%s", u, -1)
scope := ldap.ScopeWholeSubtree
if c.Scope == "one" {
scope = ldap.ScopeSingleLevel
}
return ldap.NewSearchRequest(
base,
scope,
ldap.NeverDerefAliases,
0,
0,
false,
filter,
[]string{c.Attr},
nil,
)
}
// LDAPConfig holds the global configuration for the LDAP user backend.
type LDAPConfig struct {
URI string `yaml:"uri"`
BindDN string `yaml:"bind_dn"`
BindPwFile string `yaml:"bind_pw_file"`
Query *LDAPQueryConfig `yaml:"query"`
}
// Valid returns an error if the configuration is invalid.
func (c *LDAPConfig) Valid() error {
if c.URI == "" {
return errors.New("empty uri")
}
if c.BindDN == "" {
return errors.New("empty bind_dn")
}
if c.BindPwFile == "" {
return errors.New("empty bind_pw_file")
}
if c.Query == nil {
return errors.New("missing query configuration")
}
return c.Query.Valid()
}
type ldapBackend struct {
config *LDAPConfig
conn *ldap.Conn
}
func NewLDAPBackend(config *LDAPConfig) (*ldapBackend, error) {
// Validate configuration.
if err := config.Valid(); err != nil {
return nil, err
}
// Read the bind password.
bindPw, err := ioutil.ReadFile(config.BindPwFile)
if err != nil {
return nil, err
}
// Connect.
conn, err := ldap.Dial("unix", "/var/lib/ldapi")
if err != nil {
return nil, err
}
if err = conn.Bind(config.BindDN, strings.TrimSpace(string(bindPw))); err != nil {
conn.Close()
return nil, err
}
return &ldapBackend{
config: config,
conn: conn,
}, nil
}
func (b *ldapBackend) GetKeys(ctx context.Context, username string) [][]byte {
// Try to turn the context deadline into a LDAP connection timeout...
if deadline, ok := ctx.Deadline(); ok {
b.conn.SetTimeout(time.Until(deadline))
}
result, err := b.conn.Search(b.config.Query.searchRequest(username))
if err != nil {
log.Printf("LDAP error: %v", err)
return nil
}
var out [][]byte
for _, ent := range result.Entries {
k := []byte(ent.GetAttributeValue(b.config.Query.Attr))
out = append(out, k)
}
return out
}
package main
import (
"flag"
"io/ioutil"
"log"
"os"
"strings"
"gopkg.in/yaml.v2"
"git.autistici.org/ai3/go-common/serverutil"
"git.autistici.org/id/keystore"
"git.autistici.org/id/keystore/server"
)
// func init() {
// if err := syscall.Mlockall(syscall.MCL_CURRENT | syscall.MCL_FUTURE); err != nil {
// panic(err)
// }
// }
var (
addr = flag.String("addr", ":5006", "address to listen on")
configFile = flag.String("config", "/etc/keystore/config.yml", "path of config file")
)
// Wrap the keystore.Config together with the server setup in a single
// configuration object.
type Config struct {
KeyStoreConfig *keystore.Config `yaml:"keystore"`
ServerConfig *serverutil.ServerConfig `yaml:"http_server"`
}
func loadConfig() (*Config, error) {
// Read YAML config.
data, err := ioutil.ReadFile(*configFile)
if err != nil {
return nil, err
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
// Set defaults for command-line flags using variables from the environment.
func setFlagDefaultsFromEnv() {
flag.VisitAll(func(f *flag.Flag) {
envVar := "KEYSTORE_" + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1))
if value := os.Getenv(envVar); value != "" {
f.DefValue = value
f.Value.Set(value)
}
})
}
func main() {
setFlagDefaultsFromEnv()
flag.Parse()
config, err := loadConfig()
if err != nil {
log.Fatal(err)
}
ks, err := keystore.New(config.KeyStoreConfig)
if err != nil {
log.Fatal(err)
}
srv := server.New(ks)
if err := serverutil.Serve(srv, config.ServerConfig, *addr); err != nil {
log.Fatal(err)
}
}
package keystore
import (
"github.com/miscreant/miscreant/go"
"golang.org/x/crypto/scrypt"
)
func decrypt(data, pw []byte) ([]byte, error) {
// Apply the key derivation function to the password to obtain
// a 64 byte key.
dk, err := scrypt.Key(pw, nil, 16384, 1, 8, 64)
if err != nil {
return nil, err
}
// Set up the AES-SIV secret box.
cipher, err := miscreant.NewAESCMACSIV(dk)
if err != nil {
return nil, err
}
return cipher.Open(nil, data)
}
package keystore
import (
"context"
"errors"
"io/ioutil"
"strings"
"sync"
"time"
"git.autistici.org/id/go-sso"
"git.autistici.org/id/keystore/backend"
)
var (
ErrNoKeys = errors.New("no keys available")
ErrBadPassword = errors.New("could not decrypt key with password")
ErrBadUser = errors.New("username does not match authentication token")
ErrInvalidTTL = errors.New("invalid ttl")
)
// Database represents the interface to the underlying backend for
// encrypted user keys.
type Database interface {
GetKeys(context.Context, string) [][]byte
}
type userKey struct {
pkey []byte
expiry time.Time
}
// Config for the KeyStore.
type Config struct {
SSOPublicKeyFile string `yaml:"sso_public_key_file"`
SSOService string `yaml:"sso_service"`
SSODomain string `yaml:"sso_domain"`
//Backend string `yaml:"backend"`
LDAPConfig *backend.LDAPConfig `yaml:"ldap"`
}
func (c *Config) check() error {
if c.SSOService == "" {
return errors.New("sso_service is empty")
}
if !strings.HasSuffix(c.SSOService, "/") {
return errors.New("sso_service is invalid (does not end with /)")
}
if c.SSODomain == "" {
return errors.New("sso_domain is empty")
}
if c.LDAPConfig == nil {
return errors.New("missing backend config")
}
return nil
}
// KeyStore holds decrypted secrets for users in memory for a short
// time (of the order of a SSO session lifespan). User secrets can be
// opened with a password (used to decrypt the key, which is stored
// encrypted in a database), queried, and closed (forgotten).
//
// The database can provide multiple versions of the encrypted key (to
// support multiple decryption passwords), in which case we'll try
// them all sequentially until one of them decrypts successfully with
// the provided password.
//
// In order to query the KeyStore, you need to present a valid SSO
// token for the user whose secrets you would like to obtain.
//
type KeyStore struct {
mx sync.Mutex
userKeys map[string]userKey
db Database
service string
validator sso.Validator
}
// New creates a new KeyStore with the given config and returns it.
func New(config *Config) (*KeyStore, error) {
if err := config.check(); err != nil {
return nil, err
}
ssoKey, err := ioutil.ReadFile(config.SSOPublicKeyFile)
if err != nil {
return nil, err
}
v, err := sso.NewValidator(ssoKey, config.SSODomain)
if err != nil {
return nil, err
}
// There is only one supported backend type, ldap.
ldap, err := backend.NewLDAPBackend(config.LDAPConfig)
if err != nil {
return nil, err
}
s := &KeyStore{
userKeys: make(map[string]userKey),
service: config.SSOService,
validator: v,
db: ldap,
}
go s.expire()
return s, nil
}
func (s *KeyStore) expire() {
for t := range time.NewTicker(600 * time.Second).C {
s.mx.Lock()
for u, k := range s.userKeys {
if k.expiry.After(t) {
wipeBytes(k.pkey)
delete(s.userKeys, u)
}
}
s.mx.Unlock()
}
}
// Open the user's key store with the given password. If successful,
// the unencrypted user key will be stored for at most ttlSeconds, or
// until Close is called.
//
// A Context is needed because this method might issue an RPC.
func (s *KeyStore) Open(ctx context.Context, username, password string, ttlSeconds int) error {
if ttlSeconds == 0 {
return ErrInvalidTTL
}
encKeys := s.db.GetKeys(ctx, username)
if len(encKeys) == 0 {
return ErrNoKeys
}
var pkey []byte
var err error
for _, key := range encKeys {
pkey, err = decrypt(key, []byte(password))
if err == nil {
break
}
}
if err != nil {
return ErrBadPassword
}
s.mx.Lock()
s.userKeys[username] = userKey{
pkey: pkey,
expiry: time.Now().Add(time.Duration(ttlSeconds) * time.Second),
}
s.mx.Unlock()
return nil
}
// Get the unencrypted key for the specified user. The caller needs to
// provide a valid SSO ticket for the user.
func (s *KeyStore) Get(username, ssoTicket string) ([]byte, error) {
// Validate the SSO ticket.
tkt, err := s.validator.Validate(ssoTicket, "", s.service, nil)
if err != nil {
return nil, err
}
if tkt.User != username {
return nil, ErrBadUser
}
s.mx.Lock()
defer s.mx.Unlock()
u, ok := s.userKeys[username]
if !ok {
return nil, ErrNoKeys
}
return u.pkey, nil
}
// Close the user's key store and wipe the associated unencrypted key
// from memory.
func (s *KeyStore) Close(username string) {
s.mx.Lock()
if k, ok := s.userKeys[username]; ok {
wipeBytes(k.pkey)
delete(s.userKeys, username)
}
s.mx.Unlock()
}
func wipeBytes(b []byte) {
for i := 0; i < len(b); i++ {
b[i] = 0
}
}
package keystore
type OpenRequest struct {
Username string `json:"username"`
Password string `json:"password"`
TTL int `json:"ttl"`
}
type OpenResponse struct{}
type GetRequest struct {
Username string `json:"username"`
SSOTicket string `json:"sso_ticket"`
}
type GetResponse struct {
Key []byte `json:"key"`
}
type CloseRequest struct {
Username string `json:"username"`
}
type CloseResponse struct{}
package server
import (
"log"
"net/http"
"git.autistici.org/ai3/go-common/serverutil"
"git.autistici.org/id/keystore"
)
var emptyResponse struct{}
type keyStoreServer struct {
*keystore.KeyStore
}
func (s *keyStoreServer) handleOpen(w http.ResponseWriter, r *http.Request) {
var req keystore.OpenRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return
}
err := s.KeyStore.Open(r.Context(), req.Username, req.Password, req.TTL)
if err != nil {
log.Printf("Open(%s) error: %v", req.Username, err)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
serverutil.EncodeJSONResponse(w, &emptyResponse)
}
func (s *keyStoreServer) handleGet(w http.ResponseWriter, r *http.Request) {
var req keystore.GetRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return
}
key, err := s.KeyStore.Get(req.Username, req.SSOTicket)
if err != nil {
log.Printf("Get(%s) error: %v", req.Username, err)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
serverutil.EncodeJSONResponse(w, &keystore.GetResponse{Key: key})
}
func (s *keyStoreServer) handleClose(w http.ResponseWriter, r *http.Request) {
var req keystore.CloseRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return
}
s.KeyStore.Close(req.Username)
serverutil.EncodeJSONResponse(w, &emptyResponse)
}
func (s *keyStoreServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/open":
s.handleOpen(w, r)
case "/api/get_key":
s.handleGet(w, r)
case "/api/close":
s.handleClose(w, r)
default:
http.NotFound(w, r)
}
}
func New(ks *keystore.KeyStore) http.Handler {
return &keyStoreServer{ks}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment