blob: 9200f38fb07b8937dacecfd79330c87e76aae4ce [file] [log] [blame]
// Copyright 2016 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package signup provides an http.Handler implementation that serves and
// validates KeyServer signup requests.
package signup
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"upspin.io/cloud/mail"
"upspin.io/config"
"upspin.io/errors"
"upspin.io/factotum"
"upspin.io/log"
"upspin.io/rpc"
"upspin.io/serverutil"
"upspin.io/upspin"
"upspin.io/user"
"upspin.io/valid"
)
const (
// signupGracePeriod is the period of validity for a signup request.
signupGracePeriod = 24 * time.Hour
// signupNotifyAddress is the address that should receive signup notifications.
signupNotifyAddress = "upspin-sendgrid@google.com"
// fromAddress is the origin address for signup messages.
fromAddress = "noreply@upspin.io"
noHTML = "" // for mail.Send
)
// handler implements an http.Handler that handles user creation requests
// made by 'upspin signup' and the user themselves.
type handler struct {
baseURL string
fact upspin.Factotum
key upspin.KeyServer
mail mail.Mail
project string
rate serverutil.RateLimiter
}
// NewHandler creates a new handler that serves signup requests (made by
// 'upspin signup') and verification requests (visited by clicking the link in
// the email).
// The Factotum is used to sign the verification URL. The KeyServer is where
// the new user will be created. The Mail is used to send mail. The provided
// project name is used in the subject line of signup notifications, to
// distinguish test and production keyserver instances.
func NewHandler(baseURL string, fact upspin.Factotum, key upspin.KeyServer, m mail.Mail, project string) http.Handler {
return &handler{
baseURL: baseURL,
fact: fact,
key: key,
mail: m,
project: project,
rate: serverutil.RateLimiter{
Backoff: 1 * time.Minute,
Max: 24 * time.Hour,
},
}
}
func (m *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
errorf := func(code int, format string, args ...interface{}) {
s := fmt.Sprintf(format, args...)
http.Error(w, s, code)
}
// Parse and validate request.
v := r.FormValue
name, dir, store, pkey, sigR, sigS := v("name"), v("dir"), v("store"), v("key"), v("sigR"), v("sigS")
u := &upspin.User{
Name: upspin.UserName(name),
PublicKey: upspin.PublicKey(pkey),
}
if dir != "" {
u.Dirs = []upspin.Endpoint{{
Transport: upspin.Remote,
NetAddr: upspin.NetAddr(dir),
}}
}
if store != "" {
u.Stores = []upspin.Endpoint{{
Transport: upspin.Remote,
NetAddr: upspin.NetAddr(store),
}}
}
if err := valid.UserName(u.Name); err != nil {
errorf(http.StatusBadRequest, "invalid user name: %s", u.Name)
return
}
_, suffix, _, _ := user.Parse(u.Name) // valid already checked the error.
if suffix != "" {
errorf(http.StatusBadRequest, "user name must not be suffixed: %s", u.Name)
return
}
// Lookup userName. It must not exist yet.
_, err := m.key.Lookup(u.Name)
if err == nil {
errorf(http.StatusBadRequest, "user already exists on key server: %s", u.Name)
return
} else if !errors.Is(errors.NotExist, err) {
errorf(http.StatusInternalServerError, "error looking up user: %v", err)
return
}
nowS := v("now")
create := nowS != ""
if create {
// This is the user clicking the link in the signup mail.
// Validate the server signature and create the user.
// Parse signature.
var rs, ss big.Int
if _, ok := rs.SetString(sigR, 10); !ok {
errorf(http.StatusBadRequest, "invalid signature R value")
return
}
if _, ok := ss.SetString(sigS, 10); !ok {
errorf(http.StatusBadRequest, "invalid signature S value")
return
}
sig := upspin.Signature{R: &rs, S: &ss}
// Parse time.
nowI, err := strconv.ParseInt(nowS, 10, 64)
if err != nil {
errorf(http.StatusBadRequest, "invalid now value: %v", err)
return
}
now := time.Unix(nowI, 0)
// Validate signature.
if err := m.validateSignature(u, now, sig); err != nil {
errorf(http.StatusBadRequest, "invalid signature: %v", err)
return
}
// Create user.
err = m.createUser(u)
if err != nil {
errorf(http.StatusInternalServerError, "could not create user: %v", err)
return
}
// Send a note to our internal list, so we're aware of signups.
subject := fmt.Sprintf("New signup on %s: %s", m.project, string(u.Name))
body := fmt.Sprintf("%s signed up on %s on %s", u.Name, m.project, time.Now().Format(time.Stamp))
err = m.mail.Send(signupNotifyAddress, fromAddress, subject, body, noHTML)
if err != nil {
log.Error.Printf("Error sending mail to %q: %v", signupNotifyAddress, err)
// Don't prevent signup if this fails.
}
// TODO(adg): display user friendly welcome message
fmt.Fprintf(w, "An account for %q has been registered with the key server.", u.Name)
return
}
// We are being called by 'upspin signup'.
if err := verifySignupSignature(name, dir, store, pkey, sigR, sigS); err != nil {
errorf(http.StatusBadRequest, "invalid request: %s", err)
return
}
if r.Method != "POST" {
errorf(http.StatusMethodNotAllowed, "method not allowed")
return
}
// Aggressively rate limit requests to this service,
// so that we can't be used for a mail bomb.
// TODO(adg): also limit by remote IP address
name, _, domain, err := user.Parse(u.Name)
if err != nil {
errorf(http.StatusBadRequest, "invalid user name: %v", err)
return
}
key := strings.ToLower(name + "@" + domain)
if ok, wait := m.rate.Pass(key); !ok {
errorf(http.StatusTooManyRequests, "repeated signup attempt; please wait %v before trying again", wait)
return
}
// Construct signed sign-up URL.
// Important: the signaure must only be transmitted to the calling user
// by email, as it is proof of ownership of that email address. We must
// take care not to expose the signature in response to this request
// (in an error message, for example).
now := time.Now()
sig, err := m.sign(u, now)
if err != nil {
errorf(http.StatusInternalServerError, "could not generate signature: %v", err)
return
}
dir, store = "", ""
if len(u.Dirs) == 1 {
dir = string(u.Dirs[0].NetAddr)
}
if len(u.Stores) == 1 {
store = string(u.Stores[0].NetAddr)
}
vals := url.Values{
"name": {string(u.Name)},
"dir": {dir},
"store": {store},
"key": {string(u.PublicKey)},
"sigR": {sig.R.String()},
"sigS": {sig.S.String()},
"now": {fmt.Sprint(now.Unix())},
}
signupURL := m.baseURL + "?" + vals.Encode()
// Send signup confirmation mail to user.
body := new(bytes.Buffer)
fmt.Fprintln(body, "Follow this link to complete the Upspin signup process:")
fmt.Fprintln(body, signupURL)
fmt.Fprintln(body, "\nIf you were not expecting this message, please ignore it.")
// TODO(adg): implement opt out link
const subject = "Upspin signup confirmation"
err = m.mail.Send(string(u.Name), fromAddress, subject, body.String(), noHTML)
if err != nil {
log.Error.Printf("Error sending mail to %q: %v", u.Name, err)
errorf(http.StatusInternalServerError, "could not send signup email")
return
}
fmt.Fprintln(w, "OK")
}
// verifySignupSignature verifies that the new user record comprised of name,
// dir, store and key were properly signed by the new user using the private key
// that corresponds to the public key provided.
func verifySignupSignature(user, dir, store, key, sigR, sigS string) error {
var rs, ss big.Int
if _, ok := rs.SetString(sigR, 10); !ok {
return errors.Str("invalid signature R value")
}
if _, ok := ss.SetString(sigS, 10); !ok {
return errors.Str("invalid signature S value")
}
sig := upspin.Signature{R: &rs, S: &ss}
hash, _ := RequestHash(upspin.UserName(user), upspin.NetAddr(dir), upspin.NetAddr(store), upspin.PublicKey(key))
return factotum.Verify(hash, sig, upspin.PublicKey(key))
}
func (m *handler) createUser(u *upspin.User) error {
key, err := m.dialForUser(u.Name)
if err != nil {
return err
}
defer key.Close()
if err := key.Put(u); err != nil {
return err
}
snapshotUser, err := snapshotUser(u.Name)
if err != nil {
return err
}
// Lookup snapshotUser to ensure we don't overwrite an existing one.
_, err = key.Lookup(snapshotUser)
if err != nil && !errors.Is(errors.NotExist, err) {
return err
}
if err == nil {
// Snapshot user exists; no need to create it.
return nil
}
// Create snapshot user.
key, err = m.dialForUser(snapshotUser)
if err != nil {
return err
}
defer key.Close() // be nice and release resources.
return key.Put(&upspin.User{
Name: snapshotUser,
PublicKey: u.PublicKey,
})
}
func (m *handler) dialForUser(name upspin.UserName) (upspin.KeyServer, error) {
// We need to dial this server locally so the new user is authenticated
// with it implicitly.
cfg := config.New()
cfg = config.SetKeyEndpoint(cfg, m.key.Endpoint())
cfg = config.SetUserName(cfg, name)
service, err := m.key.Dial(cfg, m.key.Endpoint())
if err != nil {
return nil, err
}
keyServer, ok := service.(upspin.KeyServer)
if !ok {
return nil, errors.E(errors.Internal, errors.Str("dialed service not an instance of upspin.KeyServer"))
}
return keyServer, nil
}
// sign generates a signature for the given user creation request at time now.
func (m *handler) sign(u *upspin.User, now time.Time) (upspin.Signature, error) {
b, err := sigBytes(u, now)
if err != nil {
return upspin.Signature{}, err
}
return m.fact.Sign(b)
}
func (m *handler) validateSignature(u *upspin.User, now time.Time, sig upspin.Signature) error {
// Check that the signature is still valid.
if time.Now().After(now.Add(signupGracePeriod)) {
return errors.Str("request too old; please try again")
}
b, err := sigBytes(u, now)
if err != nil {
return err
}
return factotum.Verify(b, sig, m.fact.PublicKey())
}
func sigBytes(u *upspin.User, now time.Time) ([]byte, error) {
b, err := json.Marshal(u)
if err != nil {
return nil, err
}
b = strconv.AppendInt(b, now.Unix(), 10)
h := sha256.Sum256(b)
return h[:], nil
}
// snapshotUser returns the snapshot username for the named user.
func snapshotUser(u upspin.UserName) (upspin.UserName, error) {
// Attempt to create a "+snapshot" user.
name, suffix, domain, err := user.Parse(u)
if err != nil {
return "", err
}
if suffix != "" {
name = name[:len(name)-len(suffix)-1]
}
return upspin.UserName(name + "+snapshot@" + domain), nil
}
var signupURLScheme = "https" // Tests may override this.
// MakeRequest sends a signup request for the given Config to the Config's
// KeyServer Endpoint using the Config's TLS certs (if any).
func MakeRequest(cfg upspin.Config) error {
query, err := makeQueryString(cfg)
if err != nil {
return err
}
certPool, err := rpc.CertPoolFromConfig(cfg)
if err != nil {
return err
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
},
}
signupURL := fmt.Sprintf("%s://%s/signup", signupURLScheme, cfg.KeyEndpoint().NetAddr)
r, err := client.Post(signupURL+"?"+query, "text/plain", nil)
if err != nil {
return err
}
b, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
return err
}
if r.StatusCode != http.StatusOK {
return fmt.Errorf("key server error: %s", b)
}
return nil
}
// makeQueryString returns an encoded query string used to sign up a new user
// with the KeyServer.
func makeQueryString(cfg upspin.Config) (string, error) {
f := cfg.Factotum()
if f == nil {
return "", errors.Str("cannot sign up without Factotum")
}
hash, vals := RequestHash(cfg.UserName(), cfg.DirEndpoint().NetAddr, cfg.StoreEndpoint().NetAddr, cfg.Factotum().PublicKey())
sig, err := cfg.Factotum().Sign(hash)
if err != nil {
return "", err
}
vals.Add("sigR", sig.R.String())
vals.Add("sigS", sig.S.String())
return vals.Encode(), nil
}
// RequestHash generates a hash of the supplied arguments that, when signed, is
// used to prove that a signup request originated from the user that owns the
// supplied private key.
func RequestHash(name upspin.UserName, dir, store upspin.NetAddr, key upspin.PublicKey) ([]byte, url.Values) {
const magic = "signup-request"
u := url.Values{}
h := sha256.New()
h.Write([]byte(magic))
w := func(key, val string) {
var l [4]byte
binary.BigEndian.PutUint32(l[:], uint32(len(val)))
h.Write(l[:])
h.Write([]byte(val))
u.Add(key, val)
}
w("name", string(name))
w("dir", string(dir))
w("store", string(store))
w("key", string(key))
return h.Sum(nil), u
}