Skip to content

Commit 73411c1

Browse files
MB-61292: Add integrity check for stored keys
This commit adds an integrity check for stored keys. The check is performed every time the stored keys are loaded from disk. The check is done by comparing the stored key proof with the proof that is calculated using token from the tokens file. Proof for Key with KeyId is calculated the following way: Proof := TokenUUID + ENCRYPT(SHA512(Token + KeyId) with Key) Token file contains all the tokens that are considered trusted for the integrity check. Tokens are stored in the following format: TokenUUID1:Token1\n TokenUUID2:Token2\n ... Token file is encrypted with current config dek. If configuration encryption is disabled, tokens are stored in plain, so there basically no integrity protection for keys in this case. When configuration encryption gets enabled, we generate new configuration key first, then we generate new token, and then we store that new token in the tokens file encrypted with the newly generated congifuration key. By doing so we are making sure that new token is never stored in plain text. Old tokens (that were stored in plain text) are removed from the tokens file once all the existing keys have their proofs recalculated with the latest token. Rotation of tokens is performed on any config key generation, or config encryption enabling/disabling. Change-Id: I3de47705964337f20b4eb035115210549979f5c5 Reviewed-on: https://review.couchbase.org/c/ns_server/+/220792 Tested-by: Timofey Barmin <[email protected]> Tested-by: Build Bot <[email protected]> Reviewed-by: Navdeep S Boparai <[email protected]> Well-Formed: Build Bot <[email protected]>
1 parent b662a1d commit 73411c1

File tree

9 files changed

+827
-122
lines changed

9 files changed

+827
-122
lines changed

apps/ns_babysitter/src/cb_gosecrets_runner.erl

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
decrypt_with_key/5,
3939
read_key/3,
4040
defaults/0,
41-
key_path/2]).
41+
key_path/2,
42+
rotate_integrity_tokens/2,
43+
remove_old_integrity_tokens/2,
44+
get_key_id_in_use/1]).
4245

4346
-record(state, {config :: file:filename(),
4447
loop :: pid() | undefined,
@@ -135,6 +138,15 @@ decrypt_with_key(Name, Data, AD, KeyKind, KeyName) ->
135138
gen_server:call(Name, {decrypt_with_key, Data, AD, KeyKind, KeyName},
136139
infinity).
137140

141+
rotate_integrity_tokens(Name, KeyName) ->
142+
gen_server:call(Name, {rotate_integrity_tokens, KeyName}, infinity).
143+
144+
remove_old_integrity_tokens(Name, Paths) ->
145+
gen_server:call(Name, {remove_old_integrity_tokens, Paths}, infinity).
146+
147+
get_key_id_in_use(Name) ->
148+
gen_server:call(Name, get_key_id_in_use, infinity).
149+
138150
start_link(Logger, PasswordPromptAllowed) ->
139151
start_link(Logger, PasswordPromptAllowed, gosecrets_cfg_path()).
140152

@@ -352,6 +364,12 @@ handle_call({encrypt_with_key, _Data, _AD, _KeyKind, _Name} = Cmd, _From,
352364
handle_call({decrypt_with_key, _Data, _AD, _KeyKind, _Name} = Cmd, _From,
353365
State) ->
354366
{reply, convert_empty_data(call_gosecrets(Cmd, State)), State};
367+
handle_call({rotate_integrity_tokens, _KeyName} = Cmd, _From, State) ->
368+
{reply, call_gosecrets(Cmd, State), State};
369+
handle_call({remove_old_integrity_tokens, _Paths} = Cmd, _From, State) ->
370+
{reply, call_gosecrets(Cmd, State), State};
371+
handle_call(get_key_id_in_use, _From, State) ->
372+
{reply, convert_empty_data(call_gosecrets(get_key_id_in_use, State)), State};
355373
handle_call(stop, _From, State) ->
356374
{stop, normal, call_gosecrets(stop, State), State};
357375
handle_call(Call, _From, State) ->
@@ -493,7 +511,14 @@ encode({decrypt_with_key, Data, AD, KeyKind, Name}) ->
493511
(encode_param(KeyKind))/binary,
494512
(encode_param(Name))/binary>>;
495513
encode({read_key, Kind, Name}) ->
496-
<<15, (encode_param(Kind))/binary, (encode_param(Name))/binary>>.
514+
<<15, (encode_param(Kind))/binary, (encode_param(Name))/binary>>;
515+
encode({rotate_integrity_tokens, KeyName}) ->
516+
<<17, (encode_param(KeyName))/binary>>;
517+
encode({remove_old_integrity_tokens, Paths}) ->
518+
PathsBin = list_to_binary([encode_param(P) || P <- Paths]),
519+
<<18, PathsBin/binary>>;
520+
encode(get_key_id_in_use) ->
521+
<<19>>.
497522

498523
encode_param(B) when is_atom(B) ->
499524
encode_param(atom_to_binary(B));
@@ -853,6 +878,86 @@ config_reload_test() ->
853878
?assertEqual({ok, <<"default">>}, get_state(Pid))
854879
end).
855880

881+
-define(key1, <<"key10000-0000-0000-0000-000000000000">>).
882+
-define(key2, <<"key20000-0000-0000-0000-000000000000">>).
883+
-define(key3, <<"key30000-0000-0000-0000-000000000000">>).
884+
-define(key4, <<"key40000-0000-0000-0000-000000000000">>).
885+
-define(key5, <<"key50000-0000-0000-0000-000000000000">>).
886+
-define(key6, <<"key60000-0000-0000-0000-000000000000">>).
887+
-define(key7, <<"key70000-0000-0000-0000-000000000000">>).
888+
889+
integrity_check_for_stored_keys_test() ->
890+
DKFile = path_config:tempfile("bucket_dek_test", ".tmp"),
891+
ok = filelib:ensure_dir(DKFile),
892+
Cfg = [{bucket_dek_path, iolist_to_binary(DKFile)}],
893+
BucketPath = fun (Bucket) ->
894+
iolist_to_binary(filename:join([DKFile, Bucket, "deks"]))
895+
end,
896+
KeyDirs = [key_path(configDek, Cfg),
897+
key_path(logDek, Cfg),
898+
key_path(auditDek, Cfg),
899+
key_path(kek, Cfg),
900+
BucketPath("bucket1"),
901+
BucketPath("bucket2")],
902+
with_all_stored_keys_cleaned(
903+
Cfg,
904+
fun () ->
905+
with_gosecrets(
906+
Cfg,
907+
fun (CfgPath, Pid) ->
908+
TokensPath = filename:join(filename:dirname(CfgPath),
909+
"stored_keys_tokens"),
910+
StoreKey = fun (Kind, KeyId, EncryptWithKey) ->
911+
store_key(Pid, Kind, KeyId, 'raw-aes-gcm',
912+
rand:bytes(32),
913+
EncryptWithKey,
914+
<<"2024-07-26T19:32:19Z">>, false)
915+
end,
916+
917+
ok = StoreKey(configDek, ?key1, <<"encryptionService">>),
918+
919+
{ok, [undefined]} = cb_crypto:get_file_dek_ids(TokensPath),
920+
921+
%% First rotation with key1
922+
ok = rotate_integrity_tokens(Pid, ?key1),
923+
{ok, [?key1]} = cb_crypto:get_file_dek_ids(TokensPath),
924+
ok = remove_old_integrity_tokens(Pid, KeyDirs),
925+
{ok, [?key1]} = cb_crypto:get_file_dek_ids(TokensPath),
926+
927+
%% Store more keys of different kinds
928+
ok = StoreKey(kek, ?key2, <<"encryptionService">>),
929+
ok = StoreKey(configDek, ?key3, ?key2),
930+
ok = StoreKey(logDek, ?key4, ?key2),
931+
ok = StoreKey(auditDek, ?key5, ?key2),
932+
Bucket1Key = <<"bucket1/deks/", ?key6/binary>>,
933+
ok = StoreKey(bucketDek, Bucket1Key, ?key2),
934+
Bucket2Key = <<"bucket2/deks/", ?key7/binary>>,
935+
ok = StoreKey(bucketDek, Bucket2Key, ?key2),
936+
937+
%% Second rotation with key1
938+
ok = rotate_integrity_tokens(Pid, ?key3),
939+
{ok, [?key3]} = cb_crypto:get_file_dek_ids(TokensPath),
940+
941+
ok = remove_old_integrity_tokens(Pid, KeyDirs),
942+
{ok, [?key3]} = cb_crypto:get_file_dek_ids(TokensPath),
943+
944+
%% Final rotation with empty key
945+
ok = rotate_integrity_tokens(Pid, <<>>),
946+
{ok, [undefined]} = cb_crypto:get_file_dek_ids(TokensPath),
947+
ok = remove_old_integrity_tokens(Pid, KeyDirs),
948+
{ok, [undefined]} = cb_crypto:get_file_dek_ids(TokensPath),
949+
950+
%% Verify all keys are still readable
951+
{ok, _} = read_key(Pid, configDek, ?key1),
952+
{ok, _} = read_key(Pid, kek, ?key2),
953+
{ok, _} = read_key(Pid, configDek, ?key3),
954+
{ok, _} = read_key(Pid, logDek, ?key4),
955+
{ok, _} = read_key(Pid, auditDek, ?key5),
956+
{ok, _} = read_key(Pid, bucketDek, Bucket1Key),
957+
{ok, _} = read_key(Pid, bucketDek, Bucket2Key)
958+
end)
959+
end).
960+
856961
store_and_read_key_test() ->
857962
Cfg = [],
858963
DecodeKey = fun (EncodedData) ->
@@ -1216,15 +1321,15 @@ with_tmp_datakey_cfg(Fun) ->
12161321
end.
12171322

12181323
with_all_stored_keys_cleaned(Cfg, Fun) ->
1219-
KeksPath = binary_to_list(key_path(kek, Cfg)),
1220-
DeksPath = binary_to_list(key_path(configDek, Cfg)),
1221-
ok = misc:rm_rf(KeksPath), %% In case if there are leftovers
1222-
ok = misc:rm_rf(DeksPath),
1324+
Keys = [configDek, logDek, auditDek, kek, bucketDek],
1325+
Paths = [binary_to_list(P) || Key <- Keys,
1326+
P <- [key_path(Key, Cfg)],
1327+
P =/= undefined],
1328+
[ok = misc:rm_rf(Path) || Path <- Paths],
12231329
try
12241330
Fun()
12251331
after
1226-
ok = misc:rm_rf(KeksPath),
1227-
ok = misc:rm_rf(DeksPath)
1332+
[ok = misc:rm_rf(Path) || Path <- Paths]
12281333
end.
12291334

12301335
with_password_sent(WrongPassword, CorrectPassword, Fun) ->
@@ -1313,6 +1418,8 @@ with_tmp_cfg(Cfg, Fun) ->
13131418
Fun(CfgPath)
13141419
after
13151420
delete_all_default_files(),
1421+
file:delete(filename:join(filename:dirname(CfgPath),
1422+
"stored_keys_tokens")),
13161423
file:delete(CfgPath)
13171424
end.
13181425

apps/ns_server/src/cb_cluster_secrets.erl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,7 @@ maybe_update_deks(Kind, #state{deks = CurDeks} = OldState) ->
12631263
%% On disk it is enabled but in config it is disabled:
12641264
true when EncrMethod == disabled ->
12651265
NewState = set_active(Kind, ActiveId, false, State),
1266+
ok = maybe_rotate_integrity_tokens(Kind, undefined),
12661267
call_set_active_cb(Kind, NewState);
12671268

12681269
%% It is enabled on disk and in config:
@@ -1283,6 +1284,7 @@ maybe_update_deks(Kind, #state{deks = CurDeks} = OldState) ->
12831284
%% and we already have a dek
12841285
false when is_binary(ActiveId) and not ShouldRotate ->
12851286
NewState = set_active(Kind, ActiveId, true, State),
1287+
ok = maybe_rotate_integrity_tokens(Kind, ActiveId),
12861288
call_set_active_cb(Kind, NewState);
12871289

12881290
%% On disk it is disabled but in config it is enabled
@@ -1293,6 +1295,7 @@ maybe_update_deks(Kind, #state{deks = CurDeks} = OldState) ->
12931295
case generate_new_dek(Kind, Deks, EncrMethod, Snapshot) of
12941296
{ok, DekId} ->
12951297
NewState = set_active(Kind, DekId, true, State),
1298+
ok = maybe_rotate_integrity_tokens(Kind, DekId),
12961299
call_set_active_cb(Kind, NewState);
12971300
%% Too many DEKs and encryption is being enabled
12981301
%% We could not create new DEK, but should still
@@ -1496,6 +1499,13 @@ call_set_active_cb(Kind, #state{deks = AllDeks} = State) ->
14961499
{error, State, Reason}
14971500
end.
14981501

1502+
-spec maybe_rotate_integrity_tokens(cb_deks:dek_kind(),
1503+
cb_deks:dek_id() | undefined) -> ok.
1504+
maybe_rotate_integrity_tokens(configDek, DekId) ->
1505+
ok = encryption_service:maybe_rotate_integrity_tokens(DekId);
1506+
maybe_rotate_integrity_tokens(_Kind, _DekId) ->
1507+
ok.
1508+
14991509
call_dek_callback(CallbackName, Kind, Args) ->
15001510
#{CallbackName := CB} = cb_deks:dek_config(Kind),
15011511
try erlang:apply(CB, Args) of
@@ -1597,6 +1607,19 @@ maybe_read_deks(#state{proc_type = ?NODE_PROC, deks = undefined} = State) ->
15971607
NewAcc
15981608
end, NewState2, Kinds),
15991609
ok = garbage_collect_keks(),
1610+
1611+
%% We should rotate here (when process is starting) because we can
1612+
%% hypothetically crash after calling set_active() but before calling
1613+
%% maybe_rotate_integrity_tokens() below
1614+
case NewState3 of
1615+
#state{deks = #{configDek := #{is_enabled := true,
1616+
active_id := ActiveId}}} ->
1617+
ok = maybe_rotate_integrity_tokens(configDek, ActiveId);
1618+
#state{deks = #{configDek := #{is_enabled := false}}} ->
1619+
ok = maybe_rotate_integrity_tokens(configDek, undefined);
1620+
#state{} ->
1621+
ok
1622+
end,
16001623
add_jobs([{maybe_update_deks, K} || K <- Kinds], NewState3);
16011624
maybe_read_deks(#state{} = State) ->
16021625
State.

apps/ns_server/src/cb_deks.erl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,8 @@ force_config_encryption_keys() ->
425425
ok ?= simple_store:resave(?XDCR_CHECKPOINT_STORE),
426426
ok ?= chronicle_local:maybe_apply_new_keys(),
427427
ok ?= ns_ssl_services_setup:resave_encrypted_files(),
428+
ok ?= encryption_service:remove_old_integrity_tokens(
429+
[kek | dek_kinds_list()]),
428430
ok
429431
end.
430432

@@ -439,10 +441,11 @@ get_config_dek_ids_in_use() ->
439441
{ok, Ids7} ?= simple_store:get_key_ids_in_use(?XDCR_CHECKPOINT_STORE),
440442
{ok, Ids8} ?= chronicle_local:get_encryption_dek_ids(),
441443
{ok, Ids9} ?= ns_ssl_services_setup:get_key_ids_in_use(),
444+
{ok, Ids10} ?= encryption_service:get_key_ids_in_use(),
442445
{ok, lists:map(fun (undefined) -> ?NULL_DEK;
443446
(Id) -> Id
444447
end, lists:uniq(Ids1 ++ Ids2 ++ Ids3 ++ Ids4 ++ Ids5 ++
445-
Ids6 ++ Ids7 ++ Ids8 ++ Ids9))}
448+
Ids6 ++ Ids7 ++ Ids8 ++ Ids9 ++ Ids10))}
446449
end.
447450

448451
get_dek_ids_in_use(logDek) ->

apps/ns_server/src/encryption_service.erl

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
key_path/1,
3939
decode_key_info/1,
4040
garbage_collect_keks/1,
41-
garbage_collect_keys/2]).
41+
garbage_collect_keys/2,
42+
maybe_rotate_integrity_tokens/1,
43+
remove_old_integrity_tokens/1,
44+
get_key_ids_in_use/0]).
4245

4346

4447
-export_type([stored_key_error/0]).
@@ -173,6 +176,32 @@ decrypt_key(Data, AD, KekId) when is_binary(Data), is_binary(AD),
173176
cb_gosecrets_runner:decrypt_with_key(?RUNNER, Data, FinalAD, kek, KekId),
174177
decrypt_key_error).
175178

179+
maybe_rotate_integrity_tokens(undefined) ->
180+
maybe_rotate_integrity_tokens(<<>>);
181+
maybe_rotate_integrity_tokens(KeyName) ->
182+
wrap_error_msg(
183+
cb_gosecrets_runner:rotate_integrity_tokens(?RUNNER, KeyName),
184+
rotate_integrity_tokens_error).
185+
186+
remove_old_integrity_tokens(Kinds) ->
187+
Paths = lists:filtermap(
188+
fun(Kind) ->
189+
case key_path(Kind) of
190+
undefined -> false;
191+
Path -> {true, Path}
192+
end
193+
end, Kinds),
194+
wrap_error_msg(
195+
cb_gosecrets_runner:remove_old_integrity_tokens(?RUNNER, Paths),
196+
remove_old_integrity_tokens_error).
197+
198+
get_key_ids_in_use() ->
199+
case cb_gosecrets_runner:get_key_id_in_use(?RUNNER) of
200+
{ok, <<>>} -> {ok, [undefined]};
201+
{ok, KeyId} -> {ok, [KeyId]};
202+
{error, Error} -> {error, Error}
203+
end.
204+
176205
%%%===================================================================
177206
%%% callbacks
178207
%%%===================================================================

deps/gocode/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/aws/aws-sdk-go-v2/config v1.27.18
77
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5
88
github.com/evanw/esbuild v0.13.13
9+
github.com/google/uuid v1.6.0
910
gocloud.dev v0.37.0
1011
golang.org/x/crypto v0.24.0
1112
)

deps/gocode/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
9191
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
9292
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
9393
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
94+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
95+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
9496
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
9597
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
9698
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=

0 commit comments

Comments
 (0)