diff options
-rw-r--r-- | NEWS.md | 11 | ||||
-rw-r--r-- | doc/system.md | 41 | ||||
-rw-r--r-- | src/catlfish.erl | 74 | ||||
-rwxr-xr-x | tools/compileconfig.py | 11 |
4 files changed, 106 insertions, 31 deletions
@@ -1,4 +1,13 @@ -# Changes in catlfish 0.10.1-dev +# Changes in catlfish 1.0.1-alpha-dev + +## Features + +- New configuration option 'storage-sign-quorum-size' determines the + minimum number of storage nodes successfully storing an entry in + order for signing nodes to generate an SCT for it. This prevents a + rouge frontend node from sending out an SCT for an entry that will + never be merged. An effect of this is that the SCT cache is now + mandatory and can not be disabled. ## Bug fixes diff --git a/doc/system.md b/doc/system.md new file mode 100644 index 0000000..d5670d5 --- /dev/null +++ b/doc/system.md @@ -0,0 +1,41 @@ +# This document + +This document contains system documentation of catlfish and plop. + +Note that this document is far from complete. Don't draw any +conclusions from missing topics. + +## A certificate chain is being submitted to a frontend node + +External HTTP endpoint ct/v1/add-chain [RFC6962 sect 4.1] has one +input element "chain" which is an array of base64-encoded +certificates. + +The certificate chain is verified and normalised and a "duplicate +check" is done using plop:get() with a hash over the whole chain. If +the entry isn't already present in the database or if a matching SCT +signature is not found in the SCT cache, + +- the entry is added -- plop:add() +- an SCT signature is retrieved from a signing node -- plop:spt\_sig() +- the SCT signature is added to the SCT cache -- plop:add\_spt\_sig() + +If the entry wasn't already present in the database, the entry is +"committed" by calling plop:commit() which calls internal API +storage/entrycommitted on all storage nodes. + +Internal API storage/entrycommitted passes contents of the +"timestamp\_signature" header to plop:add\_spt() which + +- adds the leafhash to the entryhash key-value store, for retrieval of + leafhash given an entry (used in the duplicate check) +- adds the SPT signature to the SPT cache, i.e. the SCT cache for + catlfish + +Internal API storage/sendentry returns a "sig" header with +<KeyName>:<Signature>. The signature is returned by plop:add() to +catlfish for later use in call to plop:commit(). + +Internal API signing/sct verifies the signatures in the "signatures" +header, counts proper signatures against configured storage sign +quorum and calls its own gen\_server for an SCT signature. diff --git a/src/catlfish.erl b/src/catlfish.erl index 4bf1cdf..6d8d430 100644 --- a/src/catlfish.erl +++ b/src/catlfish.erl @@ -5,7 +5,7 @@ -export([add_chain/3, entries/2, entry_and_proof/2]). -export([known_roots/0, update_known_roots/0]). -export([init_cache_table/0]). --export([entryhash_from_entry/1, verify_entry/1, verify_entry/2]). +-export([entryhash_from_entry/1, verify_entry/1, verify_entry/2, spt_data/1]). -include_lib("eunit/include/eunit.hrl"). -define(PROTOCOL_VERSION, 0). @@ -92,28 +92,22 @@ deserialise_entry_type(<<1:16>>) -> serialise_signature_type(certificate_timestamp) -> <<0:8>>. -calc_sct(TimestampedEntry) -> +spt_data(DBEntry) -> + {_Type, MTLText, _Cert, _Chain} = unpack_entry(DBEntry), + MTL = deserialise_mtl(MTLText), + TSE = MTL#mtl.entry, + sct_data(TSE). + +sct_data(TimestampedEntry) -> + list_to_binary([<<?PROTOCOL_VERSION:8>>, + serialise_signature_type(certificate_timestamp), + serialise(TimestampedEntry)]). + +calc_sct_sig(TimestampedEntry, Signatures) -> plop:serialise( - plop:spt(list_to_binary([<<?PROTOCOL_VERSION:8>>, - serialise_signature_type(certificate_timestamp), - serialise(TimestampedEntry)]))). - -get_sct(Hash, TimestampedEntry) -> - case application:get_env(catlfish, sctcache_root_path) of - {ok, RootPath} -> - case perm:readfile(RootPath, Hash) of - Contents when is_binary(Contents) -> - Contents; - noentry -> - SCT = calc_sct(TimestampedEntry), - ok = perm:ensurefile_nosync(RootPath, Hash, SCT), - SCT - end; - _ -> - calc_sct(TimestampedEntry) - end. + plop:spt_sig(sct_data(TimestampedEntry), Signatures)). -add_to_db(Type, LeafCert, CertChain, EntryHash) -> +create_logentry(Type, LeafCert, CertChain) -> EntryType = case Type of normal -> x509_entry; precert -> precert_entry @@ -125,21 +119,39 @@ add_to_db(Type, LeafCert, CertChain, EntryHash) -> entry = TSE}), MTLHash = ht:leaf_hash(MTLText), LogEntry = pack_entry(Type, MTLText, LeafCert, CertChain), - ok = plop:add(LogEntry, MTLHash, EntryHash), - {TSE, MTLHash}. + {TSE, MTLHash, LogEntry}. get_ratelimit_token(Type) -> ratelimit:get_token(Type). + +maybe_add_to_db(Hash, LogEntry, TimestampedEntry, HasEntry) -> + CachedSCTSig = plop:get_spt_sig(Hash), + HasSig = is_binary(CachedSCTSig), + + case {HasEntry, HasSig} of + {true, true} -> + %% Entry is present in the database and a signature was + %% found in the SCT cache. + CachedSCTSig; + _ -> + %% We don't have the entry or we don't have the SCT in the + %% cache. + {ok, Signatures} = plop:add(LogEntry, Hash), + SCT_sig = calc_sct_sig(TimestampedEntry, Signatures), + ok = plop:add_spt_sig(Hash, SCT_sig), + SCT_sig + end. + -spec add_chain(binary(), [binary()], normal|precert) -> {[{_,_},...]}. add_chain(LeafCert, CertChain, Type) -> EntryHash = crypto:hash(sha256, [LeafCert | CertChain]), - {TimestampedEntry, Hash} = + {{TimestampedEntry, Hash, LogEntry}, HasEntry} = case plop:get(EntryHash) of notfound -> case get_ratelimit_token(add_chain) of ok -> - add_to_db(Type, LeafCert, CertChain, EntryHash); + {create_logentry(Type, LeafCert, CertChain), false}; _ -> exit({internalerror, "Rate limiting"}) end; @@ -147,10 +159,18 @@ add_chain(LeafCert, CertChain, Type) -> {_Type, MTLText, _Cert, _Chain} = unpack_entry(DBEntry), MTL = deserialise_mtl(MTLText), MTLText = serialise(MTL), % verify FIXME: remove - {MTL#mtl.entry, MTLHash} + {{MTL#mtl.entry, MTLHash, DBEntry}, true} end, - SCT_sig = get_sct(Hash, TimestampedEntry), + SCT_sig = maybe_add_to_db(Hash, LogEntry, TimestampedEntry, HasEntry), + + case HasEntry of + false -> + plop:commit(Hash, EntryHash, SCT_sig); + _ -> + none + end, + {[{sct_version, ?PROTOCOL_VERSION}, {id, base64:encode(plop:get_logid())}, {timestamp, TimestampedEntry#timestamped_entry.timestamp}, diff --git a/tools/compileconfig.py b/tools/compileconfig.py index fd77b90..9973a95 100755 --- a/tools/compileconfig.py +++ b/tools/compileconfig.py @@ -257,8 +257,7 @@ def gen_config(nodename, config, localconfig): if nodetype & set(["frontendnodes", "mergenodes"]): catlfishconfig.append((Symbol("known_roots_path"), localconfig["paths"]["knownroots"])) if "frontendnodes" in nodetype: - if "sctcaching" in options: - catlfishconfig.append((Symbol("sctcache_root_path"), paths["db"] + "sctcache/")) + plopconfig.append((Symbol("sptcache_root_path"), paths["db"] + "sctcache")) if "ratelimits" in localconfig: ratelimits = map(parse_ratelimit, localconfig["ratelimits"].items()) catlfishconfig.append((Symbol("ratelimits"), ratelimits)) @@ -312,6 +311,8 @@ def gen_config(nodename, config, localconfig): (Symbol("sendsth_verified_path"), paths["db"] + "sendsth-verified"), (Symbol("entryhash_from_entry"), (Symbol("catlfish"), Symbol("entryhash_from_entry"))), + (Symbol("spt_data"), + (Symbol("catlfish"), Symbol("spt_data"))), ] if "storagenodes" in nodetype: plopconfig += [ @@ -340,12 +341,14 @@ def gen_config(nodename, config, localconfig): allowed_clients = [] allowed_servers = [] + storagenodenames = [node["name"] for node in config["storagenodes"]] services = set() + storage_sign_quorum = config.get("storage-sign-quorum-size", 0) if "frontendnodes" in nodetype: - storagenodenames = [node["name"] for node in config["storagenodes"]] reloadableplopconfig.append((Symbol("storage_nodes"), storagenodeaddresses)) reloadableplopconfig.append((Symbol("storage_nodes_quorum"), config["storage-quorum-size"])) + reloadableplopconfig.append((Symbol("storage_sign_quorum"), storage_sign_quorum)) services.add(Symbol("ht")) allowed_clients += allowed_clients_frontend(mergenodenames, primarymergenodename) allowed_clients += allowed_clients_public() @@ -353,6 +356,7 @@ def gen_config(nodename, config, localconfig): if "storagenodes" in nodetype: allowed_clients += allowed_clients_storage(frontendnodenames, mergenodenames) if "signingnodes" in nodetype: + reloadableplopconfig.append((Symbol("storage_sign_quorum"), storage_sign_quorum)) allowed_clients += allowed_clients_signing(frontendnodenames, primarymergenodename) services = [Symbol("sign")] if "mergenodes" in nodetype: @@ -409,6 +413,7 @@ def gen_config(nodename, config, localconfig): reloadableplopconfig += [ (Symbol("allowed_clients"), list(allowed_clients)), (Symbol("allowed_servers"), list(allowed_servers)), + (Symbol("storage_node_names"), list(storagenodenames)), (Symbol("apikeys"), apikeys), (Symbol("version"), config["version"]), ] |