summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS.md11
-rw-r--r--doc/system.md41
-rw-r--r--src/catlfish.erl74
-rwxr-xr-xtools/compileconfig.py11
4 files changed, 106 insertions, 31 deletions
diff --git a/NEWS.md b/NEWS.md
index 9551b4f..52d2017 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -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"]),
]