summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Nordberg <linus@nordberg.se>2014-11-18 11:21:15 +0100
committerLinus Nordberg <linus@nordberg.se>2014-11-18 11:23:59 +0100
commit5847ef948baeadf4582234f4c3e7ecff2791b4cf (patch)
treee25cbcfb6e570a113a069c26c1b81d5117472229
parent293b1df48c6d376dee0f1f2512486b8a68488a9c (diff)
Verify certificates by decoding them as 'plain' certs rather than 'otp.
OTP cert validation is too strict. Let's see if this is forgiving enough for our needs. Also, move all cert reading from disk to x509.erl.
-rw-r--r--src/catlfish.erl56
-rw-r--r--src/x509.erl212
2 files changed, 201 insertions, 67 deletions
diff --git a/src/catlfish.erl b/src/catlfish.erl
index 98ec4dd..83ca3db 100644
--- a/src/catlfish.erl
+++ b/src/catlfish.erl
@@ -177,66 +177,24 @@ known_roots() ->
undefined -> []
end.
--spec known_roots(file:filename(), use_cache|update_tab) -> list().
+-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_pemfiles_from_dir(Directory);
+ read_files_and_udpate_table(Directory);
[{roots, DerList}] ->
DerList
end;
update_tab ->
- read_pemfiles_from_dir(Directory)
+ read_files_and_udpate_table(Directory)
end.
--spec read_pemfiles_from_dir(file:filename()) -> list().
-read_pemfiles_from_dir(Dir) ->
- DerList =
- case file:list_dir(Dir) of
- {error, enoent} ->
- []; % FIXME: log enoent
- {error, _Reason} ->
- []; % FIXME: log Reason
- {ok, Filenames} ->
- Files = lists:filter(
- fun(F) ->
- string:equal(".pem", filename:extension(F))
- end,
- Filenames),
- ders_from_pemfiles(Dir, Files)
- end,
- true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, DerList}),
- DerList.
-
-ders_from_pemfiles(Dir, Filenames) ->
- L = [ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames],
- lists:flatten(L).
-
-ders_from_pemfile(Filename) ->
- Pems = case (catch public_key:pem_decode(pems_from_file(Filename))) of
- {'EXIT', Reason} ->
- lager:info("badly encoded cert in ~p: ~p", [Filename, Reason]),
- [];
- P -> P
- end,
- [der_from_pem(X) || X <- Pems].
-
--include_lib("public_key/include/public_key.hrl").
-der_from_pem(Pem) ->
- case Pem of
- {_Type, Der, not_encrypted} ->
- case x509:valid_cert_p(Der) of
- true -> Der;
- false -> []
- end;
- _ -> []
- end.
-
-pems_from_file(Filename) ->
- {ok, Pems} = file:read_file(Filename),
- Pems.
+read_files_and_udpate_table(Directory) ->
+ L = x509:read_pemfiles_from_dir(Directory),
+ true = ets:insert(?CACHE_TABLE, {?ROOTS_CACHE_KEY, L}),
+ L.
%%%%%%%%%%%%%%%%%%%%
%% Testing internal functions.
diff --git a/src/x509.erl b/src/x509.erl
index 5a96a29..b0363cd 100644
--- a/src/x509.erl
+++ b/src/x509.erl
@@ -2,9 +2,10 @@
%%% See LICENSE for licensing information.
-module(x509).
--export([normalise_chain/2, cert_string/1, valid_cert_p/1]).
+-export([normalise_chain/2, cert_string/1, read_pemfiles_from_dir/1]).
-include_lib("public_key/include/public_key.hrl").
+-include_lib("eunit/include/eunit.hrl").
-type reason() :: {chain_too_long |
root_unknown |
@@ -68,14 +69,72 @@ signer(Cert, [H|T]) ->
{false, _} -> signer(Cert, T)
end.
+%% encoded_tbs_cert: verbatim from pubkey_cert.erl
+encoded_tbs_cert(Cert) ->
+ {ok, PKIXCert} =
+ 'OTP-PUB-KEY':decode_TBSCert_exclusive(Cert),
+ {'Certificate',
+ {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} = PKIXCert,
+ EncodedTBSCert.
+
+%% extract_verify_data: close to pubkey_cert:extract_verify_data/2
+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),
+ #'SubjectPublicKeyInfo'{
+ algorithm = #'AlgorithmIdentifier'{algorithm = Alg, parameters = Params},
+ subjectPublicKey = {0, Key0}} = IssuerSPKI,
+ KeyType = pubkey_cert_records:supportedPublicKeyAlgorithms(Alg),
+
+ %% public_key:pem_entry_decode()
+ 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)
+ end,
+ public_key:verify(DigestOrPlainText, DigestType, Signature, IssuerKey).
+
-spec signed_by_p(binary(), binary()) -> true | {false, reason()}.
-signed_by_p(Cert, IssuerCert) ->
+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",
- [mochihex:to_hex(crypto:hash(sha, Cert)), Reason]),
+ [cert_string(Cert), Reason]),
{false, encoding_invalid};
true ->
%% Cert.issuer does match IssuerCert.subject. Now verify
@@ -99,23 +158,18 @@ public_key(#'OTPCertificate'{
Key.
cert_string(Der) ->
- lists:flatten([io_lib:format("~2.16.0B", [X]) ||
- X <- binary_to_list(crypto:hash(sha, Der))]).
-
-valid_cert_p(Der) ->
- %% Use the customized ASN.1 specification "OTP-PKIX.asn1" since
- %% that's what's required for public_key functions we're using
- %% (pkix_verify, public_key:pkix_is_issuer).
- case (catch public_key:pkix_decode_cert(Der, otp)) of
- #'OTPCertificate'{} ->
+ mochihex:to_hex(crypto:hash(sha, Der)).
+
+parsable_cert_p(Der) ->
+ case (catch public_key:pkix_decode_cert(Der, plain)) of
+ #'Certificate'{} ->
true;
{'EXIT', Reason} ->
- lager:info("invalid certificate: ~p: ~p",
- [mochihex:to_hex(crypto:hash(sha, Der)), Reason]),
+ lager:info("invalid certificate: ~p: ~p", [cert_string(Der), Reason]),
false;
Unknown ->
lager:info("unknown error decoding cert: ~p: ~p",
- [mochihex:to_hex(crypto:hash(sha, Der)), Unknown]),
+ [cert_string(Der), Unknown]),
false
end.
@@ -142,13 +196,118 @@ valid_cert_p(Der) ->
detox_precert(CertChain) ->
CertChain. % NYI
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-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.
+
+ders_from_pemfiles(Dir, Filenames) ->
+ lists:flatten(
+ [ders_from_pemfile(filename:join(Dir, X)) || X <- Filenames]).
+
+ders_from_pemfile(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].
+
+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(CertDer) ->
+ 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(CertDer),
+ NowMegaSec * 1000 * 1000 + NowSec,
+ NowMicroSec])),
+ lager:debug("dumping cert to ~p~n", [Filename]),
+ file:write_file(Filename, CertDer);
+ _ ->
+ not_logged
+ end.
+
%%%%%%%%%%%%%%%%%%%%
%% Testing private functions.
--include_lib("eunit/include/eunit.hrl").
-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_() ->
- C0 = ?C0,
- C1 = ?C1,
+ {setup,
+ 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:
+ %% {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
+ ?_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))
+ ] 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]}, valid_chain_p([C1], [C0], 10)),
@@ -169,3 +328,20 @@ valid_cert_test_() ->
%% Max chain length 0 is not OK.
?_assertMatch({false, chain_too_long}, valid_chain_p([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).