diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/catlfish.erl | 415 | ||||
-rw-r--r-- | src/catlfish_app.erl | 20 | ||||
-rw-r--r-- | src/catlfish_sup.erl | 53 | ||||
-rw-r--r-- | src/catlfish_web.erl | 74 | ||||
-rw-r--r-- | src/ratelimit.erl | 87 | ||||
-rw-r--r-- | src/v1.erl | 182 | ||||
-rw-r--r-- | src/x509.erl | 514 | ||||
-rw-r--r-- | src/x509_test.hrl | 111 |
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>>). |