/*
pollen: Entropy-as-a-Server web server
Copyright (C) 2012-2013 Dustin Kirkland <dustin.kirkland@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"crypto/sha512"
"crypto/tls"
"flag"
"fmt"
"io"
"io/ioutil"
"log/syslog"
"net/http"
"os"
"strings"
"sync"
"time"
)
var (
httpPort = flag.String("http-port", "80", "The HTTP port on which to listen")
httpsPort = flag.String("https-port", "443", "The HTTPS port on which to listen")
device = flag.String("device", "/dev/random", "The device to use for reading and writing random data")
size = flag.Int("bytes", 64, "The size in bytes to read from the random device")
cert = flag.String("cert", "/etc/pollen/cert.pem", "The full path to cert.pem")
key = flag.String("key", "/etc/pollen/key.pem", "The full path to key.pem")
)
// this matches the syslog.Writer functions
type logger interface {
Close() error
Info(string) error
Err(string) error
Crit(string) error
Emerg(string) error
}
type PollenServer struct {
// randomSource is usually /dev/random or /dev/urandom
randomSource io.ReadWriter
log logger
readSize int
}
const usePollinateError = "Please use the pollinate client. 'sudo apt-get install pollinate' or download from: https://bazaar.launchpad.net/~pollinate/pollinate/trunk/view/head:/pollinate"
func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
var avail []byte
challenge := r.FormValue("challenge")
if challenge == "" {
http.Error(w, usePollinateError, http.StatusBadRequest)
return
}
checksum := sha512.New()
io.WriteString(checksum, challenge)
challengeResponse := checksum.Sum(nil)
var err error
_, err = p.randomSource.Write(challengeResponse)
if err != nil {
/* Non-fatal error, but let's log this to syslog */
p.log.Err(fmt.Sprintf("Cannot write to random device at [%v]", time.Now().UnixNano()))
}
/* Record entropy bits before */
avail, err = ioutil.ReadFile("/proc/sys/kernel/random/entropy_avail")
if err != nil {
/* Non-fatal error */
p.log.Err(fmt.Sprintf("Cannot record entropy bits at [%v]", time.Now().UnixNano()))
avail = []byte{'?'}
}
p.log.Info(fmt.Sprintf("Server received challenge from [%s, %s] at [%v] with [e%s] available", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano(), strings.Split(string(avail), "\n")[0]))
data := make([]byte, p.readSize)
_, err = io.ReadFull(p.randomSource, data)
if err != nil {
/* Fatal error for this connection, if we can't read from device */
p.log.Err(fmt.Sprintf("Cannot read from random device at [%v]", time.Now().UnixNano()))
http.Error(w, "Failed to read from random device", http.StatusInternalServerError)
return
}
checksum.Write(data)
/* The checksum of the bytes from /dev/random is simply for print-ability, when debugging */
seed := checksum.Sum(nil)
fmt.Fprintf(w, "%x\n%x\n", challengeResponse, seed)
/* Record entropy bits after */
avail, err = ioutil.ReadFile("/proc/sys/kernel/random/entropy_avail")
if err != nil {
/* Non-fatal error */
p.log.Err(fmt.Sprintf("Cannot record entropy bits at [%v]", time.Now().UnixNano()))
avail = []byte{'?'}
}
p.log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v] in [%.6fs] with [e%s] available",
r.RemoteAddr, r.UserAgent(), time.Now().UnixNano(), time.Since(startTime).Seconds(), strings.Split(string(avail), "\n")[0]))
}
func main() {
flag.Parse()
if *httpPort == "" && *httpsPort == "" {
fatal("Nothing to do if http and https are both disabled")
}
log, err := syslog.New(syslog.LOG_ERR, "pollen")
if err != nil {
fatalf("Cannot open syslog: %s\n", err)
}
defer log.Close()
log.Info(fmt.Sprintf("pollen starting at [%v]", time.Now().UnixNano()))
dev, err := os.OpenFile(*device, os.O_RDWR, 0)
if err != nil {
fatalf("Cannot open device: %s\n", err)
}
defer dev.Close()
handler := &PollenServer{randomSource: dev, log: log, readSize: *size}
http.Handle("/", handler)
var httpListeners sync.WaitGroup
if *httpPort != "" {
httpAddr := fmt.Sprintf(":%s", *httpPort)
httpListeners.Add(1)
go func() {
handler.fatal(http.ListenAndServe(httpAddr, nil))
httpListeners.Done()
}()
}
if *httpsPort != "" {
httpsAddr := fmt.Sprintf(":%s", *httpsPort)
httpListeners.Add(1)
go func() {
config := &tls.Config{MinVersion: tls.VersionTLS10}
server := &http.Server{Addr: httpsAddr, Handler: handler, TLSConfig: config}
handler.fatal(server.ListenAndServeTLS(*cert, *key))
httpListeners.Done()
}()
}
httpListeners.Wait()
}
func (p *PollenServer) fatal(args ...interface{}) {
p.log.Crit(fmt.Sprint(args...))
fatal(args...)
}
func (p *PollenServer) fatalf(format string, args ...interface{}) {
p.log.Emerg(fmt.Sprintf(format, args...))
fatalf(format, args...)
}
func fatal(args ...interface{}) {
args = append(args, "\n")
fmt.Fprint(os.Stderr, args...)
os.Exit(1)
}
func fatalf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
os.Exit(1)
}