mirror of
https://github.com/cloudflare/redoctober.git
synced 2026-01-08 15:21:50 +00:00
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:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
57
src/redoctober/cryptor/cryptor_test.go
Normal file
57
src/redoctober/cryptor/cryptor_test.go
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user