Small changes

The string used for selecting the transaction type should
be copied because it is passed by reference.

Augment HMAC to validate entire decryption request

All the valued fields need to be hashed for incoming
encrypted file.  This is to keep the integrity of the
request.

Add static test case for core

Test the output of a pre-computed encrypted blob with associated
vault.

Support hosting static file under /index

Require client auth only when server CA present

Add tests for cryptor.go

Improve comments.
This commit is contained in:
Nick Sullivan
2013-11-20 13:31:40 -08:00
parent a0516a473a
commit d7d64a0c6b
7 changed files with 272 additions and 24 deletions

View File

@@ -11,7 +11,7 @@ usage:
The Red October server is a TLS server. It requires a local file to hold the key vault, an internet address and a certificate keypair.
i.e.
redoctober -addr=localhost:8080 -vaultpath=/tmp/diskrecord.json -cert=certs/servercertsigned.pem -key=certs/serverkey.pem
redoctober -addr=localhost:8080 -vaultpath=/tmp/diskrecord.json -cert=certs/servercertsigned.pem -key=certs/serverkey.pem -static=index.html
## Using
@@ -25,6 +25,8 @@ The server exposes several JSON API endpoints. JSON of the prescribed format is
- Encrypt = "/encrypt"
- Decrypt = "/decrypt"
Optionally, the server can host a static HTML file to serve from "/index".
### Create
Create is the necessary first call to a new red october vault. It creates an admin account.

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,8 @@ import (
"crypto/rand"
"crypto/sha1"
"encoding/json"
"strconv"
"sort"
"errors"
"redoctober/keycache"
"redoctober/padding"
@@ -37,9 +39,9 @@ type SingleWrappedKey struct {
aesKey []byte
}
// EncryptedFile is the format for encrypted data containing all the
// EncryptedData is the format for encrypted data containing all the
// keys necessary to decrypt it when delegated.
type EncryptedFile struct {
type EncryptedData struct {
Version int
VaultId int
KeySet []MultiWrappedKey
@@ -151,6 +153,105 @@ func unwrapKey(keys []MultiWrappedKey, rsaKeys map[string]SingleWrappedKey) (unw
return
}
// mwkSorter describes a slice of MultiWrappedKeys to be sorted.
type mwkSorter struct {
keySet []MultiWrappedKey
}
// Len is part of sort.Interface.
func (s *mwkSorter) Len() int {
return len(s.keySet)
}
// Swap is part of sort.Interface.
func (s *mwkSorter) Swap(i, j int) {
s.keySet[i], s.keySet[j] = s.keySet[j], s.keySet[i]
}
// Less is part of sort.Interface, it sorts lexicographically
// based on the list of names
func (s *mwkSorter) Less(i, j int) bool {
var shorter = i
if len(s.keySet[i].Name) > len(s.keySet[j].Name) {
shorter = j
}
for index := range s.keySet[shorter].Name {
if s.keySet[i].Name[index] != s.keySet[j].Name[index] {
return s.keySet[i].Name[index] < s.keySet[j].Name[index]
}
}
return false
}
// swkSorter joins a slice of names with SingleWrappedKeys to be sorted.
type pair struct {
name string
key []byte
}
type swkSorter []pair
// Len is part of sort.Interface.
func (s swkSorter) Len() int {
return len(s)
}
// Swap is part of sort.Interface.
func (s swkSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less is part of sort.Interface.
func (s swkSorter) Less(i, j int) bool {
return s[i].name < s[j].name
}
// computeHmac computes the signature of the encrypted data structure
// the signature takes into account every element of the EncryptedData
// structure, with all keys sorted alphabetically by name
func computeHmac(key []byte, encrypted EncryptedData) []byte {
mac := hmac.New(sha1.New, key)
// sort the multi-wrapped keys
mwks := &mwkSorter{
keySet: encrypted.KeySet,
}
sort.Sort(mwks)
// sort the singly-wrapped keys
var swks swkSorter
for name, val := range encrypted.KeySetRSA {
swks = append(swks, pair{name, val.Key})
}
sort.Sort(&swks)
// start hashing
mac.Write([]byte(strconv.Itoa(encrypted.Version)))
mac.Write([]byte(strconv.Itoa(encrypted.VaultId)))
// hash the multi-wrapped keys
for _, mwk := range encrypted.KeySet {
for _, name := range mwk.Name {
mac.Write([]byte(name))
}
mac.Write(mwk.Key)
}
// hash the single-wrapped keys
for index, _ := range swks {
mac.Write([]byte(swks[index].name))
mac.Write(swks[index].key)
}
// hash the IV and data
mac.Write(encrypted.IV)
mac.Write(encrypted.Data)
return mac.Sum(nil)
}
// Encrypt encrypts data with the keys associated with names. This
// requires a minimum of min keys to decrypt. NOTE: as currently
// implemented, the maximum value for min is 2.
@@ -159,7 +260,7 @@ func Encrypt(in []byte, names []string, min int) (resp []byte, err error) {
return nil, errors.New("Minimum restricted to 2")
}
var encrypted EncryptedFile
var encrypted EncryptedData
encrypted.Version = DEFAULT_VERSION
if encrypted.VaultId, err = passvault.GetVaultId(); err != nil {
return
@@ -241,9 +342,7 @@ func Encrypt(in []byte, names []string, min int) (resp []byte, err error) {
if err != nil {
return
}
mac := hmac.New(sha1.New, hmacKey)
mac.Write(encrypted.Data)
encrypted.Signature = mac.Sum(nil)
encrypted.Signature = computeHmac(hmacKey, encrypted)
return json.Marshal(encrypted)
}
@@ -251,7 +350,7 @@ func Encrypt(in []byte, names []string, min int) (resp []byte, err error) {
// Decrypt decrypts a file using the keys in the key cache.
func Decrypt(in []byte) (resp []byte, err error) {
// unwrap encrypted file
var encrypted EncryptedFile
var encrypted EncryptedData
if err = json.Unmarshal(in, &encrypted); err != nil {
return
}
@@ -281,9 +380,7 @@ func Decrypt(in []byte) (resp []byte, err error) {
if err != nil {
return
}
mac := hmac.New(sha1.New, hmacKey)
mac.Write(encrypted.Data)
expectedMAC := mac.Sum(nil)
expectedMAC := computeHmac(hmacKey, encrypted)
if !hmac.Equal(encrypted.Signature, expectedMAC) {
err = errors.New("Signature mismatch")
return
@@ -307,3 +404,4 @@ func Decrypt(in []byte) (resp []byte, err error) {
return padding.RemovePadding(clearData)
}

View File

@@ -0,0 +1,57 @@
// cryptor_test.go: tests for core.go
//
// Copyright (c) 2013 CloudFlare, Inc.
package cryptor
import (
"encoding/base64"
"encoding/json"
"bytes"
"testing"
)
func TestHash(t *testing.T) {
decryptJson := []byte("{\"Version\":1,\"VaultId\":5298538957754540810,\"KeySet\":[{\"Name\":[\"Bob\",\"Alice\"],\"Key\":\"2j3tI2PBFBbwFX0BlQdUuA==\"},{\"Name\":[\"Bob\",\"Carol\"],\"Key\":\"yLSSB/U6+5rc1E+gjGXT4w==\"},{\"Name\":[\"Alice\",\"Bob\"],\"Key\":\"DDlWHF7szzISuXWaEz8llQ==\"},{\"Name\":[\"Alice\",\"Carol\"],\"Key\":\"TkA13aPrYFNbveIbl0qdww==\"},{\"Name\":[\"Carol\",\"Bob\"],\"Key\":\"KXm0uObmRJ2ZvSYEWPwk2A==\"},{\"Name\":[\"Carol\",\"Alice\"],\"Key\":\"L9c+PqtxPh9y6apRvtbCQw==\"}],\"KeySetRSA\":{\"Alice\":{\"Key\":\"fj5mqnq7y5KCafCGT1I51xI5JsX746+9TTSsp/8ybf3iZjhFzSlwP3aNmsOx3SUKTmZlfs+b+MeD4eKJ2uKBFzQHAIPO0fwoiCDKHhKH6KsolNq4+jgpkLAMOLsQGs8g6BhJy6bCFRjZVc3IdlQABPM6PkTbuSvKhn9atDFwZQD5TJBISi7d2hw4LradtLITbNqwiFMTQQ9+psXzyavY8H3LNHKGgf5Od7IpthEQPCHi4nw7X/YVRTEMfoIVcMcKOwYjlC45/VJEHK9Zy3DSiLBzjmr57YNIVjw8YZY5DGBWqbgu51RUbIcrqyLphBhXoBRu4R+yrhygBNWbvkkifA==\"},\"Bob\":{\"Key\":\"HTYiZ18sf721cAN1LRNkJ/+L4AKWilMrkMyNiBjWcl9HRTVPNXITqQBXd0fBggGNPiZr6VQTySK4ZFvJKGDGiz17Te/ToDn8Yk/B9cqMsN5fHoQtXvl8IZo2wioA67ccAJ1gHMMNpPyLdF43SQqgI+XaQ2lMSYLMfxxDmBBOQ1SWAto0BDRdnsqpwUwIPKQ9Y3/1osmrjLmJoAC3MPplexYWhexNwJtSd+mFdVZ3Qe4x9RsRHcN/myihOt/67V60qzs13F0RZkMSDzj5Ddg+1KVNJZY9dmolPNkAZj8z20L9uzpatrTYTR6A8q/sRn+inO7ZQVQ00XO6q6lYYQzxnw==\"},\"Carol\":{\"Key\":\"ItrvS02nSfbcA2fl1L1i61xqPEDKRdsrYe3+UCbkT+ipheiQRPSuikbzeV2kshn4yJDeku5bmTNqW8HSGtU7GTgCoIWV8WmEf4w6ovzShPbu+VrIZvRz3wjh2oYHT/gtPVAQnBa/71FeoBNxy5l/hBcUmBky43j83Mlt2+8QZx6PEUDmpaPQemVh99+C20nQtkAUFeMc2Ge4y7RlHSxtfABvwlXx1NzCD40nyJfF1SjV/fZh/E2Al4Tavx6DOJkYGoJ2mp7XBvX0IF2tp8T3U5VpnTek/WuNrLL9z7/jqzWh87lZ5KheWXhGkU1BNH4lfIj43pDkSy50aDvS0zYfHQ==\"}},\"IV\":\"58r9Mz8e06mItBG9nSV/0Q==\",\"Data\":\"QE9ZhcGXNXauUdMk04biUGy1SoP5H2nF/j2JjiiVFKPdIdRp/Gc+AZvUI9n22ZM4q+zDiJz7qvK4bKaPpXhTmGP0XheaFUukeVNS9STMoTbNcY/ZtVOz6hizUPF7gSq388QPUsT+Axml3rEUTWOhnw==\",\"Signature\":\"VViRoaCxqKAvNxyMxDhrtYo0CZA=\"}")
var encrypted EncryptedData
if err := json.Unmarshal(decryptJson, &encrypted); err != nil {
t.Fatalf("Error unmarshalling json,", err)
}
var hmacKey, _ = base64.StdEncoding.DecodeString("Qugc5ZQ0vC7KQSgmDHTVgQ==")
var signature = append([]byte{}, encrypted.Signature...)
expectedSig := computeHmac(hmacKey, encrypted)
if diff := bytes.Compare(signature, expectedSig); diff != 0 {
t.Fatalf("Error comparing signature", base64.StdEncoding.EncodeToString(expectedSig))
}
// change version and check hmac
encrypted.Version = 2
unexpectedSig := computeHmac(hmacKey, encrypted)
if diff := bytes.Compare(signature, unexpectedSig); diff == 0 {
t.Fatalf("Error comparing signature")
}
encrypted.Version = 1
// swap two records and check hmac
encrypted.KeySet[0], encrypted.KeySet[1] = encrypted.KeySet[1], encrypted.KeySet[0]
unexpectedSig = computeHmac(hmacKey, encrypted)
if diff := bytes.Compare(signature, unexpectedSig); diff != 0 {
t.Fatalf("Error comparing signature", base64.StdEncoding.EncodeToString(expectedSig))
}
// delete RSA key and check hmac
encrypted.Version = 1
delete(encrypted.KeySetRSA, "Carol")
unexpectedSig = computeHmac(hmacKey, encrypted)
if diff := bytes.Compare(signature, unexpectedSig); diff == 0 {
t.Fatalf("Error comparing signature")
}
}

View File

@@ -19,7 +19,7 @@ import (
// UserKeys is the set of decrypted keys in memory, indexed by name.
var UserKeys map[string]ActiveUser = make(map[string]ActiveUser)
// ActiveUser holds the information about an actively delegated key
// ActiveUser holds the information about an actively delegated key.
type ActiveUser struct {
Admin bool
Type string
@@ -30,16 +30,20 @@ type ActiveUser struct {
rsaKey rsa.PrivateKey
}
// matchUser returns the matching active user if present
// and a boolean to indicate its presence.
func matchUser(name string) (out ActiveUser, present bool) {
out, present = UserKeys[name]
return
}
// setUser takes an ActiveUser and adds it to the cache.
func setUser(in ActiveUser, name string) {
UserKeys[name] = in
}
// mark a use of the key, only for decryption or symmetric encryption
// useKey decrements the counter on an active key
// for decryption or symmetric encryption
func useKey(name string) {
if val, present := matchUser(name); present {
val.Uses -= 1
@@ -63,7 +67,7 @@ func FlushCache() {
func Refresh() {
for name, active := range UserKeys {
if active.Expiry.Before(time.Now()) || active.Uses <= 0 {
log.Println("Record expired", name)
log.Println("Record expired", name, active.Expiry)
delete(UserKeys, name)
}
}

View File

@@ -105,6 +105,8 @@ func makeRandom(length int) ([]byte, error) {
return bytes, err
}
// encryptRSARecord takes an RSA private key and encrypts it with
// a password key
func encryptRSARecord(newRec *PasswordRecord, rsaPriv *rsa.PrivateKey, passKey []byte) (err error) {
if newRec.RSAKey.RSAExpIV, err = makeRandom(16); err != nil {
return
@@ -133,7 +135,7 @@ func encryptRSARecord(newRec *PasswordRecord, rsaPriv *rsa.PrivateKey, passKey [
return
}
// Create new record from username and password
// createPasswordRec creates a new record from a username and password
func createPasswordRec(password string, admin bool) (newRec PasswordRecord, err error) {
newRec.Type = RSARecord
@@ -374,6 +376,7 @@ func ChangePassword(name, password, newPassword string) (err error) {
return
}
// add the password salt and hash
if pr.PasswordSalt, err = makeRandom(16); err != nil {
return
}

View File

@@ -70,7 +70,7 @@ func queueRequest(process chan userRequest, requestType string, w http.ResponseW
//
// Returns a valid http.Server handling redoctober JSON requests (and
// its associated listener) or an error
func NewServer(process chan userRequest, addr string, certPath, keyPath, caPath string) (*http.Server, *net.Listener, error) {
func NewServer(process chan userRequest, staticPath, addr, certPath, keyPath, caPath string) (*http.Server, *net.Listener, error) {
mux := http.NewServeMux()
srv := http.Server{
Addr: addr,
@@ -82,9 +82,8 @@ func NewServer(process chan userRequest, addr string, certPath, keyPath, caPath
}
config := tls.Config{
Certificates: []tls.Certificate{cert},
Rand: rand.Reader,
ClientAuth: tls.RequestClientCert,
Certificates: []tls.Certificate{cert},
Rand: rand.Reader,
PreferServerCipherSuites: true,
SessionTicketsDisabled: true,
}
@@ -108,6 +107,7 @@ func NewServer(process chan userRequest, addr string, certPath, keyPath, caPath
}
rootPool.AddCert(cert)
config.ClientAuth = tls.RequireAndVerifyClientCert
config.ClientCAs = rootPool
}
@@ -118,18 +118,28 @@ func NewServer(process chan userRequest, addr string, certPath, keyPath, caPath
lstnr := tls.NewListener(conn, &config)
for requestType := range functions {
// queue up post URIs
for current := range functions {
// copy this so reference does not get overwritten
var requestType = current
mux.HandleFunc(requestType, func(w http.ResponseWriter, r *http.Request) {
queueRequest(process, requestType, w, r)
})
}
// queue up web frontend
if staticPath != "" {
mux.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, staticPath)
})
}
return &srv, &lstnr, nil
}
const usage = `Usage:
redoctober -vaultpath <path> -addr <addr> -cert <path> -key <path> [-ca <path>]
redoctober -static <path> -vaultpath <path> -addr <addr> -cert <path> -key <path> [-ca <path>]
example:
redoctober -vaultpath /tmp/diskrecord.json -addr localhost:8080 -cert cert.pem -key cert.key
@@ -142,6 +152,7 @@ func main() {
os.Exit(2)
}
var staticPath = flag.String("staticpath", "/tmp/index.html", "Path to the the static entry")
var vaultPath = flag.String("vaultpath", "/tmp/tmpvault", "Path to the the disk vault")
var addr = flag.String("addr", "localhost:8000", "Server and port separated by :")
var certPath = flag.String("cert", "", "Path of TLS certificate in PEM format")
@@ -189,7 +200,7 @@ func main() {
}
}()
s, l, err := NewServer(process, *addr, *certPath, *keyPath, *caPath)
s, l, err := NewServer(process, *staticPath, *addr, *certPath, *keyPath, *caPath)
if err == nil {
s.Serve(*l)
} else {