summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/catlfish.erl415
-rw-r--r--src/catlfish_app.erl20
-rw-r--r--src/catlfish_sup.erl53
-rw-r--r--src/catlfish_web.erl74
-rw-r--r--src/ratelimit.erl87
-rw-r--r--src/v1.erl182
-rw-r--r--src/x509.erl514
-rw-r--r--src/x509_test.hrl111
8 files changed, 0 insertions, 1456 deletions
diff --git a/src/catlfish.erl b/src/catlfish.erl
deleted file mode 100644
index 68e96ea..0000000
--- a/src/catlfish.erl
+++ /dev/null
@@ -1,415 +0,0 @@
-%%% Copyright (c) 2014-2015, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-
--module(catlfish).
--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]).
--include_lib("eunit/include/eunit.hrl").
-
--define(PROTOCOL_VERSION, 0).
-
--type signature_type() :: certificate_timestamp | tree_hash | test. % uint8
--type entry_type() :: x509_entry | precert_entry | test. % uint16
--type leaf_type() :: timestamped_entry | test. % uint8
--type leaf_version() :: v1 | v2. % uint8
-
--record(mtl, {leaf_version :: leaf_version(),
- leaf_type :: leaf_type(),
- entry :: timestamped_entry()}).
--type mtl() :: #mtl{}.
-
--record(timestamped_entry, {timestamp :: integer(),
- entry_type :: entry_type(),
- signed_entry :: signed_x509_entry() |
- signed_precert_entry(),
- extensions = <<>> :: binary()}).
--type timestamped_entry() :: #timestamped_entry{}.
-
--record(signed_x509_entry, {asn1_cert :: binary()}).
--type signed_x509_entry() :: #signed_x509_entry{}.
--record(signed_precert_entry, {issuer_key_hash :: binary(),
- tbs_certificate :: binary()}).
--type signed_precert_entry() :: #signed_precert_entry{}.
-
--spec serialise(mtl() | timestamped_entry() |
- signed_x509_entry() | signed_precert_entry()) -> binary().
-%% @doc Serialise a MerkleTreeLeaf as per RFC6962 Section 3.4.
-serialise(#mtl{leaf_version = LeafVersion,
- leaf_type = LeafType,
- entry = TimestampedEntry}) ->
- list_to_binary(
- [serialise_leaf_version(LeafVersion),
- serialise_leaf_type(LeafType),
- serialise(TimestampedEntry)]);
-%% @doc Serialise a TimestampedEntry as per RFC6962 Section 3.4.
-serialise(#timestamped_entry{timestamp = Timestamp,
- entry_type = EntryType,
- signed_entry = SignedEntry,
- extensions = Extensions}) ->
- list_to_binary(
- [<<Timestamp:64>>,
- serialise_entry_type(EntryType),
- serialise(SignedEntry),
- encode_tls_vector(Extensions, 2)]);
-%% @doc Serialise an ASN1.Cert as per RFC6962 Section 3.1.
-serialise(#signed_x509_entry{asn1_cert = Cert}) ->
- encode_tls_vector(Cert, 3);
-%% @doc Serialise a PreCert as per RFC6962 Section 3.2.
-serialise(#signed_precert_entry{
- issuer_key_hash = IssuerKeyHash,
- tbs_certificate = TBSCertificate}) when is_binary(IssuerKeyHash),
- size(IssuerKeyHash) == 32 ->
- list_to_binary(
- [IssuerKeyHash,
- encode_tls_vector(TBSCertificate, 3)]).
-
-serialise_leaf_version(v1) ->
- <<0:8>>;
-serialise_leaf_version(v2) ->
- <<1:8>>.
-deserialise_leaf_version(<<0:8>>) ->
- v1;
-deserialise_leaf_version(<<1:8>>) ->
- v2.
-
-serialise_leaf_type(timestamped_entry) ->
- <<0:8>>.
-deserialise_leaf_type(<<0:8>>) ->
- timestamped_entry.
-
-serialise_entry_type(x509_entry) ->
- <<0:16>>;
-serialise_entry_type(precert_entry) ->
- <<1:16>>.
-deserialise_entry_type(<<0:16>>) ->
- x509_entry;
-deserialise_entry_type(<<1:16>>) ->
- precert_entry.
-
--spec serialise_signature_type(signature_type()) -> binary().
-serialise_signature_type(certificate_timestamp) ->
- <<0:8>>.
-
-calc_sct(TimestampedEntry) ->
- 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.
-
-add_to_db(Type, LeafCert, CertChain, EntryHash) ->
- EntryType = case Type of
- normal -> x509_entry;
- precert -> precert_entry
- end,
- Timestamp = plop:generate_timestamp(),
- TSE = timestamped_entry(Timestamp, EntryType, LeafCert, CertChain),
- MTLText = serialise(#mtl{leaf_version = v1,
- leaf_type = timestamped_entry,
- entry = TSE}),
- MTLHash = ht:leaf_hash(MTLText),
- LogEntry = pack_entry(Type, MTLText, LeafCert, CertChain),
- ok = plop:add(LogEntry, MTLHash, EntryHash),
- {TSE, MTLHash}.
-
-get_ratelimit_token(Type) ->
- ratelimit:get_token(Type).
-
--spec add_chain(binary(), [binary()], normal|precert) -> {[{_,_},...]}.
-add_chain(LeafCert, CertChain, Type) ->
- EntryHash = crypto:hash(sha256, [LeafCert | CertChain]),
- {TimestampedEntry, Hash} =
- case plop:get(EntryHash) of
- notfound ->
- case get_ratelimit_token(add_chain) of
- ok ->
- add_to_db(Type, LeafCert, CertChain, EntryHash);
- _ ->
- exit({internalerror, "Rate limiting"})
- end;
- {_Index, MTLHash, DBEntry} ->
- {_Type, MTLText, _Cert, _Chain} = unpack_entry(DBEntry),
- MTL = deserialise_mtl(MTLText),
- MTLText = serialise(MTL), % verify FIXME: remove
- {MTL#mtl.entry, MTLHash}
- end,
-
- SCT_sig = get_sct(Hash, TimestampedEntry),
- {[{sct_version, ?PROTOCOL_VERSION},
- {id, base64:encode(plop:get_logid())},
- {timestamp, TimestampedEntry#timestamped_entry.timestamp},
- {extensions, base64:encode(<<>>)},
- {signature, base64:encode(SCT_sig)}]}.
-
--spec timestamped_entry(integer(), entry_type(), binary(), binary()) ->
- timestamped_entry().
-timestamped_entry(Timestamp, EntryType, LeafCert, CertChain) ->
- SignedEntry =
- case EntryType of
- x509_entry ->
- #signed_x509_entry{asn1_cert = LeafCert};
- precert_entry ->
- {DetoxedLeafTBSCert, IssuerKeyHash} =
- x509:detox(LeafCert, CertChain),
- #signed_precert_entry{
- issuer_key_hash = IssuerKeyHash,
- tbs_certificate = DetoxedLeafTBSCert}
- end,
- #timestamped_entry{timestamp = Timestamp,
- entry_type = EntryType,
- signed_entry = SignedEntry}.
-
--spec deserialise_mtl(binary()) -> mtl().
-deserialise_mtl(Data) ->
- <<LeafVersionBin:1/binary,
- LeafTypeBin:1/binary,
- TimestampedEntryBin/binary>> = Data,
- #mtl{leaf_version = deserialise_leaf_version(LeafVersionBin),
- leaf_type = deserialise_leaf_type(LeafTypeBin),
- entry = deserialise_timestampedentry(TimestampedEntryBin)}.
-
--spec deserialise_timestampedentry(binary()) -> timestamped_entry().
-deserialise_timestampedentry(Data) ->
- <<Timestamp:64, EntryTypeBin:2/binary, RestData/binary>> = Data,
- EntryType = deserialise_entry_type(EntryTypeBin),
- {SignedEntry, ExtensionsBin} =
- case EntryType of
- x509_entry ->
- deserialise_signed_x509_entry(RestData);
- precert_entry ->
- deserialise_signed_precert_entry(RestData)
- end,
- {Extensions, <<>>} = decode_tls_vector(ExtensionsBin, 2),
- #timestamped_entry{timestamp = Timestamp,
- entry_type = EntryType,
- signed_entry = SignedEntry,
- extensions = Extensions}.
-
--spec deserialise_signed_x509_entry(binary()) -> {signed_x509_entry(), binary()}.
-deserialise_signed_x509_entry(Data) ->
- {E, D} = decode_tls_vector(Data, 3),
- {#signed_x509_entry{asn1_cert = E}, D}.
-
--spec deserialise_signed_precert_entry(binary()) ->
- {signed_precert_entry(), binary()}.
-deserialise_signed_precert_entry(Data) ->
- <<IssuerKeyHash:32/binary, RestData/binary>> = Data,
- {TBSCertificate, RestData2} = decode_tls_vector(RestData, 3),
- {#signed_precert_entry{issuer_key_hash = IssuerKeyHash,
- tbs_certificate = TBSCertificate},
- RestData2}.
-
-serialise_extra_data(Type, Cert, Chain) ->
- EncodedChain = encode_tls_vector(
- list_to_binary(
- [encode_tls_vector(C, 3) || C <- Chain]), 3),
- case Type of
- normal ->
- EncodedChain;
- precert ->
- list_to_binary(
- [encode_tls_vector(Cert, 3), EncodedChain])
- end.
-
--spec entries(non_neg_integer(), non_neg_integer()) -> {[{entries, list()},...]}.
-entries(Start, End) ->
- {[{entries, x_entries(plop:get(Start, End))}]}.
-
--spec entry_and_proof(non_neg_integer(), non_neg_integer()) -> {[{_,_},...]}.
-entry_and_proof(Index, TreeSize) ->
- case plop:inclusion_and_entry(Index, TreeSize) of
- {ok, Entry, Path} ->
- {Type, MTLText, Cert, Chain} = unpack_entry(Entry),
- ExtraData = serialise_extra_data(Type, Cert, Chain),
- {[{leaf_input, base64:encode(MTLText)},
- {extra_data, base64:encode(ExtraData)},
- {audit_path, [base64:encode(X) || X <- Path]}]};
- {notfound, Msg} ->
- {[{success, false},
- {error_message, list_to_binary(Msg)}]}
- end.
-
--define(CACHE_TABLE, catlfish_cache).
-init_cache_table() ->
- case ets:info(?CACHE_TABLE) of
- undefined -> ok;
- _ -> ets:delete(?CACHE_TABLE)
- end,
- ets:new(?CACHE_TABLE, [set, public, named_table]).
-
-verify_mtl(MTL, LeafCert, CertChain) ->
- Timestamp = MTL#mtl.entry#timestamped_entry.timestamp,
- EntryType = MTL#mtl.entry#timestamped_entry.entry_type,
- TSE = timestamped_entry(Timestamp, EntryType, LeafCert, CertChain),
- case MTL of
- #mtl{leaf_version = v1,
- leaf_type = timestamped_entry,
- entry = TSE} ->
- ok;
- _ ->
- error
- end.
-
-verify_entry(Entry) ->
- RootCerts = known_roots(),
- verify_entry(Entry, RootCerts).
-
-%% Used from plop.
-verify_entry(PackedEntry, RootCerts) ->
- {_Type, MTLText, Cert, Chain} = unpack_entry(PackedEntry),
- case x509:normalise_chain(RootCerts, [Cert | Chain]) of
- {ok, [Cert | FullChain]} ->
- case verify_mtl(deserialise_mtl(MTLText), Cert, FullChain) of
- ok ->
- {ok, ht:leaf_hash(MTLText)};
- error ->
- {error, "MTL verification failed"}
- end;
- {error, Reason} ->
- {error, Reason}
- end.
-
-%% Used from plop.
-entryhash_from_entry(PackedEntry) ->
- {_Type, _MTLText, Cert, Chain} = unpack_entry(PackedEntry),
- crypto:hash(sha256, [Cert | Chain]).
-
-%% Private functions.
--spec pack_entry(normal|precert, binary(), binary(), [binary()]) -> binary().
-pack_entry(Type, MTLText, EndEntityCert, CertChain) ->
- [{<<"MTL1">>, MTLText},
- {case Type of
- normal -> <<"EEC1">>;
- precert -> <<"PRC1">>
- end, EndEntityCert},
- {<<"CHN1">>,
- list_to_binary(
- [tlv:encode(<<"X509">>, E) || E <- CertChain])}].
-
--spec unpack_entry(binary()) -> {normal|precert, binary(), binary(), [binary()]}.
-unpack_entry(Entry) ->
- [{<<"MTL1">>, MTLText}|Rest1] = Entry,
- [{EECType, EndEntityCert}|Rest2] = Rest1,
- Type = case EECType of
- <<"EEC1">> ->
- normal;
- <<"PRC1">> ->
- precert
- end,
- [{<<"CHN1">>, PackedChain}|_Rest3] = Rest2,
- Chain = unpack_certchain(PackedChain),
- {Type, MTLText, EndEntityCert, Chain}.
-
-unpack_certchain(<<>>) ->
- [];
-unpack_certchain(Data) ->
- {<<"X509">>, Unpacked, Rest} = tlv:decode(Data),
- [Unpacked | unpack_certchain(Rest)].
-
--spec x_entries([{non_neg_integer(), binary(), binary()}]) -> list().
-x_entries([]) ->
- [];
-x_entries([H|T]) ->
- {_Index, _Hash, Entry} = H,
- {Type, MTL, Cert, Chain} = unpack_entry(Entry),
- ExtraData = serialise_extra_data(Type, Cert, Chain),
- [{[{leaf_input, base64:encode(MTL)},
- {extra_data, base64:encode(ExtraData)}]} | x_entries(T)].
-
--spec encode_tls_vector(binary(), non_neg_integer()) -> binary().
-encode_tls_vector(Binary, LengthLen) ->
- Length = byte_size(Binary),
- <<Length:LengthLen/integer-unit:8, Binary/binary>>.
-
--spec decode_tls_vector(binary(), non_neg_integer()) -> {binary(), binary()}.
-decode_tls_vector(Binary, LengthLen) ->
- <<Length:LengthLen/integer-unit:8, Rest/binary>> = Binary,
- <<ExtractedBinary:Length/binary-unit:8, Rest2/binary>> = Rest,
- {ExtractedBinary, Rest2}.
-
--define(ROOTS_CACHE_KEY, roots).
-
-update_known_roots() ->
- case application:get_env(catlfish, known_roots_path) of
- {ok, Dir} -> update_known_roots(Dir);
- undefined -> []
- end.
-
-update_known_roots(Directory) ->
- known_roots(Directory, update_tab).
-
-known_roots() ->
- case application:get_env(catlfish, known_roots_path) of
- {ok, Dir} -> known_roots(Dir, use_cache);
- undefined -> []
- end.
-
--spec known_roots(file:filename(), use_cache|update_tab) -> [binary()].
-known_roots(Directory, CacheUsage) ->
- case CacheUsage of
- use_cache ->
- case ets:lookup(?CACHE_TABLE, ?ROOTS_CACHE_KEY) of
- [] ->
- read_files_and_update_table(Directory);
- [{roots, DerList}] ->
- DerList
- end;
- update_tab ->
- read_files_and_update_table(Directory)
- end.
-
-read_files_and_update_table(Directory) ->
- Certs = x509:read_pemfiles_from_dir(Directory),
- Proper = x509:self_signed(Certs),
- case length(Certs) - length(Proper) of
- 0 -> ok;
- N -> lager:warning(
- "Ignoring ~p root certificates not signing themselves properly",
- [N])
- end,
- true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, Proper}),
- lager:info("Known roots imported: ~p", [length(Proper)]),
- Proper.
-
-%%%%%%%%%%%%%%%%%%%%
-%% Testing internal functions.
--define(PEMFILES_DIR_OK, "test/testdata/known_roots").
--define(PEMFILES_DIR_NONEXISTENT, "test/testdata/nonexistent-dir").
-
-read_pemfiles_test_() ->
- {setup,
- fun() ->
- init_cache_table(),
- {known_roots(?PEMFILES_DIR_OK, update_tab),
- known_roots(?PEMFILES_DIR_OK, use_cache)}
- end,
- fun(_) -> ets:delete(?CACHE_TABLE, ?ROOTS_CACHE_KEY) end,
- fun({L, LCached}) ->
- [?_assertMatch(4, length(L)),
- ?_assertEqual(L, LCached)]
- end}.
-
-read_pemfiles_fail_test_() ->
- {setup,
- fun() ->
- init_cache_table(),
- known_roots(?PEMFILES_DIR_NONEXISTENT, update_tab)
- end,
- fun(_) -> ets:delete(?CACHE_TABLE, ?ROOTS_CACHE_KEY) end,
- fun(Empty) -> [?_assertMatch([], Empty)] end}.
diff --git a/src/catlfish_app.erl b/src/catlfish_app.erl
deleted file mode 100644
index eef74d6..0000000
--- a/src/catlfish_app.erl
+++ /dev/null
@@ -1,20 +0,0 @@
-%%% Copyright (c) 2014-2015, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-
--module(catlfish_app).
-
--behaviour(application).
-
-%% Application callbacks
--export([start/2, stop/1]).
-
-%% ===================================================================
-%% Application callbacks
-%% ===================================================================
-
-start(normal, Args) ->
- catlfish:init_cache_table(),
- catlfish_sup:start_link(Args).
-
-stop(_State) ->
- ok.
diff --git a/src/catlfish_sup.erl b/src/catlfish_sup.erl
deleted file mode 100644
index 353b691..0000000
--- a/src/catlfish_sup.erl
+++ /dev/null
@@ -1,53 +0,0 @@
-%%% Copyright (c) 2014-2015, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-
--module(catlfish_sup).
--behaviour(supervisor).
-
--export([start_link/1, init/1]).
-
-start_link(_Args) ->
- supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-
-permanent_worker(Name, {Module, Function, Args}) ->
- permanent_worker(Name, {Module, Function, Args}, [Module]).
-
-permanent_worker(Name, StartFunc, Modules) ->
- {Name,
- StartFunc,
- permanent,
- 10000,
- worker, Modules}.
-
-gen_http_config(Config, SSLOptions, SSLFlag) ->
- {ChildName, IpAddress, Port, Module} = Config,
- {ok, IPv4Address} =
- inet:parse_ipv4strict_address(IpAddress),
- WebConfig = [{ip, IPv4Address},
- {port, Port},
- {ssl, SSLFlag},
- {acceptor_pool_size, application:get_env(catlfish, http_server_pool_size, 16)},
- {ssl_opts, SSLOptions}
- ],
- {ChildName,
- {catlfish_web, start, [WebConfig, Module, ChildName]},
- permanent, 5000,
- worker, dynamic}.
-
-init([]) ->
- SSLOptions =
- [{certfile, application:get_env(catlfish, https_certfile, none)},
- {keyfile, application:get_env(catlfish, https_keyfile, none)},
- {cacertfile, application:get_env(catlfish, https_cacertfile, none)}],
- Servers =
- lists:map(fun (Config) ->
- gen_http_config(Config, SSLOptions, true)
- end, application:get_env(catlfish, https_servers, [])) ++
- lists:map(fun (Config) ->
- gen_http_config(Config, SSLOptions, false)
- end, application:get_env(catlfish, http_servers, [])),
- lager:debug("Starting servers ~p", [Servers]),
- RateLimit = permanent_worker(ratelimit, {ratelimit, start_link, []}),
- {ok,
- {{one_for_one, 3, 10},
- [RateLimit | Servers]}}.
diff --git a/src/catlfish_web.erl b/src/catlfish_web.erl
deleted file mode 100644
index d49aaa1..0000000
--- a/src/catlfish_web.erl
+++ /dev/null
@@ -1,74 +0,0 @@
-%%% Copyright (c) 2014-2015, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-
--module(catlfish_web).
--export([start/3, loop/2]).
-
-start(Options, Module, Name) ->
- lager:debug("Starting catlfish web server: ~p", [Module]),
- Loop = fun (Req) ->
- ?MODULE:loop(Req, Module)
- end,
- mochiweb_http:start([{name, Name}, {loop, Loop} | Options]).
-
-
-add_auth(Path, {Code, Headers, Data}) ->
- AuthHeader = http_auth:create_auth("REPLY", Path, Data),
- lager:debug("sent auth header: ~p", [AuthHeader]),
- {Code, [{"X-Catlfish-Auth", AuthHeader} | Headers], Data}.
-
-loop(Req, Module) ->
- "/" ++ Path = Req:get(path),
- try
- Starttime = os:timestamp(),
- AuthHeader = Req:get_header_value("X-Catlfish-Auth"),
- case Req:get(method) of
- 'GET' ->
- Query = Req:parse_qs(),
- {_, RawQuery, _} = mochiweb_util:urlsplit_path(Req:get(raw_path)),
- Result = case http_auth:verify_auth(AuthHeader, "GET", "/" ++ Path, RawQuery) of
- failure ->
- {403, [{"Content-Type", "text/plain"}], "Invalid credentials"};
- success ->
- lager:debug("GET ~p ~p", [Path, Query]),
- add_auth("/" ++ Path, Module:request(get, Path, Query));
- noauth ->
- lager:debug("GET ~p ~p", [Path, Query]),
- Module:request(get, Path, Query)
- end,
- lager:debug("GET finished: ~p us", [timer:now_diff(os:timestamp(), Starttime)]),
- case Result of
- none ->
- Req:respond({404, [{"Content-Type", "text/plain"}], "Page not found"});
- _ ->
- Req:respond(Result)
- end;
- 'POST' ->
- Body = Req:recv_body(),
- Result = case http_auth:verify_auth(AuthHeader, "POST", "/" ++ Path, Body) of
- failure ->
- {403, [{"Content-Type", "text/plain"}], "Invalid credentials"};
- success ->
- lager:debug("POST ~p ~p", [Path, Body]),
- add_auth("/" ++ Path, Module:request(post, Path, Body));
- noauth ->
- lager:debug("POST ~p ~p", [Path, Body]),
- Module:request(post, Path, Body)
- end,
- lager:debug("POST finished: ~p us", [timer:now_diff(os:timestamp(), Starttime)]),
- case Result of
- none ->
- Req:respond({404, [{"Content-Type", "text/plain"}], "Page not found"});
- _ ->
- Req:respond(Result)
- end;
- _ ->
- Req:respond({501, [], []})
- end
- catch
- Type:What ->
- [CrashFunction | Stack] = erlang:get_stacktrace(),
- lager:error("Crash in ~p for path ~p: ~p ~p~n~p~n~p~n", [Module, Path, Type, What, CrashFunction, Stack]),
- Req:respond({500, [{"Content-Type", "text/plain"}],
- "Internal Server Error\n"})
- end.
diff --git a/src/ratelimit.erl b/src/ratelimit.erl
deleted file mode 100644
index 16d0f30..0000000
--- a/src/ratelimit.erl
+++ /dev/null
@@ -1,87 +0,0 @@
-%%% Copyright (c) 2015, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-%%%
-
--module(ratelimit).
--behaviour(gen_server).
-
--export([start_link/0, stop/0]).
--export([get_token/1]).
-%% gen_server callbacks.
--export([init/1, handle_call/3, terminate/2, handle_cast/2, handle_info/2,
- code_change/3]).
-
-start_link() ->
- gen_server:start_link({local, ?MODULE}, ?MODULE,
- application:get_env(catlfish, ratelimits, []), []).
-
-stop() ->
- gen_server:call(?MODULE, stop).
-
-get_token(Type) ->
- gen_server:call(?MODULE, {get_token, Type}).
-
-init(Types) ->
- lager:debug("starting ratelimit service"),
- State = dict:from_list([{Type, {Rules, queue:new()}}
- || {Type, Rules} <- Types]),
- {ok, State}.
-
-rule_interval_atom([{_, Interval}]) ->
- Interval.
-
-rule_interval([{_, second}]) ->
- 1000;
-rule_interval([{_, minute}]) ->
- 60*1000;
-rule_interval([{_, hour}]) ->
- 60*60*1000.
-
-rule_times([{Times, _}]) when is_integer(Times) ->
- Times.
-
-clean_queue(Interval, Queue) ->
- Now = plop:generate_timestamp(),
- case queue:peek(Queue) of
- {value, Item} when Item + Interval < Now ->
- clean_queue(Interval, queue:drop(Queue));
- _ ->
- Queue
- end.
-
-get_token_for_type({none, Queue}) ->
- {ok, {none, Queue}};
-get_token_for_type({Rules, Queue}) ->
- CleanedQueue = clean_queue(rule_interval(Rules), Queue),
- MaxTimes = rule_times(Rules),
- QueueLength = queue:len(CleanedQueue),
- if
- QueueLength < MaxTimes ->
- Now = plop:generate_timestamp(),
- {ok, {Rules, queue:in(Now, CleanedQueue)}};
- true ->
- {overload, {Rules, CleanedQueue}}
- end.
-
-handle_call(stop, _From, State) ->
- {stop, normal, stopped, State};
-handle_call({get_token, Type}, _From, State) ->
- case dict:find(Type, State) of
- {ok, TypeState} ->
- {Result, NewTypeState} = get_token_for_type(TypeState),
- {Rules, Queue} = NewTypeState,
- lager:debug("current rate: ~p per ~p",
- [queue:len(Queue), rule_interval_atom(Rules)]),
- {reply, Result, dict:store(Type, NewTypeState, State)};
- error ->
- {reply, ok, State}
- end.
-
-handle_cast(_Request, State) ->
- {noreply, State}.
-handle_info(_Info, State) ->
- {noreply, State}.
-code_change(_OldVersion, State, _Extra) ->
- {ok, State}.
-terminate(_Reason, _State) ->
- ok.
diff --git a/src/v1.erl b/src/v1.erl
deleted file mode 100644
index e066cdd..0000000
--- a/src/v1.erl
+++ /dev/null
@@ -1,182 +0,0 @@
-%%% Copyright (c) 2014-2015, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-
-%%% @doc Certificate Transparency (RFC 6962)
-
--module(v1).
-%% API (URL)
--export([request/3]).
-
-check_valid_sth() ->
- case plop:sth() of
- noentry ->
- lager:error("No valid STH found"),
- exit({internalerror, "No valid STH found"});
- {struct, PropList} ->
- Now = plop:generate_timestamp(),
- Timestamp = proplists:get_value(<<"timestamp">>, PropList),
- MMD = application:get_env(catlfish, mmd, 86400) * 1000,
- if
- Now - Timestamp > MMD ->
- lager:error("Old STH found, " ++
- "now: ~p, STH timestamp: ~p, diff: ~p",
- [Now, Timestamp, Now - Timestamp]),
- exit({internalerror, "No valid STH found"});
- true ->
- ok
- end
- end.
-
-%% Public functions, i.e. part of URL.
-request(post, "ct/v1/add-chain", Input) ->
- check_valid_sth(),
- add_chain(Input, normal);
-
-request(post, "ct/v1/add-pre-chain", Input) ->
- check_valid_sth(),
- add_chain(Input, precert);
-
-request(get, "ct/v1/get-sth", _Query) ->
- check_valid_sth(),
- case plop:sth() of
- noentry ->
- lager:error("No valid STH found"),
- internalerror("No valid STH found");
- R ->
- success(R)
- end;
-
-request(get, "ct/v1/get-sth-consistency", Query) ->
- check_valid_sth(),
- case lists:sort(Query) of
- [{"first", FirstInput}, {"second", SecondInput}] ->
- {First, _} = string:to_integer(FirstInput),
- {Second, _} = string:to_integer(SecondInput),
- case lists:member(error, [First, Second]) of
- true ->
- err400("get-sth-consistency: bad input:",
- [FirstInput, SecondInput]);
- false ->
- success(
- {[{consistency,
- [base64:encode(X) ||
- X <- plop:consistency(First, Second)]}]})
- end;
- _ -> err400("get-sth-consistency: bad input:", Query)
- end;
-
-request(get, "ct/v1/get-proof-by-hash", Query) ->
- check_valid_sth(),
- case lists:sort(Query) of
- [{"hash", HashInput}, {"tree_size", TreeSizeInput}] ->
- Hash = case (catch base64:decode(HashInput)) of
- {'EXIT', _} -> error;
- H -> H
- end,
- {TreeSize, _} = string:to_integer(TreeSizeInput),
- case lists:member(error, [Hash, TreeSize]) of
- true ->
- err400("get-proof-by-hash: bad input:",
- [HashInput, TreeSizeInput]);
- false ->
- case plop:inclusion(Hash, TreeSize) of
- {ok, Index, Path} ->
- success({[{leaf_index, Index},
- {audit_path,
- [base64:encode(X) || X <- Path]}]});
- {notfound, Msg} ->
- err400("get-proof-by-hash: hash not found", Msg)
- end
- end;
- _ -> err400("get-proof-by-hash: bad input:", Query)
- end;
-
-request(get, "ct/v1/get-entries", Query) ->
- check_valid_sth(),
- case lists:sort(Query) of
- [{"end", EndInput}, {"start", StartInput}] ->
- {Start, _} = string:to_integer(StartInput),
- {End, _} = string:to_integer(EndInput),
- case lists:member(error, [Start, End]) of
- true -> err400("get-entries: bad input:", [Start, End]);
- false -> success(
- catlfish:entries(Start, min(End, Start + 999)))
- end;
- _ -> err400("get-entries: bad input:", Query)
- end;
-
-request(get, "ct/v1/get-entry-and-proof", Query) ->
- check_valid_sth(),
- case lists:sort(Query) of
- [{"leaf_index", IndexInput}, {"tree_size", TreeSizeInput}] ->
- {Index, _} = string:to_integer(IndexInput),
- {TreeSize, _} = string:to_integer(TreeSizeInput),
- case lists:member(error, [Index, TreeSize]) of
- true ->
- err400("get-entry-and-proof: not integers: ",
- [IndexInput, TreeSizeInput]);
- false -> success(catlfish:entry_and_proof(Index, TreeSize))
- end;
- _ -> err400("get-entry-and-proof: bad input:", Query)
- end;
-
-request(get, "ct/v1/get-roots", _Query) ->
- check_valid_sth(),
- R = [{certificates,
- [base64:encode(Der) ||
- Der <- catlfish:update_known_roots()]}],
- success({R});
-
-request(_Method, _Path, _) ->
- none.
-
-%% Private functions.
-err400(Text, Input) ->
- {400, [{"Content-Type", "text/html"}],
- io_lib:format(
- "<html><body><p>~n" ++
- "~s~n" ++
- "~p~n" ++
- "</body></html>~n", [Text, Input])}.
-
-success(Data) ->
- {200, [{"Content-Type", "text/json"}], mochijson2:encode(Data)}.
-
-internalerror(Text) ->
- {500, [{"Content-Type", "text/html"}],
- io_lib:format(
- "<html><body><p>~n" ++
- "~s~n" ++
- "</body></html>~n", [Text])}.
-
--spec add_chain(any(), normal|precert) -> any().
-add_chain(Input, Type) ->
- case (catch mochijson2:decode(Input)) of
- {error, E} ->
- err400("add-chain: bad input:", E);
- {struct, [{<<"chain">>, ChainB64List}]} ->
- case decode_chain(ChainB64List) of
- [LeafCert | CertChain] ->
- case x509:normalise_chain(catlfish:known_roots(),
- [LeafCert|CertChain]) of
- {ok, [Leaf | Chain]} ->
- lager:info("adding ~p cert ~p",
- [Type, x509:cert_string(LeafCert)]),
- success(catlfish:add_chain(Leaf, Chain, Type));
- {error, Reason} ->
- lager:info("rejecting ~p: ~p",
- [x509:cert_string(LeafCert), Reason]),
- err400("add-chain: invalid chain", Reason)
- end;
- {invalid, ErrText} ->
- err400(io:format("add-chain: ~p", [ErrText]), [ChainB64List])
- end;
- _ -> err400("add-chain: missing input: chain", Input)
- end.
-
--spec decode_chain(string()) -> {invalid, string()} | [binary()].
-decode_chain(B64List) ->
- case (catch [base64:decode(X) || X <- B64List]) of
- {'EXIT', _} -> {invalid, "invalid base64-encoded chain"};
- L -> L
- end.
diff --git a/src/x509.erl b/src/x509.erl
deleted file mode 100644
index 279d9b9..0000000
--- a/src/x509.erl
+++ /dev/null
@@ -1,514 +0,0 @@
-%%% Copyright (c) 2014-2015, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-
--module(x509).
--export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1,
- self_signed/1, detox/2]).
--include_lib("public_key/include/public_key.hrl").
--include_lib("eunit/include/eunit.hrl").
--import(lists, [nth/2, filter/2]).
-
--type reason() :: {chain_too_long |
- root_unknown |
- signature_mismatch |
- encoding_invalid}.
-
--define(MAX_CHAIN_LENGTH, 10).
--define(LEAF_POISON_OID, {1,3,6,1,4,1,11129,2,4,3}).
--define(LEAF_POISON_VAL, [5,0]).
--define(CA_POISON_OID, {1,3,6,1,4,1,11129,2,4,4}).
-
--spec normalise_chain([binary()], [binary()]) -> {ok, [binary()]} |
- {error, reason()}.
-normalise_chain(AcceptableRootCerts, CertChain) ->
- case normalise_chain(AcceptableRootCerts, CertChain, ?MAX_CHAIN_LENGTH) of
- {false, Reason} ->
- {error, Reason};
- {true, Root} ->
- {ok, CertChain ++ Root}
- end.
-
--spec cert_string(binary()) -> string().
-cert_string(Der) ->
- mochihex:to_hex(crypto:hash(sha, Der)).
-
--spec read_pemfiles_from_dir(file:filename()) -> [binary()].
-%% @doc Reading certificates from files. Flattening the result -- all
-%% certs in all files are returned in a single list.
-read_pemfiles_from_dir(Dir) ->
- case file:list_dir(Dir) of
- {error, enoent} ->
- lager:error("directory does not exist: ~p", [Dir]),
- [];
- {error, Reason} ->
- lager:error("unable to read directory ~p: ~p", [Dir, Reason]),
- [];
- {ok, Filenames} ->
- Files = lists:filter(
- fun(F) -> string:equal(".pem", filename:extension(F)) end,
- Filenames),
- ders_from_pemfiles(Dir, Files)
- end.
-
--spec self_signed([binary()]) -> [binary()].
-%% @doc Return a list of certs in L that are self signed.
-self_signed(L) ->
- lists:filter(fun(Cert) -> signed_by_p(Cert, Cert) end, L).
-
--spec detox(binary(), [binary()]) -> {binary(), binary()}.
-%% @doc Return a detoxed LeafDer and its issuer.
-detox(LeafDer, ChainDer) ->
- detox_precert(LeafDer, nth(1, ChainDer), nth(2, ChainDer)).
-
-%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-%% Private functions.
-
--spec normalise_chain([binary()], [binary()], integer()) ->
- {false, reason()} | {true, [binary()]}.
-%% @doc Verify that the leaf cert or precert has a valid chain back to
-%% an acceptable root cert. The order of certificates in the second
-%% argument is: leaf cert in head, chain in tail. Order of first
-%% argument is irrelevant.
-%%
-%% Return {false, Reason} or {true, ListWithRoot}. Note that
-%% ListWithRoot allways contain exactly one element -- a CA cert from
-%% first argument (AcceptableRootCerts) signing the root of the
-%% chain. FIXME: Any point in returning this as a list?
-normalise_chain(_, _, MaxChainLength) when MaxChainLength =< 0 ->
- %% Chain too long.
- {false, chain_too_long};
-normalise_chain(AcceptableRootCerts, [TopCert], MaxChainLength) ->
- %% Check root of chain.
- case lists:member(TopCert, AcceptableRootCerts) of
- true ->
- %% Top cert is part of chain.
- {true, [TopCert]};
- false when MaxChainLength =< 1 ->
- %% Chain too long.
- {false, chain_too_long};
- false ->
- %% Top cert _might_ be signed by a cert in truststore.
- case signer(TopCert, AcceptableRootCerts) of
- notfound -> {false, root_unknown};
- Root -> {true, [Root]}
- end
- end;
-normalise_chain(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) ->
- case signed_by_p(BottomCert, hd(Rest)) of
- true -> normalise_chain(AcceptableRootCerts, Rest, MaxChainLength - 1);
- false -> {false, signature_mismatch}
- end.
-
--spec signer(binary(), [binary()]) -> notfound | binary().
-%% @doc Return first cert in list signing Cert, or notfound. NOTE:
-%% This is potentially expensive. It'd be more efficient to search for
-%% Cert.issuer in a list of Issuer.subject's. If so, maybe make the
-%% matching somewhat fuzzy unless that too is expensive.
-signer(_Cert, []) ->
- notfound;
-signer(Cert, [H|T]) ->
- case signed_by_p(Cert, H) of
- true ->
- H;
- false ->
- signer(Cert, T)
- end.
-
--spec encoded_tbs_cert(binary()) -> binary().
-%% Code from pubkey_cert:encoded_tbs_cert/1.
-encoded_tbs_cert(DerCert) ->
- {ok, PKIXCert} =
- 'OTP-PUB-KEY':decode_TBSCert_exclusive(DerCert),
- {'Certificate', {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} =
- PKIXCert,
- EncodedTBSCert.
-
--spec decode_cert(binary()) -> #'Certificate'{} | error.
-decode_cert(Der) ->
- case (catch public_key:pkix_decode_cert(Der, plain)) of
- #'Certificate'{} = Cert ->
- Cert;
- {'EXIT', Reason} ->
- lager:info("invalid certificate: ~p: ~p", [cert_string(Der), Reason]),
- dump_unparsable_cert(Der),
- error;
- Unknown ->
- lager:info("unknown error decoding cert: ~p: ~p",
- [cert_string(Der), Unknown]),
- error
- end.
-
-parsable_cert_p(Der) ->
- case decode_cert(Der) of
- error ->
- false;
- _ ->
- true
- end.
-
-%% @doc Is Cert signed by Issuer? Only verify that the signature
-%% matches and don't check things like Cert.issuer == Issuer.subject.
--spec signed_by_p(binary(), binary()) -> boolean().
-signed_by_p(SubjectDer, IssuerDer) ->
- SubjectCert = decode_cert(SubjectDer),
- IssuerCert = decode_cert(IssuerDer),
-
- case {SubjectCert, IssuerCert} of
- {#'Certificate'{},
- #'Certificate'{tbsCertificate =
- #'TBSCertificate'{subjectPublicKeyInfo =
- IssuerSPKI}}} ->
- %% Dig out digest, digest type and signature from subject cert and
- %% verify signature.
- case extract_verify_data(decode_cert(SubjectDer), SubjectDer) of
- error ->
- false;
- {ok, SubjectData} ->
- verify_sig(IssuerSPKI, SubjectData)
- end;
- _ ->
- false
- end.
-
-verify_sig(IssuerSPKI, {DigestOrPlainText, DigestType, Signature}) ->
- %% Dig out alg, params and key from issuer.
- #'SubjectPublicKeyInfo'{
- algorithm = #'AlgorithmIdentifier'{algorithm = Alg, parameters = Params},
- subjectPublicKey = {0, Key0}} = IssuerSPKI,
- KeyType = pubkey_cert_records:supportedPublicKeyAlgorithms(Alg),
- IssuerKey =
- case KeyType of
- 'RSAPublicKey' ->
- public_key:der_decode(KeyType, Key0);
- 'ECPoint' ->
- Point = #'ECPoint'{point = Key0},
- ECParams = public_key:der_decode('EcpkParameters', Params),
- {Point, ECParams};
- _ -> % FIXME: 'DSAPublicKey'
- lager:error("NIY: Issuer key type ~p", [KeyType]),
- false
- end,
- %% Verify the signature.
- public_key:verify(DigestOrPlainText, DigestType, Signature, IssuerKey).
-
--spec extract_verify_data(#'Certificate'{}, binary()) -> {ok, tuple()} | error.
-%% @doc Return DER encoded TBScertificate, digest type and signature.
-%% Code from pubkey_cert:extract_verify_data/2.
-extract_verify_data(Cert, DerCert) ->
- PlainText = encoded_tbs_cert(DerCert),
- {_, Sig} = Cert#'Certificate'.signature,
- SigAlgRecord = Cert#'Certificate'.signatureAlgorithm,
- SigAlg = SigAlgRecord#'AlgorithmIdentifier'.algorithm,
- try
- {DigestType, _} = public_key:pkix_sign_types(SigAlg),
- {ok, {PlainText, DigestType, Sig}}
- catch
- error:function_clause ->
- lager:debug("~p: signature algorithm not supported: ~p",
- [cert_string(DerCert), SigAlg]),
- error
- end.
-
-%% Precerts according to RFC6962.
-%%
-%% Submitted precerts have a special critical poison extension -- OID
-%% 1.3.6.1.4.1.11129.2.4.3, whose extnValue OCTET STRING contains
-%% ASN.1 NULL data (0x05 0x00).
-%%
-%% They are signed with either the CA cert that will sign the final
-%% cert or a Precertificate Signing Certificate directly signed by the
-%% CA cert that will sign the final cert. A Precertificate Signing
-%% Certificate has CA:true and Extended Key Usage: Certificate
-%% Transparency, OID 1.3.6.1.4.1.11129.2.4.4.
-%%
-%% PreCert in SignedCertificateTimestamp does _not_ contain the poison
-%% extension, nor does it have an issuer which is a Precertificate
-%% Signing Certificate. This means that we have to 1) remove the
-%% poison extension and 2) potentially change issuer and Authority Key
-%% Identifier. See RFC6962 Section 3.2.
-%%
-%% Changes in draft-ietf-trans-rfc6962-bis-??: TODO.
-
--spec detox_precert(binary(), binary(), binary()) -> {binary(), binary()}.
-%% @doc Return {DetoxedLeaf, IssuerPubKeyHash} where i) DetoxedLeaf is
-%% the tbsCertificate w/o poison and adjusted issuer and authkeyid;
-%% and ii) IssuerPubKeyHash is the hash over issuing cert's public
-%% key.
-detox_precert(LeafDer, ParentDer, GrandParentDer) ->
- Leaf = public_key:pkix_decode_cert(LeafDer, plain),
- Parent = public_key:pkix_decode_cert(ParentDer, plain),
- GrandParent = public_key:pkix_decode_cert(GrandParentDer, plain),
- DetoxedLeafTBS = remove_poison_ext(Leaf),
-
- %% If parent is a precert signing cert, change issuer and
- %% potential authority key id to refer to grandparent.
- {C, IssuerKeyHash} =
- case is_precert_signer(Parent) of
- true ->
- {set_issuer_and_authkeyid(DetoxedLeafTBS, Parent),
- extract_pub_key(GrandParent)};
- false ->
- {DetoxedLeafTBS, extract_pub_key(Parent)}
- end,
- {public_key:pkix_encode('TBSCertificate', C, plain),
- crypto:hash(sha256, public_key:pkix_encode(
- 'SubjectPublicKeyInfo', IssuerKeyHash, plain))}.
-
--spec extract_pub_key(#'Certificate'{}) -> #'SubjectPublicKeyInfo'{}.
-extract_pub_key(#'Certificate'{
- tbsCertificate = #'TBSCertificate'{
- subjectPublicKeyInfo = SPKI}}) ->
- SPKI.
-
--spec set_issuer_and_authkeyid(#'TBSCertificate'{}, #'Certificate'{}) ->
- #'TBSCertificate'{}.
-%% @doc Return Cert with issuer and AuthorityKeyIdentifier from Parent.
-set_issuer_and_authkeyid(TBSCert,
- #'Certificate'{
- tbsCertificate =
- #'TBSCertificate'{
- issuer = ParentIssuer,
- extensions = ParentExtensions}}) ->
- case pubkey_cert:select_extension(?'id-ce-authorityKeyIdentifier',
- ParentExtensions) of
- undefined ->
- lager:debug("setting issuer only", []),
- TBSCert#'TBSCertificate'{issuer = ParentIssuer};
- ParentAuthKeyExt ->
- NewExtensions =
- lists:map(
- fun(E) ->
- case E of
- #'Extension'{extnID =
- ?'id-ce-authorityKeyIdentifier'} ->
- lager:debug("swapping auth key id to ~p",
- [ParentAuthKeyExt]),
- ParentAuthKeyExt;
- _ -> E
- end
- end,
- TBSCert#'TBSCertificate'.extensions),
- lager:debug("setting issuer and auth key id", []),
- TBSCert#'TBSCertificate'{issuer = ParentIssuer,
- extensions = NewExtensions}
- end.
-
--spec is_precert_signer(#'Certificate'{}) -> boolean().
-is_precert_signer(#'Certificate'{tbsCertificate = TBSCert}) ->
- Extensions = pubkey_cert:extensions_list(TBSCert#'TBSCertificate'.extensions),
- %% NOTE: It's OK to look at only the first extension found since
- %% "A certificate MUST NOT include more than one instance of a
- %% particular extension." --RFC5280 Sect 4.2
- case pubkey_cert:select_extension(?'id-ce-extKeyUsage', Extensions) of
- #'Extension'{extnValue = Val} ->
- case 'OTP-PUB-KEY':decode('ExtKeyUsageSyntax', Val) of
- %% NOTE: We require that the poisoned OID is the
- %% _only_ extkeyusage present. RFC6962 Sect 3.1 is not
- %% really clear.
- {ok, [?CA_POISON_OID]} -> is_ca(TBSCert);
- _ -> false
- end;
- _ -> false
- end.
-
--spec is_ca(#'TBSCertificate'{}) -> boolean().
-is_ca(#'TBSCertificate'{extensions = Extensions}) ->
- case pubkey_cert:select_extension(?'id-ce-basicConstraints', Extensions) of
- #'Extension'{critical = true, extnValue = Val} ->
- case 'OTP-PUB-KEY':decode('BasicConstraints', Val) of
- {ok, {'BasicConstraints', true, _}} -> true;
- _ -> false
- end;
- _ -> false
- end.
-
--spec remove_poison_ext(#'Certificate'{}) -> #'TBSCertificate'{}.
-remove_poison_ext(#'Certificate'{tbsCertificate = TBSCert}) ->
- Extensions =
- filter(fun(E) -> not poisoned_leaf_p(E) end,
- pubkey_cert:extensions_list(TBSCert#'TBSCertificate'.extensions)),
- TBSCert#'TBSCertificate'{extensions = Extensions}.
-
--spec poisoned_leaf_p(binary()) -> boolean().
-poisoned_leaf_p(#'Extension'{extnID = ?LEAF_POISON_OID,
- critical = true,
- extnValue = ?LEAF_POISON_VAL}) ->
- true;
-poisoned_leaf_p(_) ->
- false.
-
-%%%% PEM files.
--spec ders_from_pemfiles(string(), [string()]) -> [binary()].
-ders_from_pemfiles(Dir, Filenames) ->
- lists:flatten(
- [ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]).
-
--spec ders_from_pemfile(string()) -> [binary()].
-ders_from_pemfile(Filename) ->
- lager:debug("reading PEM from ~s", [Filename]),
- PemBins = pems_from_file(Filename),
- Pems = case (catch public_key:pem_decode(PemBins)) of
- {'EXIT', Reason} ->
- lager:info("~p: invalid PEM-encoding: ~p", [Filename, Reason]),
- [];
- P -> P
- end,
- [der_from_pem(X) || X <- Pems].
-
--spec der_from_pem(binary()) -> binary().
-der_from_pem(Pem) ->
- case Pem of
- {_Type, Der, not_encrypted} ->
- case parsable_cert_p(Der) of
- true ->
- Der;
- false ->
- dump_unparsable_cert(Der),
- []
- end;
- Fail ->
- lager:info("ignoring PEM-encoded data: ~p~n", [Fail]),
- []
- end.
-
--spec pems_from_file(file:filename()) -> binary().
-pems_from_file(Filename) ->
- {ok, Pems} = file:read_file(Filename),
- Pems.
-
--spec dump_unparsable_cert(binary()) -> ok | {error, atom()} | not_logged.
-dump_unparsable_cert(Der) ->
- case application:get_env(catlfish, rejected_certs_path) of
- {ok, Directory} ->
- {NowMegaSec, NowSec, NowMicroSec} = now(),
- Filename =
- filename:join(Directory,
- io_lib:format("~p:~p.~p",
- [cert_string(Der),
- NowMegaSec * 1000 * 1000 + NowSec,
- NowMicroSec])),
- lager:info("dumping cert to ~p~n", [Filename]),
- file:write_file(Filename, Der);
- _ ->
- not_logged
- end.
-
-%%%%%%%%%%%%%%%%%%%%
-%% Testing private functions.
--include("x509_test.hrl").
-sign_test_() ->
- {setup,
- fun() -> ok end,
- fun(_) -> ok end,
- fun(_) -> [?_assertMatch(true, signed_by_p(?C0, ?C1))] end}.
-
-valid_cert_test_() ->
- {setup,
- fun() -> {read_pemfiles_from_dir("test/testdata/known_roots"),
- read_certs("test/testdata/chains")} end,
- fun(_) -> ok end,
- fun({KnownRoots, Chains}) ->
- [
- %% Self-signed but verified against itself so pass.
- %% Note that this certificate is rejected by the
- %% stricter OTP-PKIX.asn1 specification generating
- %% #'OTPCertificate'{}. The error is
- %% {invalid_choice_tag,{22,<<"US">>}}}} in
- %% 'OTP-PUB-KEY':Func('OTP-X520countryname', Value0).
- ?_assertMatch({true, _}, normalise_chain(nth(1, Chains),
- nth(1, Chains), 10)),
- %% Self-signed so fail.
- ?_assertMatch({false, root_unknown},
- normalise_chain(KnownRoots,
- nth(2, Chains), 10)),
- %% Leaf signed by known CA, pass.
- ?_assertMatch({true, _}, normalise_chain(KnownRoots,
- nth(3, Chains), 10)),
- %% Proper 3-depth chain with root in KnownRoots, pass.
- %% Bug CATLFISH-19 --> [info] rejecting "3ee62cb678014c14d22ebf96f44cc899adea72f1": chain_broken
- %% leaf sha1: 3ee62cb678014c14d22ebf96f44cc899adea72f1
- %% leaf Subject: C=KR, O=Government of Korea, OU=Group of Server, OU=\xEA\xB5\x90\xEC\x9C\xA1\xEA\xB3\xBC\xED\x95\x99\xEA\xB8\xB0\xEC\x88\xA0\xEB\xB6\x80, CN=www.berea.ac.kr, CN=haksa.bits.ac.kr
- ?_assertMatch({true, _}, normalise_chain(KnownRoots,
- nth(4, Chains), 3)),
- %% Verify against self, pass.
- %% Bug CATLFISH-??, can't handle issuer keytype ECPoint.
- %% Issuer sha1: 6969562e4080f424a1e7199f14baf3ee58ab6abb
- ?_assertMatch(true, signed_by_p(hd(nth(5, Chains)),
- hd(nth(5, Chains)))),
- %% Unsupported signature algorithm MD2-RSA, fail.
- %% Signature Algorithm: md2WithRSAEncryption
- %% CA cert with sha1 96974cd6b663a7184526b1d648ad815cf51e801a
- ?_assertMatch(false, signed_by_p(hd(nth(6, Chains)),
- hd(nth(6, Chains)))),
-
- %% Supposedly problematic chains from Google Aviator, fatal.
- %% 00459972: asn1: syntax error: sequence truncated
- ?_assertMatch({true, _}, normalise_chain(nth(7, Chains),
- nth(7, Chains), 10)),
- %% 1402673: x509: RSA modulus is not a positive number
- ?_assertMatch({true, _}, normalise_chain(nth(8, Chains),
- nth(8, Chains), 10)),
- %% 1345105: asn1: syntax error: IA5String contains invalid character
- ?_assertMatch({true, _}, normalise_chain(nth(9, Chains),
- nth(9, Chains), 10)),
- %% 1557693: asn1: structure error: integer too large
- ?_assertMatch({true, _}, normalise_chain(nth(10, Chains),
- nth(10, Chains), 10)),
-
- %% Supposedly problematic chains from Google Aviator, non-fatal.
- %% 16800: x509: negative serial number
- %% a.pem
- ?_assertMatch({true, _}, normalise_chain(nth(11, Chains),
- nth(11, Chains), 10)),
- %% 22487: x509: unhandled critical extension ([2 5 29 32])
- %% b.pem
- ?_assertMatch({true, _}, normalise_chain(nth(12, Chains),
- nth(12, Chains), 10)),
- %% 5198: x509: certificate contained IP address of length 8
- %% c.pem
- ?_assertMatch({true, _}, normalise_chain(nth(13, Chains),
- nth(13, Chains), 10))
- ] end}.
-
-chain_test_() ->
- {setup,
- fun() -> {?C0, ?C1} end,
- fun(_) -> ok end,
- fun({C0, C1}) -> chain_test(C0, C1) end}.
-
-chain_test(C0, C1) ->
- [
- %% Root not in chain but in trust store.
- ?_assertEqual({true, [C1]}, normalise_chain([C1], [C0], 10)),
- ?_assertEqual({true, [C1]}, normalise_chain([C1], [C0], 2)),
- %% Chain too long.
- ?_assertMatch({false, chain_too_long}, normalise_chain([C1], [C0], 1)),
- %% Root in chain and in trust store.
- ?_assertEqual({true, [C1]}, normalise_chain([C1], [C0, C1], 2)),
- %% Chain too long.
- ?_assertMatch({false, chain_too_long}, normalise_chain([C1], [C0, C1], 1)),
- %% Root not in trust store.
- ?_assertMatch({false, root_unknown}, normalise_chain([], [C0, C1], 10)),
- %% Selfsigned. Actually OK.
- ?_assertMatch({true, [C0]}, normalise_chain([C0], [C0], 10)),
- ?_assertMatch({true, [C0]}, normalise_chain([C0], [C0], 1)),
- %% Max chain length 0 is not OK.
- ?_assertMatch({false, chain_too_long}, normalise_chain([C0], [C0], 0))
- ].
-
-%%-spec read_certs(file:filename()) -> [string:string()].
--spec read_certs(file:filename()) -> [[binary()]].
-read_certs(Dir) ->
- {ok, Fnames} = file:list_dir(Dir),
- PemBins =
- [Pems || {ok, Pems} <-
- [file:read_file(filename:join(Dir, F)) ||
- F <- lists:sort(
- lists:filter(
- fun(FN) -> string:equal(
- ".pem", filename:extension(FN))
- end,
- Fnames))]],
- PemEntries = [public_key:pem_decode(P) || P <- PemBins],
- lists:map(fun(L) -> [Der || {'Certificate', Der, not_encrypted} <- L] end,
- PemEntries).
diff --git a/src/x509_test.hrl b/src/x509_test.hrl
deleted file mode 100644
index 93803a2..0000000
--- a/src/x509_test.hrl
+++ /dev/null
@@ -1,111 +0,0 @@
-%% C1 signs C0
-
--define(C0,
- <<48,130,5,5,48,130,3,237,160,3,2,1,2,2,17,0,223,204,174,11,223,131,19,38,89,
- 67,221,26,255,213,96,72,48,13,6,9,42,134,72,134,247,13,1,1,5,5,0,48,54,49,11,
- 48,9,6,3,85,4,6,19,2,78,76,49,15,48,13,6,3,85,4,10,19,6,84,69,82,69,78,65,49,
- 22,48,20,6,3,85,4,3,19,13,84,69,82,69,78,65,32,83,83,76,32,67,65,48,30,23,13,
- 49,52,48,55,48,51,48,48,48,48,48,48,90,23,13,49,55,48,55,48,50,50,51,53,57,
- 53,57,90,48,129,188,49,11,48,9,6,3,85,4,6,19,2,83,69,49,15,48,13,6,3,85,4,17,
- 19,6,49,49,52,32,50,55,49,24,48,22,6,3,85,4,8,12,15,83,116,111,99,107,104,
- 111,108,109,115,32,108,195,164,110,49,18,48,16,6,3,85,4,7,19,9,83,116,111,99,
- 107,104,111,108,109,49,26,48,24,6,3,85,4,9,12,17,86,97,108,104,97,108,108,97,
- 118,195,164,103,101,110,32,55,57,49,37,48,35,6,3,85,4,10,12,28,75,117,110,
- 103,108,105,103,97,32,84,101,107,110,105,115,107,97,32,72,195,182,103,115,
- 107,111,108,97,110,49,12,48,10,6,3,85,4,11,19,3,73,84,65,49,29,48,27,6,3,85,
- 4,3,19,20,110,115,45,118,105,112,45,48,49,46,115,121,115,46,107,116,104,46,
- 115,101,48,130,1,34,48,13,6,9,42,134,72,134,247,13,1,1,1,5,0,3,130,1,15,0,48,
- 130,1,10,2,130,1,1,0,166,33,67,34,241,62,140,127,233,46,254,30,221,209,182,
- 44,139,207,232,43,36,138,78,153,18,175,55,128,110,229,13,134,105,211,249,84,
- 204,189,189,176,231,127,10,112,242,167,58,17,70,21,17,119,47,55,218,214,62,
- 244,188,171,139,89,183,203,169,1,206,93,173,24,171,76,219,154,245,77,20,127,
- 84,182,27,105,154,254,238,143,154,60,184,174,228,77,35,69,216,92,218,90,121,
- 183,130,14,144,114,76,88,0,4,181,120,111,112,141,27,223,73,36,176,205,188,52,
- 123,252,183,182,248,77,250,252,94,175,54,87,242,136,152,225,209,86,154,217,
- 143,69,255,113,19,95,11,145,11,172,227,80,9,60,177,254,225,32,151,110,155,
- 137,159,133,104,222,68,228,173,154,118,49,154,56,109,158,143,7,252,182,231,
- 177,104,192,211,168,146,68,6,253,8,58,192,229,254,245,101,191,204,112,163,
- 241,26,168,125,161,33,154,152,45,87,35,164,150,2,100,13,50,205,241,159,60,
- 235,214,104,248,219,118,77,224,242,61,100,107,248,140,69,73,86,66,92,9,196,
- 47,74,130,49,128,235,28,12,217,40,153,3,2,3,1,0,1,163,130,1,133,48,130,1,129,
- 48,31,6,3,85,29,35,4,24,48,22,128,20,12,189,147,104,12,243,222,171,163,73,
- 107,43,55,87,71,234,144,227,185,237,48,29,6,3,85,29,14,4,22,4,20,220,21,64,
- 117,11,185,137,252,236,135,28,40,51,178,241,101,243,183,42,113,48,14,6,3,85,
- 29,15,1,1,255,4,4,3,2,5,160,48,12,6,3,85,29,19,1,1,255,4,2,48,0,48,29,6,3,85,
- 29,37,4,22,48,20,6,8,43,6,1,5,5,7,3,1,6,8,43,6,1,5,5,7,3,2,48,34,6,3,85,29,
- 32,4,27,48,25,48,13,6,11,43,6,1,4,1,178,49,1,2,2,29,48,8,6,6,103,129,12,1,2,
- 2,48,58,6,3,85,29,31,4,51,48,49,48,47,160,45,160,43,134,41,104,116,116,112,
- 58,47,47,99,114,108,46,116,99,115,46,116,101,114,101,110,97,46,111,114,103,
- 47,84,69,82,69,78,65,83,83,76,67,65,46,99,114,108,48,109,6,8,43,6,1,5,5,7,1,
- 1,4,97,48,95,48,53,6,8,43,6,1,5,5,7,48,2,134,41,104,116,116,112,58,47,47,99,
- 114,116,46,116,99,115,46,116,101,114,101,110,97,46,111,114,103,47,84,69,82,
- 69,78,65,83,83,76,67,65,46,99,114,116,48,38,6,8,43,6,1,5,5,7,48,1,134,26,104,
- 116,116,112,58,47,47,111,99,115,112,46,116,99,115,46,116,101,114,101,110,97,
- 46,111,114,103,48,51,6,3,85,29,17,4,44,48,42,130,20,110,115,45,118,105,112,
- 45,48,49,46,115,121,115,46,107,116,104,46,115,101,130,6,107,116,104,46,115,
- 101,130,10,119,119,119,46,107,116,104,46,115,101,48,13,6,9,42,134,72,134,247,
- 13,1,1,5,5,0,3,130,1,1,0,174,206,231,39,9,125,240,24,18,81,52,193,118,3,201,
- 18,26,144,85,135,110,139,142,210,95,67,250,122,79,51,63,41,43,50,30,44,70,
- 120,235,251,2,43,209,62,53,221,192,144,34,41,47,72,89,126,217,82,42,192,224,
- 222,106,105,2,3,33,48,185,246,253,59,177,29,233,76,148,74,157,192,222,255,
- 237,248,141,60,108,236,5,139,184,124,26,169,25,215,91,35,175,81,89,255,245,
- 39,52,118,93,234,59,234,68,58,80,44,32,220,72,171,97,252,59,206,151,149,243,
- 87,45,245,252,88,112,81,50,105,184,134,215,201,123,134,102,4,155,253,210,120,
- 167,145,13,198,22,106,233,62,132,31,228,131,185,115,8,157,170,100,82,36,254,
- 196,12,16,73,136,236,155,232,54,224,31,173,102,48,126,191,97,27,127,66,107,3,
- 80,209,183,134,158,80,0,43,18,94,83,98,2,213,38,229,245,199,1,187,82,69,69,
- 203,12,69,58,254,99,194,31,27,232,13,150,30,90,254,240,201,247,195,39,72,150,
- 105,241,6,104,213,53,195,138,113,36,251,107,82,29,84,121,138,41,245,160,225,
- 245,15,173>>).
--define(C1,
- <<48,130,4,152,48,130,3,128,160,3,2,1,2,2,16,75,200,20,3,47,7,250,106,164,240,
- 218,41,223,97,121,186,48,13,6,9,42,134,72,134,247,13,1,1,5,5,0,48,129,151,49,
- 11,48,9,6,3,85,4,6,19,2,85,83,49,11,48,9,6,3,85,4,8,19,2,85,84,49,23,48,21,6,
- 3,85,4,7,19,14,83,97,108,116,32,76,97,107,101,32,67,105,116,121,49,30,48,28,
- 6,3,85,4,10,19,21,84,104,101,32,85,83,69,82,84,82,85,83,84,32,78,101,116,119,
- 111,114,107,49,33,48,31,6,3,85,4,11,19,24,104,116,116,112,58,47,47,119,119,
- 119,46,117,115,101,114,116,114,117,115,116,46,99,111,109,49,31,48,29,6,3,85,
- 4,3,19,22,85,84,78,45,85,83,69,82,70,105,114,115,116,45,72,97,114,100,119,97,
- 114,101,48,30,23,13,48,57,48,53,49,56,48,48,48,48,48,48,90,23,13,50,48,48,53,
- 51,48,49,48,52,56,51,56,90,48,54,49,11,48,9,6,3,85,4,6,19,2,78,76,49,15,48,
- 13,6,3,85,4,10,19,6,84,69,82,69,78,65,49,22,48,20,6,3,85,4,3,19,13,84,69,82,
- 69,78,65,32,83,83,76,32,67,65,48,130,1,34,48,13,6,9,42,134,72,134,247,13,1,1,
- 1,5,0,3,130,1,15,0,48,130,1,10,2,130,1,1,0,195,227,72,196,47,92,193,203,169,
- 153,253,27,162,131,93,138,61,173,58,208,226,164,67,31,77,14,254,53,37,48,165,
- 105,27,196,232,229,193,143,84,126,225,106,162,154,92,92,222,61,252,2,206,150,
- 184,95,143,131,91,204,96,64,144,248,228,182,58,37,156,95,20,81,236,177,231,
- 175,158,80,161,49,85,199,2,189,172,82,138,127,53,142,130,250,132,173,21,254,
- 162,127,131,16,58,85,83,148,44,1,22,116,148,84,99,40,163,242,91,41,61,148,
- 136,128,32,226,20,89,33,25,180,164,152,225,96,230,242,235,162,128,131,67,224,
- 173,104,243,121,25,139,104,67,81,63,138,155,65,133,12,53,140,93,181,241,182,
- 229,167,195,131,181,107,35,111,212,165,235,80,229,148,241,74,95,238,39,75,20,
- 18,21,36,76,13,207,98,141,183,0,33,173,58,50,15,88,11,95,30,155,209,223,157,
- 142,169,25,53,80,47,65,169,173,59,198,224,69,178,83,57,127,33,191,34,26,135,
- 92,52,174,82,111,7,125,162,11,78,159,43,121,166,125,19,221,245,127,131,124,
- 47,90,93,119,120,120,145,160,20,191,125,2,3,1,0,1,163,130,1,62,48,130,1,58,
- 48,31,6,3,85,29,35,4,24,48,22,128,20,161,114,95,38,27,40,152,67,149,93,7,55,
- 213,133,150,157,75,210,195,69,48,29,6,3,85,29,14,4,22,4,20,12,189,147,104,12,
- 243,222,171,163,73,107,43,55,87,71,234,144,227,185,237,48,14,6,3,85,29,15,1,
- 1,255,4,4,3,2,1,6,48,18,6,3,85,29,19,1,1,255,4,8,48,6,1,1,255,2,1,0,48,24,6,
- 3,85,29,32,4,17,48,15,48,13,6,11,43,6,1,4,1,178,49,1,2,2,29,48,68,6,3,85,29,
- 31,4,61,48,59,48,57,160,55,160,53,134,51,104,116,116,112,58,47,47,99,114,108,
- 46,117,115,101,114,116,114,117,115,116,46,99,111,109,47,85,84,78,45,85,83,69,
- 82,70,105,114,115,116,45,72,97,114,100,119,97,114,101,46,99,114,108,48,116,6,
- 8,43,6,1,5,5,7,1,1,4,104,48,102,48,61,6,8,43,6,1,5,5,7,48,2,134,49,104,116,
- 116,112,58,47,47,99,114,116,46,117,115,101,114,116,114,117,115,116,46,99,111,
- 109,47,85,84,78,65,100,100,84,114,117,115,116,83,101,114,118,101,114,95,67,
- 65,46,99,114,116,48,37,6,8,43,6,1,5,5,7,48,1,134,25,104,116,116,112,58,47,47,
- 111,99,115,112,46,117,115,101,114,116,114,117,115,116,46,99,111,109,48,13,6,
- 9,42,134,72,134,247,13,1,1,5,5,0,3,130,1,1,0,78,35,238,72,156,246,133,139,
- 113,196,10,110,115,147,114,192,58,142,128,138,217,179,202,178,212,1,156,40,
- 207,242,92,14,33,68,147,11,182,26,33,227,152,1,148,14,103,73,129,30,190,61,
- 13,78,96,218,239,160,49,78,149,239,243,221,122,90,130,32,67,182,161,99,67,
- 179,80,105,67,98,75,86,98,176,52,138,185,19,67,89,147,236,20,121,136,243,72,
- 147,232,157,201,250,135,114,12,107,86,160,195,21,141,104,165,135,31,113,45,
- 230,90,109,60,105,113,64,4,85,220,160,67,148,32,69,56,120,215,189,138,216,57,
- 198,223,9,183,90,154,169,3,184,40,16,120,205,191,1,27,90,17,62,56,244,216,27,
- 52,121,207,51,210,1,253,172,152,205,109,71,17,144,76,187,185,91,216,112,231,
- 213,175,182,204,196,134,230,117,192,158,41,182,43,15,42,165,105,2,13,227,233,
- 162,180,93,192,243,206,44,106,133,56,118,97,198,73,130,171,81,179,130,166,
- 185,65,152,40,152,251,107,254,138,22,255,49,126,84,71,168,60,220,67,38,169,
- 155,5,183,158,192,52,67,145,48,212,50,195,17,90,225>>).