Skip to content

Commit

Permalink
- Implement integration between internal and external KMIP service wi…
Browse files Browse the repository at this point in the history
…th the RPC protocol.

- Introduce optional strong client identity verification via TLS.
- Hash password before transit.
- Fix an issue that previously allowed a malicious administrator to craft RPC request to overwrite files outside of key database.
  • Loading branch information
HouzuoGuo committed May 11, 2017
1 parent cd9e060 commit 726a77c
Show file tree
Hide file tree
Showing 25 changed files with 867 additions and 353 deletions.
30 changes: 24 additions & 6 deletions command/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const (
MSG_ASK_HOSTNAME = "Key server's host name"
MSG_ASK_PORT = "Key server's port number"
MSG_ASK_CA = "(Optional) PEM-encoded CA certificate of key server"
MSG_ASK_CLIENT_CERT = "If key server will validate client identity, enter path to PEM-encoded client certificate"
MSG_ASK_CLIENT_CERT_KEY = "If key server will validate client identity, enter path to PEM-encoded client key"
MSG_ASK_DIFF_HOST = `Previously, this computer used "%s" as its key server; now you wish to use "%s".
Only a single key server can be used to unlock all encrypted disks on this computer.
Do you wish to proceed and switch to the new key server?`
Expand Down Expand Up @@ -59,7 +61,7 @@ The encryption sequence will carry out the following tasks:
)

// Prompt user to enter key server's CA file, host name, and port. Defaults are provided by existing configuration.
func PromptForKeyServer() (sysconf *sys.Sysconfig, caFile, host string, port int, err error) {
func PromptForKeyServer() (sysconf *sys.Sysconfig, caFile, certFile, certKeyFile, host string, port int, err error) {
sysconf, err = sys.ParseSysconfigFile(CLIENT_CONFIG_PATH, true)
if err != nil {
return
Expand All @@ -76,6 +78,16 @@ func PromptForKeyServer() (sysconf *sys.Sysconfig, caFile, host string, port int
if caFile = sys.InputAbsFilePath(false, defaultCAFile, MSG_ASK_CA); caFile == "" {
caFile = defaultCAFile
}
defaultCertFile := sysconf.GetString(keyserv.CLIENT_CONF_CERT, "")
if certFile = sys.InputAbsFilePath(false, defaultCertFile, MSG_ASK_CLIENT_CERT); certFile == "" {
certFile = defaultCertFile
}
if certFile != "" {
defaultCertKeyFile := sysconf.GetString(keyserv.CLIENT_CONF_CERT_KEY, "")
if certKeyFile = sys.InputAbsFilePath(false, defaultCertKeyFile, MSG_ASK_CLIENT_CERT_KEY); certKeyFile == "" {
certKeyFile = defaultCertKeyFile
}
}
return
}

Expand All @@ -84,7 +96,7 @@ func EncryptFS() error {
sys.LockMem()

// Prompt for connection details
sysconf, caFile, host, port, err := PromptForKeyServer()
sysconf, caFile, certFile, certKeyFile, host, port, err := PromptForKeyServer()
if err != nil {
return err
}
Expand All @@ -96,7 +108,7 @@ func EncryptFS() error {
}

// Check server connectivity before commencing encryption
client, password, err := ConnectToKeyServer(caFile, fmt.Sprintf("%s:%d", host, port))
client, password, err := ConnectToKeyServer(caFile, certFile, certKeyFile, fmt.Sprintf("%s:%d", host, port))
if err != nil {
return err
}
Expand Down Expand Up @@ -140,6 +152,8 @@ func EncryptFS() error {
sysconf.Set(keyserv.CLIENT_CONF_HOST, host)
sysconf.Set(keyserv.CLIENT_CONF_PORT, strconv.Itoa(port))
sysconf.Set(keyserv.CLIENT_CONF_CA, caFile)
sysconf.Set(keyserv.CLIENT_CONF_CERT, certFile)
sysconf.Set(keyserv.CLIENT_CONF_CERT_KEY, certKeyFile)
if err := ioutil.WriteFile(CLIENT_CONFIG_PATH, []byte(sysconf.ToText()), 0600); err != nil {
return fmt.Errorf(MSG_E_SAVE_SYSCONF, CLIENT_CONFIG_PATH, err)
}
Expand All @@ -151,11 +165,11 @@ func EncryptFS() error {
// Sub-command: forcibly unlock all file systems that have their keys on a key server.
func ManOnlineUnlockFS() error {
sys.LockMem()
_, caFile, host, port, err := PromptForKeyServer()
_, caFile, certFile, certKeyFile, host, port, err := PromptForKeyServer()
if err != nil {
return err
}
client, password, err := ConnectToKeyServer(caFile, fmt.Sprintf("%s:%d", host, port))
client, password, err := ConnectToKeyServer(caFile, certFile, certKeyFile, fmt.Sprintf("%s:%d", host, port))
if err != nil {
return err
}
Expand Down Expand Up @@ -228,7 +242,11 @@ func EraseKey() error {
return errors.New(MSG_E_ERASE_NO_CONF)
}
caFile := sysconf.GetString(keyserv.CLIENT_CONF_CA, "")
client, password, err := ConnectToKeyServer(caFile, fmt.Sprintf("%s:%d", host, port))
client, password, err := ConnectToKeyServer(
caFile,
sysconf.GetString(keyserv.CLIENT_CONF_CERT, ""),
sysconf.GetString(keyserv.CLIENT_CONF_CERT_KEY, ""),
fmt.Sprintf("%s:%d", host, port))
if err != nil {
return err
}
Expand Down
37 changes: 30 additions & 7 deletions command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const (
)

// Interactively read server password from terminal, then use the password to ping RPC server.
func ConnectToKeyServer(caFile, keyServer string) (client *keyserv.CryptClient, password string, err error) {
func ConnectToKeyServer(caFile, certFile, keyFile, keyServer string) (client *keyserv.CryptClient, password string, err error) {
sys.LockMem()
serverAddr := keyServer
port := keyserv.SRV_DEFAULT_PORT
Expand All @@ -52,13 +52,17 @@ func ConnectToKeyServer(caFile, keyServer string) (client *keyserv.CryptClient,
customCA = caFileContent
}
// Initialise client and test connectivity with the server
client, err = keyserv.NewCryptClient(serverAddr, port, customCA)
client, err = keyserv.NewCryptClient(serverAddr, port, customCA, certFile, keyFile)
if err != nil {
return nil, "", err
}
password = sys.InputPassword(true, "", "Enter key server's password (no echo)")
fmt.Fprintf(os.Stderr, "Establishing connection to %s on port %d...\n", serverAddr, port)
if err := client.Ping(keyserv.PingRequest{Password: password}); err != nil {
salt, err := client.GetSalt()
if err != nil {
return nil, "", err
}
if err := client.Ping(keyserv.PingRequest{Password: keyserv.HashPassword(salt, password)}); err != nil {
return nil, "", err
}
return
Expand Down Expand Up @@ -229,6 +233,23 @@ Important notes for client computers:
"Key database directory"); keyDBDir != "" {
sysconf.Set(keyserv.SRV_CONF_KEYDB_DIR, keyDBDir)
}
// Walk through client certificate verification settings
validateClient := sys.InputBool("Should clients present their certificate in order to access this server?")
sysconf.Set(keyserv.SRV_CONF_TLS_VALIDATE_CLIENT, validateClient)
if validateClient {
sysconf.Set(keyserv.SRV_CONF_TLS_CA, sys.Input(true, "", "PEM-encoded TLS certificate authority that will issue client certificates"))
}
// Walk through KMIP settings
useExternalKMIPServer := sys.InputBool("Should encryption keys be kept on a KMIP-compatible key management appliance?")
if useExternalKMIPServer {
sysconf.Set(keyserv.SRV_CONF_KMIP_SERVER_HOST, sys.Input(true, "", "KMIP server host name"))
sysconf.Set(keyserv.SRV_CONF_KMIP_SERVER_PORT, sys.InputInt(true, 5696, 1, 65535, "KMIP port number"))
sysconf.Set(keyserv.SRV_CONF_KMIP_SERVER_USER, sys.Input(false, "", "KMIP username"))
sysconf.Set(keyserv.SRV_CONF_KMIP_SERVER_PASS, sys.Input(false, "", "KMIP password"))
sysconf.Set(keyserv.SRV_CONF_KMIP_SERVER_TLS_CA, sys.Input(false, "", "PEM-encoded TLS certificate authority that issued KMIP server certificate"))
sysconf.Set(keyserv.SRV_CONF_KMIP_SERVER_TLS_CERT, sys.Input(false, "", "PEM-encoded TLS client identitiy certificate"))
sysconf.Set(keyserv.SRV_CONF_KMIP_SERVER_TLS_KEY, sys.Input(false, "", "PEM-encoded TLS client identitiy certificate key"))
}
// Walk through optional email settings
fmt.Println("\nTo enable Email notifications, enter the following parameters:")
if mta := sys.Input(false,
Expand Down Expand Up @@ -339,6 +360,7 @@ func KeyRPCDaemon() error {
if err := srv.ListenRPC(); err != nil {
return fmt.Errorf("Failed to listen for connections: %v", err)
}
srv.HandleConnections() // intentionally block here
return nil
}

Expand All @@ -351,12 +373,13 @@ func ListKeys() error {
recList := db.List()
fmt.Printf("Total: %d records (date and time are in zone %s)\n", len(recList), time.Now().Format("MST"))
// Print mount point last, making output possible to be parsed by a program
// Max field length: 15 (IP), 19 (IP When), 36 (UUID), 9 (Max Active), 9 (Current Active) last field (mount point)
fmt.Println("Used By When UUID Max.Users Num.Users Mount Point")
// Max field length: 15 (IP), 19 (IP When), 12(ID), 36 (UUID), 9 (Max Active), 9 (Current Active) last field (mount point)
fmt.Println("Used By When ID UUID Max.Users Num.Users Mount Point")
for _, rec := range recList {
outputTime := time.Unix(rec.LastRetrieval.Timestamp, 0).Format(TIME_OUTPUT_FORMAT)
rec.RemoveDeadHosts()
fmt.Printf("%-15s %-19s %-36s %-9s %-9s %s\n", rec.LastRetrieval.IP, outputTime, rec.UUID,
fmt.Printf("%-15s %-19s %-12s %-36s %-9s %-9s %s\n", rec.LastRetrieval.IP, outputTime,
rec.ID, rec.UUID,
strconv.Itoa(rec.MaxActive), strconv.Itoa(len(rec.AliveMessages)), rec.MountPoint)
}
return nil
Expand Down Expand Up @@ -391,7 +414,7 @@ func EditKey(uuid string) error {
rec.AliveCount = roundedAliveTimeout / routine.REPORT_ALIVE_INTERVAL_SEC
}
// Write record file and restart server to let it reload all records into memory
if err := db.Upsert(rec); err != nil {
if _, err := db.Upsert(rec); err != nil {
return fmt.Errorf("Failed to update record - %v", err)
}
fmt.Println("Record has been updated successfully.")
Expand Down
6 changes: 3 additions & 3 deletions fs/crypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ const (
)

// Call cryptsetup luksFormat on the block device node.
func CryptFormat(key []byte, blockDev string) error {
func CryptFormat(key []byte, blockDev, uuid string) error {
if err := CheckBlockDevice(blockDev); err != nil {
return err
}
_, stdout, stderr, err := sys.Exec(bytes.NewReader(key), nil, nil,
BIN_CRYPTSETUP, "--batch-mode", "--cipher", LUKS_CIPHER, "--hash", LUKS_HASH, "--key-size", LUKS_KEY_SIZE_S,
"luksFormat", "--key-file=-", blockDev)
"luksFormat", "--key-file=-", blockDev, "--uuid="+uuid)
if err != nil {
return fmt.Errorf("CryptFormat: failed to format \"%s\" - %v %s %s", blockDev, err, stdout, stderr)
}
Expand Down Expand Up @@ -133,7 +133,7 @@ func CryptStatus(name string) (mapping CryptMapping, err error) {
_, stdout, _, _ := sys.Exec(nil, nil, nil, BIN_CRYPTSETUP, "status", name)
mapping = ParseCryptStatus(stdout)
if !mapping.IsValid() {
err = fmt.Errorf("CryptStatus: failed to retrieve a valid output for \"%s\"", name)
err = fmt.Errorf("CryptStatus: failed to retrieve a valid output for \"%s\", gathered information is: %+v", name, mapping)
}
return
}
2 changes: 1 addition & 1 deletion fs/crypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "testing"

// The unit test simply makes sure that the functions do not crash, it does not set up an encrypted device node.
func TestCryptSetup(t *testing.T) {
if err := CryptFormat([]byte{}, "doesnotexist"); err == nil {
if err := CryptFormat([]byte{}, "doesnotexist", "testuuid"); err == nil {
t.Fatal("did not error")
}
if err := CryptOpen([]byte{}, "doesnotexist", "doesnotexist"); err == nil {
Expand Down
8 changes: 4 additions & 4 deletions keydb/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Caller should consider ot lock memory.
*/
func OpenDBOneRecord(dir, recordUUID string) (db *DB, err error) {
if err := os.MkdirAll(dir, DB_DIR_FILE_MODE); err != nil {
return nil, fmt.Errorf("OpenDB: failed to make db directory \"%s\" - %v", dir, err)
return nil, fmt.Errorf("OpenDBOneRecord: failed to make db directory \"%s\" - %v", dir, err)
}
db = &DB{Dir: dir, Lock: new(sync.RWMutex), RecordsByUUID: map[string]Record{}, RecordsByID: map[string]Record{}}
keyRecord, err := db.LoadRecord(path.Join(dir, recordUUID))
Expand Down Expand Up @@ -81,7 +81,7 @@ func (db *DB) ReloadDB() error {
db.RecordsByID = make(map[string]Record)
keyFiles, err := ioutil.ReadDir(db.Dir)
if err != nil {
return fmt.Errorf("ReloadDB: failed to read directory \"%s\" - %v", db.Dir, err)
return fmt.Errorf("DB.ReloadDB: failed to read directory \"%s\" - %v", db.Dir, err)
}

var lastSequenceNum int64
Expand Down Expand Up @@ -273,11 +273,11 @@ func (db *DB) Erase(uuid string) error {
db.Lock.Lock()
defer db.Lock.Unlock()
if _, exists := db.RecordsByUUID[uuid]; !exists {
return fmt.Errorf("DB.Delete: record '%s' does not exist", uuid)
return fmt.Errorf("DB.Erase: record '%s' does not exist", uuid)
}
delete(db.RecordsByUUID, uuid)
if err := fs.SecureErase(path.Join(db.Dir, uuid), true); err != nil {
return fmt.Errorf("DB.Delete: failed to delete db record for %s - %v", uuid, err)
return fmt.Errorf("DB.Erase: failed to delete db record for %s - %v", uuid, err)
}
return nil
}
34 changes: 17 additions & 17 deletions keydb/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,17 @@ func TestRecordCRUD(t *testing.T) {
rec2Alive := rec2
rec2Alive.LastRetrieval = aliveMsg
rec2Alive.AliveMessages = map[string][]AliveMessage{aliveMsg.IP: []AliveMessage{aliveMsg}}
if seq, err := db.Upsert(rec1); err != nil || seq != 1 {
if seq, err := db.Upsert(rec1); err != nil || seq != "1" {
t.Fatal(err, seq)
}
if seq, err := db.Upsert(rec2); err != nil || seq != 2 {
if seq, err := db.Upsert(rec2); err != nil || seq != "2" {
t.Fatal(err, seq)
}
// Match sequence number in my copy of records with their should-be ones
rec1.SequenceNum = 1
rec1Alive.SequenceNum = 1
rec2.SequenceNum = 2
rec2Alive.SequenceNum = 2
rec1.ID = "1"
rec1Alive.ID = "1"
rec2.ID = "2"
rec2Alive.ID = "2"
// Select one record and then select both records
if found, rejected, missing := db.Select(aliveMsg, true, "1", "doesnotexist"); !reflect.DeepEqual(found, map[string]Record{rec1.UUID: rec1Alive}) ||
!reflect.DeepEqual(rejected, []string{}) ||
Expand Down Expand Up @@ -89,7 +89,7 @@ func TestRecordCRUD(t *testing.T) {
if len(db.RecordsByUUID["1"].AliveMessages["ip1"]) != 2 || len(db.RecordsByUUID["2"].AliveMessages["ip1"]) != 2 {
t.Fatal(db.RecordsByUUID)
}
if len(db.RecordsByID[1].AliveMessages["ip1"]) != 2 || len(db.RecordsByID[2].AliveMessages["ip1"]) != 2 {
if len(db.RecordsByID["1"].AliveMessages["ip1"]) != 2 || len(db.RecordsByID["2"].AliveMessages["ip1"]) != 2 {
t.Fatal(db.RecordsByUUID)
}
// Erase a record
Expand Down Expand Up @@ -135,7 +135,7 @@ func TestOpenDBOneRecord(t *testing.T) {
},
AliveMessages: make(map[string][]AliveMessage),
}
if seq, err := db.Upsert(rec); err != nil || seq != 1 {
if seq, err := db.Upsert(rec); err != nil || seq != "1" {
t.Fatal(err)
}
dbOneRecord, err := OpenDBOneRecord(TEST_DIR, "a")
Expand All @@ -145,17 +145,17 @@ func TestOpenDBOneRecord(t *testing.T) {
if len(dbOneRecord.RecordsByUUID) != 1 {
t.Fatal(dbOneRecord.RecordsByUUID)
}
rec.SequenceNum = 1
rec.ID = "1"
if recA, found := dbOneRecord.GetByUUID("a"); !found || !reflect.DeepEqual(recA, rec) {
t.Fatal(recA, found)
}
if recA, found := dbOneRecord.GetByID(1); !found || !reflect.DeepEqual(recA, rec) {
if recA, found := dbOneRecord.GetByID("1"); !found || !reflect.DeepEqual(recA, rec) {
t.Fatal(recA, found)
}
if _, found := dbOneRecord.GetByUUID("doesnotexist"); found {
t.Fatal("false positive")
}
if _, found := dbOneRecord.GetByID(78598123); found {
if _, found := dbOneRecord.GetByID("78598123"); found {
t.Fatal("false positive")
}
}
Expand Down Expand Up @@ -209,18 +209,18 @@ func TestList(t *testing.T) {
}
rec3NoKey := rec3
rec3NoKey.Key = nil
if seq, err := db.Upsert(rec1); err != nil || seq != 1 {
if seq, err := db.Upsert(rec1); err != nil || seq != "1" {
t.Fatal(err, seq)
}
if seq, err := db.Upsert(rec2); err != nil || seq != 2 {
if seq, err := db.Upsert(rec2); err != nil || seq != "2" {
t.Fatal(err)
}
if seq, err := db.Upsert(rec3); err != nil || seq != 3 {
if seq, err := db.Upsert(rec3); err != nil || seq != "3" {
t.Fatal(err)
}
rec1NoKey.SequenceNum = 1
rec2NoKey.SequenceNum = 2
rec3NoKey.SequenceNum = 3
rec1NoKey.ID = "1"
rec2NoKey.ID = "2"
rec3NoKey.ID = "3"
recs := db.List()
if !reflect.DeepEqual(recs[0], rec1NoKey) ||
!reflect.DeepEqual(recs[1], rec3NoKey) ||
Expand Down
14 changes: 7 additions & 7 deletions keydb/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,16 @@ When stored on disk, the record resides in a file encoded in gob.
The binary encoding method is intentionally chosen to deter users from manually editing the files on disk.
*/
type Record struct {
UUID string // partition UUID
ID string // KMIP key ID
KeyOnKMIP bool // true only if key content is located on an external KMIP server
CreationTime time.Time // creation time
Version int // Record version
CreationTime time.Time // creation time
Key []byte // encryption key in plain form
MountPoint string // mount point on client computer
MountOptions []string // file system's mount options
UUID string // file system uuid
MountPoint string // mount point of the file system
MountOptions []string // mount options of the file system
MaxActive int // maximum allowed active key users (computers), set to <=0 to allow unlimited.
LastRetrieval AliveMessage // the most recent host who retrieved this key
AliveIntervalSec int // interval in seconds at which all client computers holding this key must report their liveness
AliveIntervalSec int // interval in seconds at which all user of the file system holding this key must report they're online
AliveCount int // a client computer is considered dead after missing so many alive messages
AliveMessages map[string][]AliveMessage // recent alive messages (latest is last), string map key is the host IP as seen by this server.
}
Expand Down Expand Up @@ -170,11 +169,12 @@ func (rec *Record) Deserialise(in []byte) error {

// Format all attributes (except the binary key) for pretty printing, using the specified separator.
func (rec *Record) FormatAttrs(separator string) string {
return fmt.Sprintf(`Timestamp="%d"%sIP="%s"%sHostname="%s"%sFileSystemUUID="%s"%sMountPoint="%s"%sMountOptions="%s"`,
return fmt.Sprintf(`Timestamp="%d"%sIP="%s"%sHostname="%s"%sFileSystemUUID="%s"%sKMIPID="%s"%sMountPoint="%s"%sMountOptions="%s"`,
rec.LastRetrieval.Timestamp, separator,
rec.LastRetrieval.IP, separator,
rec.LastRetrieval.Hostname, separator,
rec.UUID, separator,
rec.ID, separator,
strings.Replace(rec.MountPoint, `"`, `\"`, -1), separator,
rec.GetMountOptionStr())
}
Expand Down
3 changes: 2 additions & 1 deletion keydb/record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ func TestRecordAliveMessage2(t *testing.T) {
func TestRecord(t *testing.T) {
rec := Record{
UUID: "testuuid",
ID: "testid",
Key: []byte{0, 1, 2, 3},
MountPoint: "/tmp/a",
MountOptions: []string{"rw", "noatime"},
Expand Down Expand Up @@ -366,7 +367,7 @@ func TestRecord(t *testing.T) {
}

// Format as string
if s := rec.FormatAttrs("|"); s != `Timestamp="123456"|IP="ip1"|Hostname="host1"|FileSystemUUID="testuuid"|MountPoint="/tmp/a"|MountOptions="rw,noatime"` {
if s := rec.FormatAttrs("|"); s != `Timestamp="123456"|IP="ip1"|Hostname="host1"|FileSystemUUID="testuuid"|KMIPID="testid"|MountPoint="/tmp/a"|MountOptions="rw,noatime"` {
t.Fatal(s)
}
}
Loading

0 comments on commit 726a77c

Please sign in to comment.