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

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
WKD service
===
This little daemon serves public OpenPGP keys of our users, using the
[WKD](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/) protocol.
The mechanism has multiple moving parts:
* code in ai3/accountserver to expose and manipulate PGP keys as part
of the user API ("business logic")
* code in ai3/pannello to let users attach keys to their email
accounts (UI)
* this server, which implements the WKD protocol
package main
import (
"flag"
"io/ioutil"
"log"
"git.autistici.org/ai3/go-common/serverutil"
"git.autistici.org/ai3/tools/wkd"
"gopkg.in/yaml.v3"
)
var (
addr = flag.String("addr", ":5057", "address to listen on")
configFile = flag.String("config", "/etc/wkd.yml", "path of config file")
)
type config struct {
LDAP struct {
URI string `yaml:"uri"`
BindDN string `yaml:"bind_dn"`
BindPw string `yaml:"bind_pw"`
BaseDN string `yaml:"base_dn"`
Filter string `yaml:"filter"`
} `yaml:"ldap"`
HTTP *serverutil.ServerConfig `yaml:"http_server"`
}
// Read the YAML configuration file.
func loadConfig() (*config, error) {
data, err := ioutil.ReadFile(*configFile)
if err != nil {
return nil, err
}
// Set some default values.
var c config
if err := yaml.Unmarshal(data, &c); err != nil {
return nil, err
}
return &c, nil
}
func main() {
log.SetFlags(0)
flag.Parse()
conf, err := loadConfig()
if err != nil {
log.Fatal(err)
}
storage, err := wkd.NewLDAPStorage(
conf.LDAP.URI,
conf.LDAP.BindDN,
conf.LDAP.BindPw,
conf.LDAP.BaseDN,
conf.LDAP.Filter)
if err != nil {
log.Fatal(err)
}
server := wkd.NewServer(storage, wkd.AdvancedSchema)
log.Printf("starting wkdserver on %s", *addr)
if err := serverutil.Serve(server, conf.HTTP, *addr); err != nil {
log.Fatal(err)
}
}
go.mod 0 → 100644
module git.autistici.org/ai3/tools/wkd
go 1.15
require (
git.autistici.org/ai3/go-common v0.0.0-20220322095548-58b071b836f9
github.com/go-ldap/ldap/v3 v3.4.2
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
go.sum 0 → 100644
This diff is collapsed.
ldap.go 0 → 100644
package wkd
import (
"context"
"errors"
"fmt"
"strings"
ldaputil "git.autistici.org/ai3/go-common/ldap"
"github.com/go-ldap/ldap/v3"
)
const poolSize = 5
const (
mailLDAPAttr = "mail"
keyLDAPAttr = "openPGPKey"
hashLDAPAttr = "openPGPKeyHash"
)
type ldapStorage struct {
pool *ldaputil.ConnectionPool
baseDN string
filter string
}
func NewLDAPStorage(uri, bindDN, bindPw, baseDN, filter string) (Storage, error) {
pool, err := ldaputil.NewConnectionPool(uri, bindDN, bindPw, poolSize)
if err != nil {
return nil, err
}
if filter == "" {
filter = fmt.Sprintf("(%s=%%s)", hashLDAPAttr)
}
if !strings.Contains(filter, "%s") {
return nil, errors.New("filter expression does not contain literal '%s' token")
}
return &ldapStorage{
pool: pool,
baseDN: baseDN,
filter: filter,
}, nil
}
func (s *ldapStorage) Lookup(ctx context.Context, hash string) (*Key, error) {
filter := fmt.Sprintf(s.filter, ldap.EscapeFilter(hash))
req := ldap.NewSearchRequest(
s.baseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
filter,
[]string{mailLDAPAttr, keyLDAPAttr},
nil,
)
result, err := s.pool.Search(ctx, req)
if err != nil {
return nil, err
}
if len(result.Entries) < 1 {
return nil, ErrNotFound
}
entry := result.Entries[0]
return &Key{
Addr: entry.GetAttributeValue(mailLDAPAttr),
Data: []byte(entry.GetAttributeValue(keyLDAPAttr)),
}, nil
}
server.go 0 → 100644
package wkd
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
)
const (
WELLKNOWN = "/.well-known/openpgpkey/"
VERSION = 13
)
var ErrNotFound = errors.New("not found")
var errInvalidPath = errors.New("path did not match")
// Request parameters for the WKD protocol.
type Request struct {
IsPolicy bool
LocalPart string
Domain string
Hash string
}
func (r *Request) Addr() string {
return fmt.Sprintf("%s@%s", strings.ToLower(r.LocalPart), r.Domain)
}
// SchemaFunc is our request parser function.
type SchemaFunc func(*http.Request) (*Request, error)
// AdvancedSchema implements the 'advanced' URL schema, e.g.:
// https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe
// https://openpgpkey.example.org/.well-known/openpgpkey/example.org/policy
func AdvancedSchema(r *http.Request) (*Request, error) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, WELLKNOWN), "/")
switch {
case len(parts) == 2 && parts[1] == "policy":
return &Request{IsPolicy: true}, nil
case len(parts) == 3 && parts[1] == "hu":
return &Request{
LocalPart: r.FormValue("l"),
Domain: parts[0],
Hash: parts[2],
}, nil
default:
return nil, errInvalidPath
}
}
// DirectSchema implements the 'direct' URL schema, e.g.:
// https://example.org/.well-known/openpgpkey/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe
// https://example.org/.well-known/openpgpkey/policy
func DirectSchema(r *http.Request) (*Request, error) {
path := strings.TrimPrefix(r.URL.Path, WELLKNOWN)
if path == "policy" {
return &Request{IsPolicy: true}, nil
}
if strings.HasPrefix(path, "hu/") {
return &Request{
LocalPart: r.FormValue("l"),
Domain: requestHost(r),
Hash: strings.TrimPrefix(path, "hu/"),
}, nil
}
return nil, errInvalidPath
}
// Key data retrieved from storage.
type Key struct {
Addr string
Data []byte
}
// Storage interface.
type Storage interface {
// Lookup a hash in the user database, returning key and user
// information. The special ErrNotFound error can be used to
// indicate that no key was found, as opposed to a generic
// backend error.
Lookup(context.Context, string) (*Key, error)
}
// Server for the WKD protocol.
type Server struct {
storage Storage
schema SchemaFunc
}
func NewServer(storage Storage, schema SchemaFunc) *Server {
return &Server{
storage: storage,
schema: schema,
}
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !strings.HasPrefix(r.URL.Path, WELLKNOWN) {
http.NotFound(w, r)
return
}
req, err := s.schema(r)
if err != nil {
http.NotFound(w, r)
return
}
if req.IsPolicy {
s.servePolicy(w, r)
return
}
s.serveDiscovery(w, r, req)
}
func (s *Server) servePolicy(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Cache-Control", "max-age=604800")
fmt.Fprintf(w, "protocol-version: %d\n", VERSION)
}
func (s *Server) serveDiscovery(w http.ResponseWriter, r *http.Request, request *Request) {
key, err := s.storage.Lookup(r.Context(), request.Hash)
if err == ErrNotFound {
http.NotFound(w, r)
return
} else if err != nil {
log.Printf("lookup error (%s): %v", request.Hash, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Check that the requested address matches the hash.
if reqAddr := request.Addr(); reqAddr != key.Addr {
log.Printf("unauthorized lookup for %s (requested %s, actually %s)", request.Hash, reqAddr, key.Addr)
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", strconv.Itoa(len(key.Data)))
w.Header().Set("Cache-Control", "max-age=3600")
if _, err := w.Write(key.Data); err != nil {
log.Printf("error writing response to %s: %v", r.RemoteAddr, err)
}
}
func requestHost(r *http.Request) string {
if s := r.Header.Get("X-Forwarded-Host"); s != "" {
return s
}
return r.Host
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment