summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/catlfish.erl47
-rw-r--r--src/catlfish.hrl4
-rw-r--r--src/catlfish_app.erl10
-rw-r--r--src/catlfish_web.erl32
-rw-r--r--src/v1.erl12
-rw-r--r--src/x509.erl199
6 files changed, 175 insertions, 129 deletions
diff --git a/src/catlfish.erl b/src/catlfish.erl
index 83ca3db..3956eec 100644
--- a/src/catlfish.erl
+++ b/src/catlfish.erl
@@ -4,8 +4,8 @@
-module(catlfish).
-export([add_chain/2, entries/2, entry_and_proof/2]).
-export([known_roots/0, update_known_roots/0]).
+-export([init_cache_table/0]).
-include_lib("eunit/include/eunit.hrl").
--include("catlfish.hrl").
-define(PROTOCOL_VERSION, 0).
@@ -133,6 +133,14 @@ entry_and_proof(Index, TreeSize) ->
{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]).
+
%% Private functions.
unpack_entry(Entry) ->
<<Timestamp:64, LogEntry/binary>> = Entry,
@@ -183,37 +191,50 @@ known_roots(Directory, CacheUsage) ->
use_cache ->
case ets:lookup(?CACHE_TABLE, ?ROOTS_CACHE_KEY) of
[] ->
- read_files_and_udpate_table(Directory);
+ read_files_and_update_table(Directory);
[{roots, DerList}] ->
DerList
end;
update_tab ->
- read_files_and_udpate_table(Directory)
+ read_files_and_update_table(Directory)
end.
-read_files_and_udpate_table(Directory) ->
- L = x509:read_pemfiles_from_dir(Directory),
- true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, L}),
- L.
+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").
+-define(PEMFILES_DIR_OK, "test/testdata/known_roots").
+-define(PEMFILES_DIR_NONEXISTENT, "test/testdata/nonexistent-dir").
read_pemfiles_test_() ->
{setup,
- fun() -> {known_roots(?PEMFILES_DIR_OK, use_cache),
- known_roots(?PEMFILES_DIR_OK, use_cache)}
+ 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(7, length(L)),
+ [?_assertMatch(4, length(L)),
?_assertEqual(L, LCached)]
end}.
read_pemfiles_fail_test_() ->
{setup,
- fun() -> known_roots(?PEMFILES_DIR_NONEXISTENT, use_cache) end,
+ 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.hrl b/src/catlfish.hrl
deleted file mode 100644
index 46e882b..0000000
--- a/src/catlfish.hrl
+++ /dev/null
@@ -1,4 +0,0 @@
-%%% Copyright (c) 2014, NORDUnet A/S.
-%%% See LICENSE for licensing information.
-
--define(CACHE_TABLE, catlfish_cache).
diff --git a/src/catlfish_app.erl b/src/catlfish_app.erl
index e24a1bb..56f6cc2 100644
--- a/src/catlfish_app.erl
+++ b/src/catlfish_app.erl
@@ -8,20 +8,12 @@
%% Application callbacks
-export([start/2, stop/1]).
--include("catlfish.hrl").
-
%% ===================================================================
%% Application callbacks
%% ===================================================================
start(normal, Args) ->
- case ets:info(?CACHE_TABLE) of
- undefined ->
- ok;
- _ ->
- ets:delete(?CACHE_TABLE)
- end,
- ets:new(?CACHE_TABLE, [set, public, named_table]),
+ catlfish:init_cache_table(),
catlfish_sup:start_link(Args).
stop(_State) ->
diff --git a/src/catlfish_web.erl b/src/catlfish_web.erl
index 9869b21..5ee5743 100644
--- a/src/catlfish_web.erl
+++ b/src/catlfish_web.erl
@@ -11,15 +11,31 @@ start(Options, Module) ->
end,
mochiweb_http:start([{name, Module}, {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(),
- lager:debug("GET ~p ~p", [Path, Query]),
- Result = Module:request(get, Path, Query),
+ {_, 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 ->
@@ -29,8 +45,16 @@ loop(Req, Module) ->
end;
'POST' ->
Body = Req:recv_body(),
- lager:debug("POST ~p ~p", [Path, Body]),
- Result = Module:request(post, Path, 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 ->
diff --git a/src/v1.erl b/src/v1.erl
index d9796fa..006990d 100644
--- a/src/v1.erl
+++ b/src/v1.erl
@@ -39,16 +39,8 @@ request(post, "ct/v1/add-pre-chain", _Input) ->
niy();
request(get, "ct/v1/get-sth", _Query) ->
- { Treesize,
- Timestamp,
- Roothash,
- Signature} = plop:sth(),
- R = [{tree_size, Treesize},
- {timestamp, Timestamp},
- {sha256_root_hash, base64:encode(Roothash)},
- {tree_head_signature, base64:encode(
- plop:serialise(Signature))}],
- success({R});
+ R = plop:sth(),
+ success(R);
request(get, "ct/v1/get-sth-consistency", Query) ->
case lists:sort(Query) of
diff --git a/src/x509.erl b/src/x509.erl
index b0363cd..5a0e871 100644
--- a/src/x509.erl
+++ b/src/x509.erl
@@ -2,14 +2,14 @@
%%% See LICENSE for licensing information.
-module(x509).
--export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1]).
+-export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1,
+ self_signed/1]).
-include_lib("public_key/include/public_key.hrl").
-include_lib("eunit/include/eunit.hrl").
-type reason() :: {chain_too_long |
root_unknown |
- chain_broken |
signature_mismatch |
encoding_invalid}.
@@ -28,10 +28,15 @@ normalise_chain(AcceptableRootCerts, CertChain) ->
%%%%%%%%%%%%%%%%%%%%
%% @doc Verify that the leaf cert or precert has a valid chain back to
-%% an acceptable root cert. Order of certificates in second argument
-%% is: leaf cert in head, chain in tail. Order of first argument is
-%% irrelevant.
-
+%% 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 is the empty list when the root of the chain is found
+%% amongst the acceptable root certs. Otherwise it contains exactly
+%% one element, a CA cert from the acceptable root certs signing the
+%% root of the chain.
-spec valid_chain_p([binary()], [binary()], integer()) ->
{false, reason()} | {true, list()}.
valid_chain_p(_, _, MaxChainLength) when MaxChainLength =< 0 ->
@@ -56,106 +61,103 @@ valid_chain_p(AcceptableRootCerts, [TopCert], MaxChainLength) ->
valid_chain_p(AcceptableRootCerts, [BottomCert|Rest], MaxChainLength) ->
case signed_by_p(BottomCert, hd(Rest)) of
true -> valid_chain_p(AcceptableRootCerts, Rest, MaxChainLength - 1);
- Err -> Err
+ false -> {false, signature_mismatch}
end.
-%% @doc Return first cert in list signing Cert, or notfound.
+%% @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.
-spec signer(binary(), [binary()]) -> notfound | binary().
signer(_Cert, []) ->
notfound;
signer(Cert, [H|T]) ->
+ lager:debug("Is ~p signed by ~p?", [cert_string(Cert), cert_string(H)]),
case signed_by_p(Cert, H) of
- true -> H;
- {false, _} -> signer(Cert, T)
+ true ->
+ lager:debug("~p is signed by ~p",
+ [cert_string(Cert), cert_string(H)]),
+ H;
+ false ->
+ signer(Cert, T)
end.
-%% encoded_tbs_cert: verbatim from pubkey_cert.erl
-encoded_tbs_cert(Cert) ->
+%% Code from pubkey_cert:encoded_tbs_cert/1.
+encoded_tbs_cert(DerCert) ->
{ok, PKIXCert} =
- 'OTP-PUB-KEY':decode_TBSCert_exclusive(Cert),
- {'Certificate',
- {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} = PKIXCert,
+ 'OTP-PUB-KEY':decode_TBSCert_exclusive(DerCert),
+ {'Certificate', {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} =
+ PKIXCert,
EncodedTBSCert.
-%% extract_verify_data: close to pubkey_cert:extract_verify_data/2
+%% Code from pubkey_cert:extract_verify_data/2.
+-spec verifydata_from_cert(#'Certificate'{}, binary()) -> {ok, tuple()} | error.
verifydata_from_cert(Cert, DerCert) ->
PlainText = encoded_tbs_cert(DerCert),
{_, Sig} = Cert#'Certificate'.signature,
SigAlgRecord = Cert#'Certificate'.signatureAlgorithm,
SigAlg = SigAlgRecord#'AlgorithmIdentifier'.algorithm,
- {DigestType,_} = public_key:pkix_sign_types(SigAlg),
- {PlainText, DigestType, Sig}.
-
-verify(Cert, DerCert,
- #'Certificate'{
- tbsCertificate = #'TBSCertificate'{
- subjectPublicKeyInfo = IssuerSPKI}}) ->
- {DigestOrPlainText, DigestType, Signature} =
- verifydata_from_cert(Cert, DerCert),
+ lager:debug("SigAlg: ~p", [SigAlg]),
+ try
+ {DigestType, _} = public_key:pkix_sign_types(SigAlg),
+ {ok, {PlainText, DigestType, Sig}}
+ catch
+ error:function_clause ->
+ lager:debug("signature algorithm not supported: ~p", [SigAlg]),
+ error
+ end.
+
+%% @doc Verify that Cert/DerCert is signed by Issuer.
+-spec verify_sig(#'Certificate'{}, binary(), #'Certificate'{}) -> boolean().
+verify_sig(Cert, DerCert, % Certificate to verify.
+ #'Certificate'{ % Issuer.
+ tbsCertificate = #'TBSCertificate'{
+ subjectPublicKeyInfo = IssuerSPKI}}) ->
+ %% Dig out digest, digest type and signature from Cert/DerCert.
+ case verifydata_from_cert(Cert, DerCert) of
+ error -> false;
+ {ok, Tuple} -> verify_sig2(IssuerSPKI, Tuple)
+ end.
+
+verify_sig2(IssuerSPKI, {DigestOrPlainText, DigestType, Signature}) ->
+ %% Dig out issuer key from issuer cert.
#'SubjectPublicKeyInfo'{
algorithm = #'AlgorithmIdentifier'{algorithm = Alg, parameters = Params},
subjectPublicKey = {0, Key0}} = IssuerSPKI,
KeyType = pubkey_cert_records:supportedPublicKeyAlgorithms(Alg),
-
- %% public_key:pem_entry_decode()
+ lager:debug("Alg: ~p", [Alg]),
+ lager:debug("Params: ~p", [Params]),
+ lager:debug("KeyType: ~p", [KeyType]),
+ lager:debug("Key0: ~p", [Key0]),
IssuerKey =
case KeyType of
'RSAPublicKey' ->
public_key:der_decode(KeyType, Key0);
- 'DSAPublicKey' ->
- {params, DssParams} = public_key:der_decode('DSAParams', Params),
- {public_key:der_decode(KeyType, Key0), DssParams};
'ECPoint' ->
- public_key:der_decode(KeyType, Key0)
+ 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,
+
+ lager:debug("DigestOrPlainText: ~p", [DigestOrPlainText]),
+ lager:debug("DigestType: ~p", [DigestType]),
+ lager:debug("Signature: ~p", [Signature]),
+ lager:debug("IssuerKey: ~p", [IssuerKey]),
+
+ %% Verify the signature.
public_key:verify(DigestOrPlainText, DigestType, Signature, IssuerKey).
--spec signed_by_p(binary(), binary()) -> true | {false, reason()}.
+%% @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(DerCert, IssuerDerCert) when is_binary(DerCert),
is_binary(IssuerDerCert) ->
- Cert = public_key:pkix_decode_cert(DerCert, plain),
- TBSCert = Cert#'Certificate'.tbsCertificate,
- IssuerCert = public_key:pkix_decode_cert(IssuerDerCert, plain),
- IssuerTBSCert = IssuerCert#'Certificate'.tbsCertificate,
- case pubkey_cert:is_issuer(TBSCert#'TBSCertificate'.issuer,
- IssuerTBSCert#'TBSCertificate'.subject) of
- false ->
- {false, chain_broken};
- true -> % Verify signature.
- case verify(Cert, DerCert, IssuerCert) of
- false -> {false, signature_mismatch};
- true -> true
- end
- end;
-signed_by_p(#'OTPCertificate'{} = Cert,
- #'OTPCertificate'{} = IssuerCert) ->
- %% FIXME: Validate presence and contents (against constraints) of
- %% names (subject, subjectAltName, emailAddress) too?
- case (catch public_key:pkix_is_issuer(Cert, IssuerCert)) of
- {'EXIT', Reason} ->
- lager:info("invalid certificate: ~p: ~p",
- [cert_string(Cert), Reason]),
- {false, encoding_invalid};
- true ->
- %% Cert.issuer does match IssuerCert.subject. Now verify
- %% the signature.
- case public_key:pkix_verify(Cert, public_key(IssuerCert)) of
- true -> true;
- false -> {false, signature_mismatch}
- end;
- false ->
- {false, chain_broken}
- end.
-
--spec public_key(binary() | #'OTPCertificate'{}) -> public_key:public_key().
-public_key(CertDer) when is_binary(CertDer) ->
- public_key(public_key:pkix_decode_cert(CertDer, otp));
-public_key(#'OTPCertificate'{
- tbsCertificate =
- #'OTPTBSCertificate'{subjectPublicKeyInfo =
- #'OTPSubjectPublicKeyInfo'{
- subjectPublicKey = Key}}}) ->
- Key.
+ verify_sig(public_key:pkix_decode_cert(DerCert, plain),
+ DerCert,
+ public_key:pkix_decode_cert(IssuerDerCert, plain)).
cert_string(Der) ->
mochihex:to_hex(crypto:hash(sha, Der)).
@@ -173,6 +175,10 @@ parsable_cert_p(Der) ->
false
end.
+-spec self_signed([binary()]) -> [binary()].
+self_signed(L) ->
+ lists:filter(fun(Cert) -> signed_by_p(Cert, Cert) end, L).
+
%%%%%%%%%%%%%%%%%%%%
%% Precertificates according to draft-ietf-trans-rfc6962-bis-04.
@@ -222,6 +228,7 @@ ders_from_pemfiles(Dir, Filenames) ->
[ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]).
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} ->
@@ -279,26 +286,42 @@ sign_test_() ->
valid_cert_test_() ->
{setup,
- fun() -> {read_pemfiles_from_dir("../test/testdata/known_roots"),
- read_certs("../test/testdata/chains")} end,
+ fun() -> {read_pemfiles_from_dir("test/testdata/known_roots"),
+ read_certs("test/testdata/chains")} end,
fun(_) -> ok end,
fun({KnownRoots, Chains}) ->
[
- %% self-signed, not a valid OTPCertificate:
+ %% Self-signed but verified against itself so pass.
+ %% Not a valid OTPCertificate:
%% {error,{asn1,{invalid_choice_tag,{22,<<"US">>}}}}
%% 'OTP-PUB-KEY':Func('OTP-X520countryname', Value0)
- %% FIXME: this doesn't make much sense -- is my environment borked?
- ?_assertMatch({true, _},
- valid_chain_p(lists:nth(1, Chains),
- lists:nth(1, Chains), 10)),
- %% self-signed
+ %% FIXME: This error doesn't make much sense -- is my
+ %% environment borked?
+ ?_assertMatch({true, _}, valid_chain_p(lists:nth(1, Chains),
+ lists:nth(1, Chains), 10)),
+ %% Self-signed so fail.
?_assertMatch({false, root_unknown},
valid_chain_p(KnownRoots,
lists:nth(2, Chains), 10)),
- %% leaf signed by known CA
- ?_assertMatch({true, _},
- valid_chain_p(KnownRoots,
- lists:nth(3, Chains), 10))
+ %% Leaf signed by known CA, pass.
+ ?_assertMatch({true, _}, valid_chain_p(KnownRoots,
+ lists: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, _}, valid_chain_p(KnownRoots,
+ lists: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(lists:nth(5, Chains)),
+ hd(lists:nth(5, Chains)))),
+ %% Unsupported signature algorithm MD2-RSA, fail.
+ %% Signature Algorithm: md2WithRSAEncryption
+ %% CA cert with sha1 96974cd6b663a7184526b1d648ad815cf51e801a
+ ?_assertMatch(false, signed_by_p(hd(lists:nth(6, Chains)),
+ hd(lists:nth(6, Chains))))
] end}.
chain_test_() ->
@@ -320,8 +343,6 @@ chain_test(C0, C1) ->
?_assertMatch({false, chain_too_long}, valid_chain_p([C1], [C0, C1], 1)),
%% Root not in trust store.
?_assertMatch({false, root_unknown}, valid_chain_p([], [C0, C1], 10)),
- %% Invalid signer.
- ?_assertMatch({false, chain_broken}, valid_chain_p([C0], [C1, C0], 10)),
%% Selfsigned. Actually OK.
?_assertMatch({true, []}, valid_chain_p([C0], [C0], 10)),
?_assertMatch({true, []}, valid_chain_p([C0], [C0], 1)),