aboutsummaryrefslogtreecommitdiffstats
path: root/lib/public_key/src/pubkey_cert.erl
diff options
context:
space:
mode:
authorErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
committerErlang/OTP <[email protected]>2009-11-20 14:54:40 +0000
commit84adefa331c4159d432d22840663c38f155cd4c1 (patch)
treebff9a9c66adda4df2106dfd0e5c053ab182a12bd /lib/public_key/src/pubkey_cert.erl
downloadotp-84adefa331c4159d432d22840663c38f155cd4c1.tar.gz
otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.bz2
otp-84adefa331c4159d432d22840663c38f155cd4c1.zip
The R13B03 release.OTP_R13B03
Diffstat (limited to 'lib/public_key/src/pubkey_cert.erl')
-rw-r--r--lib/public_key/src/pubkey_cert.erl988
1 files changed, 988 insertions, 0 deletions
diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl
new file mode 100644
index 0000000000..0ccc74799c
--- /dev/null
+++ b/lib/public_key/src/pubkey_cert.erl
@@ -0,0 +1,988 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2008-2009. All Rights Reserved.
+%%
+%% The contents of this file are subject to the Erlang Public License,
+%% Version 1.1, (the "License"); you may not use this file except in
+%% compliance with the License. You should have received a copy of the
+%% Erlang Public License along with this software. If not, it can be
+%% retrieved online at http://www.erlang.org/.
+%%
+%% Software distributed under the License is distributed on an "AS IS"
+%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+%% the License for the specific language governing rights and limitations
+%% under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%
+
+-module(pubkey_cert).
+
+-include("public_key.hrl").
+
+-export([verify_signature/3,
+ init_validation_state/3, prepare_for_next_cert/2,
+ validate_time/3, validate_signature/6,
+ validate_issuer/4, validate_names/6,
+ validate_revoked_status/3, validate_extensions/4,
+ validate_unknown_extensions/3,
+ normalize_general_name/1, digest_type/1, digest/2, is_self_signed/1,
+ is_issuer/2, issuer_id/2, is_fixed_dh_cert/1]).
+
+-define(NULL, 0).
+
+%%====================================================================
+%% Internal application API
+%%====================================================================
+
+verify_signature(DerCert, Key, KeyParams) ->
+ {ok, OtpCert} = pubkey_cert_records:decode_cert(DerCert, otp),
+ verify_signature(OtpCert, DerCert, Key, KeyParams).
+
+init_validation_state(#'OTPCertificate'{} = OtpCert, DefaultPathLen,
+ Options) ->
+ PolicyTree = #policy_tree_node{valid_policy = ?anyPolicy,
+ qualifier_set = [],
+ criticality_indicator = false,
+ expected_policy_set = [?anyPolicy]},
+ MaxLen = proplists:get_value(max_path_length, Options, DefaultPathLen),
+ ExplicitPolicy = policy_indicator(MaxLen,
+ proplists:get_value(explicit_policy, Options, false)),
+ InhibitAnyPolicy = policy_indicator(MaxLen,
+ proplists:get_value(inhibit_any_policy,
+ Options, false)),
+ PolicyMapping = policy_indicator(MaxLen,
+ proplists:get_value(policy_mapping, Options, false)),
+ AccErrors = proplists:get_value(acc_errors, Options, []),
+ State = #path_validation_state{max_path_length = MaxLen,
+ valid_policy_tree = PolicyTree,
+ explicit_policy = ExplicitPolicy,
+ inhibit_any_policy = InhibitAnyPolicy,
+ policy_mapping = PolicyMapping,
+ acc_errors = AccErrors,
+ cert_num = 0},
+ prepare_for_next_cert(OtpCert, State).
+
+prepare_for_next_cert(OtpCert, ValidationState = #path_validation_state{
+ working_public_key_algorithm = PrevAlgo,
+ working_public_key_parameters =
+ PrevParams}) ->
+ TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
+ Issuer = TBSCert#'OTPTBSCertificate'.subject,
+
+ {Algorithm, PublicKey, PublicKeyParams0} =
+ public_key_info(TBSCert#'OTPTBSCertificate'.subjectPublicKeyInfo,
+ ValidationState),
+ PublicKeyParams =
+ case PublicKeyParams0 of
+ 'NULL' when Algorithm =:= PrevAlgo ->
+ PrevParams;
+ asn1_NOVALUE when Algorithm =:= PrevAlgo ->
+ PrevParams;
+ _ -> PublicKeyParams0
+ end,
+
+ ValidationState#path_validation_state{
+ working_public_key_algorithm = Algorithm,
+ working_public_key = PublicKey,
+ working_public_key_parameters = PublicKeyParams,
+ working_issuer_name = Issuer,
+ cert_num = ValidationState#path_validation_state.cert_num + 1
+ }.
+
+validate_time(OtpCert, AccErr, Verify) ->
+ TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
+ {'Validity', NotBeforeStr, NotAfterStr}
+ = TBSCert#'OTPTBSCertificate'.validity,
+ Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
+ NotBefore = time_str_2_gregorian_sec(NotBeforeStr),
+ NotAfter = time_str_2_gregorian_sec(NotAfterStr),
+
+ case ((NotBefore =< Now) and (Now =< NotAfter)) of
+ true ->
+ AccErr;
+ false ->
+ not_valid({bad_cert, cert_expired}, Verify, AccErr)
+ end.
+
+validate_issuer(OtpCert, Issuer, AccErr, Verify) ->
+ TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
+ case is_issuer(Issuer, TBSCert#'OTPTBSCertificate'.issuer) of
+ true ->
+ AccErr;
+ _ ->
+ not_valid({bad_cert, invalid_issuer}, Verify, AccErr)
+ end.
+
+validate_signature(OtpCert, DerCert, Key, KeyParams,
+ AccErr, Verify) ->
+
+ case verify_signature(OtpCert, DerCert, Key, KeyParams) of
+ true ->
+ AccErr;
+ false ->
+ not_valid({bad_cert, invalid_signature}, Verify, AccErr)
+ end.
+
+validate_names(OtpCert, Permit, Exclude, Last, AccErr, Verify) ->
+ case is_self_signed(OtpCert) andalso (not Last) of
+ true ->
+ ok;
+ false ->
+ TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
+ Subject = TBSCert#'OTPTBSCertificate'.subject,
+ AltSubject =
+ select_extension(?'id-ce-subjectAltName',
+ TBSCert#'OTPTBSCertificate'.extensions),
+
+ EmailAddress = extract_email(Subject),
+ Name = [{directoryName, Subject}|EmailAddress],
+
+ AltNames = case AltSubject of
+ undefined -> [];
+ _ -> AltSubject#'Extension'.extnValue
+ end,
+
+ case (is_permitted(Name, Permit) andalso
+ is_permitted(AltNames, Permit) andalso
+ (not is_excluded(Name, Exclude)) andalso
+ (not is_excluded(AltNames, Exclude))) of
+ true ->
+ AccErr;
+ false ->
+ not_valid({bad_cert, name_not_permitted},
+ Verify, AccErr)
+ end
+ end.
+
+
+%% See rfc3280 4.1.2.6 Subject: regarding emails.
+extract_email({rdnSequence, List}) ->
+ extract_email2(List).
+extract_email2([[#'AttributeTypeAndValue'{type=?'id-emailAddress',
+ value=Mail}]|_]) ->
+ [{rfc822Name, Mail}];
+extract_email2([_|Rest]) ->
+ extract_email2(Rest);
+extract_email2([]) -> [].
+
+validate_revoked_status(_OtpCert, _Verify, AccErr) ->
+ %% true |
+ %% throw({bad_cert, cert_revoked})
+ AccErr.
+
+validate_extensions(OtpCert, ValidationState, Verify, AccErr) ->
+ TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
+ Extensions = TBSCert#'OTPTBSCertificate'.extensions,
+ validate_extensions(Extensions, ValidationState, no_basic_constraint,
+ is_self_signed(OtpCert), [], Verify, AccErr).
+
+validate_unknown_extensions([], AccErr, _Verify) ->
+ AccErr;
+validate_unknown_extensions([#'Extension'{critical = true} | _],
+ AccErr, Verify) ->
+ not_valid({bad_cert, unknown_critical_extension}, Verify, AccErr);
+validate_unknown_extensions([#'Extension'{critical = false} | Rest],
+ AccErr, Verify) ->
+ validate_unknown_extensions(Rest, AccErr, Verify).
+
+normalize_general_name({rdnSequence, Issuer}) ->
+ NormIssuer = normalize_general_name(Issuer),
+ {rdnSequence, NormIssuer};
+
+normalize_general_name(Issuer) ->
+ Normalize = fun([{Description, Type, {printableString, Value}}]) ->
+ NewValue = string:to_lower(strip_spaces(Value)),
+ {Description, Type, {printableString, NewValue}};
+ (Atter) ->
+ Atter
+ end,
+ lists:sort(lists:map(Normalize, Issuer)).
+
+is_self_signed(#'OTPCertificate'{tbsCertificate=
+ #'OTPTBSCertificate'{issuer = Issuer,
+ subject = Subject}}) ->
+ is_issuer(Issuer, Subject).
+
+is_issuer({rdnSequence, Issuer}, {rdnSequence, Candidate}) ->
+ is_dir_name(Issuer, Candidate, true).
+
+issuer_id(Otpcert, other) ->
+ TBSCert = Otpcert#'OTPCertificate'.tbsCertificate,
+ Extensions = TBSCert#'OTPTBSCertificate'.extensions,
+ case select_extension(?'id-ce-authorityKeyIdentifier', Extensions) of
+ undefined ->
+ {error, issuer_not_found};
+ AuthKeyExt ->
+ cert_auth_key_id(AuthKeyExt#'Extension'.extnValue)
+ end;
+
+issuer_id(Otpcert, self) ->
+ TBSCert = Otpcert#'OTPCertificate'.tbsCertificate,
+ Issuer = TBSCert#'OTPTBSCertificate'.issuer,
+ SerialNr = TBSCert#'OTPTBSCertificate'.serialNumber,
+ {ok, {SerialNr, normalize_general_name(Issuer)}}.
+
+
+is_fixed_dh_cert(#'OTPCertificate'{tbsCertificate =
+ #'OTPTBSCertificate'{subjectPublicKeyInfo =
+ SubjectPublicKeyInfo,
+ extensions =
+ Extensions}}) ->
+ is_fixed_dh_cert(SubjectPublicKeyInfo, Extensions).
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+not_valid(Error, true, _) ->
+ throw(Error);
+not_valid(Error, false, AccErrors) ->
+ [Error | AccErrors].
+
+verify_signature(OtpCert, DerCert, Key, KeyParams) ->
+ %% Signature is an ASN1 compact bit string
+ {0, Signature} = OtpCert#'OTPCertificate'.signature,
+ SigAlgRec = OtpCert#'OTPCertificate'.signatureAlgorithm,
+ SigAlg = SigAlgRec#'SignatureAlgorithm'.algorithm,
+ EncTBSCert = encoded_tbs_cert(DerCert),
+ verify(SigAlg, EncTBSCert, Signature, Key, KeyParams).
+
+verify(Alg, PlainText, Signature, Key, KeyParams) ->
+ public_key:verify_signature(PlainText, digest_type(Alg),
+ Signature, Key, KeyParams).
+
+encoded_tbs_cert(Cert) ->
+ {ok, PKIXCert} =
+ 'OTP-PUB-KEY':decode_TBSCert_exclusive(Cert),
+ {'Certificate',
+ {'Certificate_tbsCertificate', EncodedTBSCert}, _, _} = PKIXCert,
+ EncodedTBSCert.
+
+digest_type(?sha1WithRSAEncryption) ->
+ sha;
+digest_type(?md5WithRSAEncryption) ->
+ md5;
+digest_type(?'id-dsa-with-sha1') ->
+ sha.
+
+digest(?sha1WithRSAEncryption, Msg) ->
+ crypto:sha(Msg);
+digest(?md5WithRSAEncryption, Msg) ->
+ crypto:md5(Msg);
+digest(?'id-dsa-with-sha1', Msg) ->
+ crypto:sha(Msg).
+
+public_key_info(PublicKeyInfo,
+ #path_validation_state{working_public_key_algorithm =
+ WorkingAlgorithm,
+ working_public_key_parameters =
+ WorkingParams}) ->
+ PublicKey = PublicKeyInfo#'OTPSubjectPublicKeyInfo'.subjectPublicKey,
+ AlgInfo = PublicKeyInfo#'OTPSubjectPublicKeyInfo'.algorithm,
+
+ PublicKeyParams = AlgInfo#'PublicKeyAlgorithm'.parameters,
+ Algorithm = AlgInfo#'PublicKeyAlgorithm'.algorithm,
+
+ NewPublicKeyParams =
+ case PublicKeyParams of
+ 'NULL' when WorkingAlgorithm == Algorithm ->
+ WorkingParams;
+ _ ->
+ PublicKeyParams
+ end,
+ {Algorithm, PublicKey, NewPublicKeyParams}.
+
+time_str_2_gregorian_sec({utcTime, [Y1,Y2,M1,M2,D1,D2,H1,H2,M3,M4,S1,S2,Z]}) ->
+ case list_to_integer([Y1,Y2]) of
+ N when N >= 50 ->
+ time_str_2_gregorian_sec({generalTime,
+ [$1,$9,Y1,Y2,M1,M2,D1,D2,
+ H1,H2,M3,M4,S1,S2,Z]});
+ _ ->
+ time_str_2_gregorian_sec({generalTime,
+ [$2,$0,Y1,Y2,M1,M2,D1,D2,
+ H1,H2,M3,M4,S1,S2,Z]})
+ end;
+
+time_str_2_gregorian_sec({_,[Y1,Y2,Y3,Y4,M1,M2,D1,D2,H1,H2,M3,M4,S1,S2,$Z]}) ->
+ Year = list_to_integer([Y1, Y2, Y3, Y4]),
+ Month = list_to_integer([M1, M2]),
+ Day = list_to_integer([D1, D2]),
+ Hour = list_to_integer([H1, H2]),
+ Min = list_to_integer([M3, M4]),
+ Sec = list_to_integer([S1, S2]),
+ calendar:datetime_to_gregorian_seconds({{Year, Month, Day},
+ {Hour, Min, Sec}}).
+
+is_dir_name([], [], _Exact) -> true;
+is_dir_name([H|R1],[H|R2], Exact) -> is_dir_name(R1,R2, Exact);
+is_dir_name([[{'AttributeTypeAndValue', Type, What1}]|Rest1],
+ [[{'AttributeTypeAndValue', Type, What2}]|Rest2],Exact) ->
+ case is_dir_name2(What1,What2) of
+ true -> is_dir_name(Rest1,Rest2,Exact);
+ false -> false
+ end;
+is_dir_name([{'AttributeTypeAndValue', Type, What1}|Rest1],
+ [{'AttributeTypeAndValue', Type, What2}|Rest2], Exact) ->
+ case is_dir_name2(What1,What2) of
+ true -> is_dir_name(Rest1,Rest2,Exact);
+ false -> false
+ end;
+is_dir_name(_,[],false) ->
+ true;
+is_dir_name(_,_,_) ->
+ false.
+
+is_dir_name2(Value, Value) -> true;
+is_dir_name2({printableString, Value1}, {printableString, Value2}) ->
+ string:to_lower(strip_spaces(Value1)) =:=
+ string:to_lower(strip_spaces(Value2));
+is_dir_name2({utf8String, Value1}, String) -> %% BUGBUG FIX UTF8 conv
+ is_dir_name2({printableString, binary_to_list(Value1)}, String);
+is_dir_name2(String, {utf8String, Value1}) -> %% BUGBUG FIX UTF8 conv
+ is_dir_name2(String, {printableString, binary_to_list(Value1)});
+is_dir_name2(_, _) ->
+ false.
+
+cert_auth_key_id(#'AuthorityKeyIdentifier'{authorityCertIssuer
+ = asn1_NOVALUE}) ->
+ {error, issuer_not_found};
+cert_auth_key_id(#'AuthorityKeyIdentifier'{authorityCertIssuer =
+ AuthCertIssuer,
+ authorityCertSerialNumber =
+ SerialNr}) ->
+ {ok, {SerialNr, decode_general_name(AuthCertIssuer)}}.
+
+decode_general_name([{directoryName, Issuer}]) ->
+ normalize_general_name(Issuer).
+
+%% Strip all leading and trailing spaces and make
+%% sure there is no double spaces in between.
+strip_spaces(String) ->
+ NewString =
+ lists:foldl(fun(Char, Acc) -> Acc ++ Char ++ " " end, [],
+ string:tokens(String, " ")),
+ string:strip(NewString).
+
+select_extension(_, []) ->
+ undefined;
+select_extension(Id, [#'Extension'{extnID = Id} = Extension | _]) ->
+ Extension;
+select_extension(Id, [_ | Extensions]) ->
+ select_extension(Id, Extensions).
+
+%% No extensions present
+validate_extensions(asn1_NOVALUE, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr) ->
+ validate_extensions([], ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+
+validate_extensions([], ValidationState, basic_constraint, _SelfSigned,
+ UnknownExtensions, _Verify, AccErr) ->
+ {ValidationState, UnknownExtensions, AccErr};
+validate_extensions([], ValidationState =
+ #path_validation_state{max_path_length = Len,
+ last_cert = Last},
+ no_basic_constraint, SelfSigned, UnknownExtensions,
+ Verify, AccErr0) ->
+ case Last of
+ true when SelfSigned ->
+ {ValidationState, UnknownExtensions, AccErr0};
+ true ->
+ {ValidationState#path_validation_state{max_path_length = Len - 1},
+ UnknownExtensions, AccErr0};
+ %% basic_constraint must appear in certs used for digital sign
+ %% see 4.2.1.10 in rfc 3280
+ false ->
+ AccErr = not_valid({bad_cert, missing_basic_constraint},
+ Verify, AccErr0),
+ case SelfSigned of
+ true ->
+ {ValidationState, UnknownExtensions, AccErr};
+ false ->
+ {ValidationState#path_validation_state{max_path_length =
+ Len - 1},
+ UnknownExtensions, AccErr}
+ end
+ end;
+
+validate_extensions([#'Extension'{extnID = ?'id-ce-basicConstraints',
+ extnValue =
+ #'BasicConstraints'{cA = true,
+ pathLenConstraint = N}} |
+ Rest],
+ ValidationState =
+ #path_validation_state{max_path_length = Len}, _,
+ SelfSigned, UnknownExtensions, Verify, AccErr) ->
+ Length = if SelfSigned -> min(N, Len);
+ true -> min(N, Len-1)
+ end,
+ validate_extensions(Rest,
+ ValidationState#path_validation_state{max_path_length =
+ Length},
+ basic_constraint, SelfSigned, UnknownExtensions,
+ Verify, AccErr);
+%% The pathLenConstraint field is meaningful only if cA is set to
+%% TRUE.
+validate_extensions([#'Extension'{extnID = ?'id-ce-basicConstraints',
+ extnValue =
+ #'BasicConstraints'{cA = false}} |
+ Rest], ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr) ->
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+
+%%
+validate_extensions([#'Extension'{extnID = ?'id-ce-keyUsage',
+ extnValue = KeyUse
+ } | Rest],
+ #path_validation_state{last_cert=Last} = ValidationState,
+ ExistBasicCon, SelfSigned, UnknownExtensions,
+ Verify, AccErr0) ->
+ case Last orelse is_valid_key_usage(KeyUse, keyCertSign) of
+ true ->
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify,
+ AccErr0);
+ false ->
+ AccErr = not_valid({bad_cert, invalid_key_usage}, Verify, AccErr0),
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify,
+ AccErr)
+ end;
+
+validate_extensions([#'Extension'{extnID = ?'id-ce-extKeyUsage',
+ extnValue = KeyUse,
+ critical = true} | Rest],
+ #path_validation_state{} = ValidationState,
+ ExistBasicCon, SelfSigned, UnknownExtensions, Verify,
+ AccErr0) ->
+ case is_valid_extkey_usage(KeyUse) of
+ true ->
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions,
+ Verify, AccErr0);
+ false ->
+ AccErr =
+ not_valid({bad_cert, invalid_ext_key_usage}, Verify, AccErr0),
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr)
+ end;
+
+validate_extensions([#'Extension'{extnID = ?'id-ce-subjectAltName',
+ extnValue = Names} | Rest],
+ ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr0) ->
+ case validate_subject_alt_names(Names) of
+ true when Names =/= [] ->
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify,
+ AccErr0);
+ _ ->
+ AccErr =
+ not_valid({bad_cert, invalid_subject_altname},
+ Verify, AccErr0),
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify,
+ AccErr)
+ end;
+
+%% This extension SHOULD NOT be marked critical. Its value
+%% does not have to be further validated at this point.
+validate_extensions([#'Extension'{extnID = ?'id-ce-issuerAltName',
+ extnValue = _} | Rest],
+ ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr) ->
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+
+%% This extension MUST NOT be marked critical.Its value
+%% does not have to be further validated at this point.
+validate_extensions([#'Extension'{extnID = Id,
+ extnValue = _,
+ critical = false} | Rest],
+ ValidationState,
+ ExistBasicCon, SelfSigned, UnknownExtensions,
+ Verify, AccErr)
+ when Id == ?'id-ce-subjectKeyIdentifier';
+ Id == ?'id-ce-authorityKeyIdentifier'->
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+
+validate_extensions([#'Extension'{extnID = ?'id-ce-nameConstraints',
+ extnValue = NameConst} | Rest],
+ ValidationState,
+ ExistBasicCon, SelfSigned, UnknownExtensions,
+ Verify, AccErr) ->
+ Permitted = NameConst#'NameConstraints'.permittedSubtrees,
+ Excluded = NameConst#'NameConstraints'.excludedSubtrees,
+
+ NewValidationState = add_name_constraints(Permitted, Excluded,
+ ValidationState),
+
+ validate_extensions(Rest, NewValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+
+
+validate_extensions([#'Extension'{extnID = ?'id-ce-certificatePolicies',
+ critical = true} | Rest], ValidationState,
+ ExistBasicCon, SelfSigned,
+ UnknownExtensions, Verify, AccErr0) ->
+ %% TODO: Remove this clause when policy handling is
+ %% fully implemented
+ AccErr =
+ not_valid({bad_cert, unknown_critical_extension}, Verify, AccErr0),
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+
+validate_extensions([#'Extension'{extnID = ?'id-ce-certificatePolicies',
+ extnValue = #'PolicyInformation'{
+ policyIdentifier = Id,
+ policyQualifiers = Qualifier}}
+ | Rest], #path_validation_state{valid_policy_tree = Tree}
+ = ValidationState,
+ ExistBasicCon, SelfSigned, UnknownExtensions,
+ Verify, AccErr) ->
+
+ %% TODO: Policy imp incomplete
+ NewTree = process_policy_tree(Id, Qualifier, Tree),
+
+ validate_extensions(Rest,
+ ValidationState#path_validation_state{
+ valid_policy_tree = NewTree},
+ ExistBasicCon, SelfSigned, UnknownExtensions,
+ Verify, AccErr);
+
+validate_extensions([#'Extension'{extnID = ?'id-ce-policyConstraints',
+ critical = true} | Rest], ValidationState,
+ ExistBasicCon, SelfSigned, UnknownExtensions, Verify,
+ AccErr0) ->
+ %% TODO: Remove this clause when policy handling is
+ %% fully implemented
+ AccErr =
+ not_valid({bad_cert, unknown_critical_extension}, Verify, AccErr0),
+ validate_extensions(Rest, ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+validate_extensions([#'Extension'{extnID = ?'id-ce-policyConstraints',
+ extnValue = #'PolicyConstraints'{
+ requireExplicitPolicy = ExpPolicy,
+ inhibitPolicyMapping = MapPolicy}}
+ | Rest], ValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr) ->
+
+ %% TODO: Policy imp incomplete
+ NewValidationState = add_policy_constraints(ExpPolicy, MapPolicy,
+ ValidationState),
+
+ validate_extensions(Rest, NewValidationState, ExistBasicCon,
+ SelfSigned, UnknownExtensions, Verify, AccErr);
+
+validate_extensions([Extension | Rest], ValidationState,
+ ExistBasicCon, SelfSigned, UnknownExtensions,
+ Verify, AccErr) ->
+ validate_extensions(Rest, ValidationState, ExistBasicCon, SelfSigned,
+ [Extension | UnknownExtensions], Verify, AccErr).
+
+is_valid_key_usage(KeyUse, Use) ->
+ lists:member(Use, KeyUse).
+
+is_valid_extkey_usage(?'id-kp-clientAuth') ->
+ true;
+is_valid_extkey_usage(?'id-kp-serverAuth') ->
+ true;
+is_valid_extkey_usage(_) ->
+ false.
+
+validate_subject_alt_names([]) ->
+ true;
+validate_subject_alt_names([AltName | Rest]) ->
+ case is_valid_subject_alt_name(AltName) of
+ true ->
+ validate_subject_alt_names(Rest);
+ false ->
+ false
+ end.
+
+is_valid_subject_alt_name({Name, Value}) when Name == rfc822Name;
+ Name == dNSName ->
+ case Value of
+ "" ->
+ false;
+ _ ->
+ true
+ end;
+
+is_valid_subject_alt_name({iPAdress, Addr}) ->
+ case length(Addr) of
+ 4 -> %ipv4
+ true;
+ 16 -> %ipv6
+ true;
+ _ ->
+ false
+ end;
+is_valid_subject_alt_name({uniformResourceIdentifier, URI}) ->
+ is_valid_uri(URI);
+
+is_valid_subject_alt_name({directoryName, _}) ->
+ true;
+is_valid_subject_alt_name({_, [_|_]}) ->
+ true;
+is_valid_subject_alt_name({_, _}) ->
+ false.
+
+min(N, M) when N =< M ->
+ N;
+min(_, M) ->
+ M.
+
+is_ip_address(Address) ->
+ case inet_parse:address(Address) of
+ {ok, _} ->
+ true;
+ _ ->
+ false
+ end.
+
+is_fully_qualified_name(_Name) ->
+ true.
+
+is_valid_uri(AbsURI) ->
+ case split_uri(AbsURI) of
+ incomplete ->
+ false;
+ {StrScheme, _, Host, _, _} ->
+ case string:to_lower(StrScheme) of
+ Scheme when Scheme =:= "http"; Scheme =:= "ftp" ->
+ is_valid_host(Host);
+ _ ->
+ false
+ end
+ end.
+
+is_valid_host(Host) ->
+ case is_ip_address(Host) of
+ true ->
+ true;
+ false ->
+ is_fully_qualified_name(Host)
+ end.
+
+%% Could have a more general split URI in stdlib? Maybe when
+%% regexs are improved. Needed also in inets!
+split_uri(Uri) ->
+ case split_uri(Uri, ":", {error, no_scheme}, 1, 1) of
+ {error, no_scheme} ->
+ incomplete;
+ {StrScheme, "//" ++ URIPart} ->
+ {Authority, PathQuery} =
+ split_auth_path(URIPart),
+ {UserInfo, HostPort} =
+ split_uri(Authority, "@", {"", Authority}, 1, 1),
+ {Host, Port} =
+ split_uri(HostPort, ":", {HostPort, dummy_port}, 1, 1),
+ {StrScheme, UserInfo, Host, Port, PathQuery}
+ end.
+
+split_auth_path(URIPart) ->
+ case split_uri(URIPart, "/", URIPart, 1, 0) of
+ Split = {_, _} ->
+ Split;
+ URIPart ->
+ case split_uri(URIPart, "\\?", URIPart, 1, 0) of
+ Split = {_, _} ->
+ Split;
+ URIPart ->
+ {URIPart,""}
+ end
+ end.
+
+split_uri(UriPart, SplitChar, NoMatchResult, SkipLeft, SkipRight) ->
+ case regexp:first_match(UriPart, SplitChar) of
+ {match, Match, _} ->
+ {string:substr(UriPart, 1, Match - SkipLeft),
+ string:substr(UriPart, Match + SkipRight, length(UriPart))};
+ nomatch ->
+ NoMatchResult
+ end.
+
+is_rdnSeq({rdnSequence,[]}, {rdnSequence,[none]}) ->
+ true;
+is_rdnSeq({rdnSequence,DirName}, {rdnSequence,Permitted}) ->
+ is_dir_name(DirName, Permitted, false).
+
+is_permitted(_, no_constraints) ->
+ true;
+is_permitted(Names, Constraints) ->
+ is_valid_name(Names, Constraints, true).
+
+is_excluded([], _) ->
+ false;
+is_excluded(Names, Constraints) ->
+ is_valid_name(Names, Constraints, false).
+
+is_valid_name([], _, Default) ->
+ Default;
+is_valid_name([{Type, Name} | Rest], Constraints, Default) ->
+ case type_subtree_names(Type, Constraints) of
+ [_|_] = ConstraintNames ->
+ case match_name(Type, Name, ConstraintNames) of
+ Default ->
+ is_valid_name(Rest, Constraints, Default);
+ Fail ->
+ Fail
+ end;
+ [] ->
+ is_valid_name(Rest, Constraints,Default)
+ end.
+
+add_name_constraints(NewPermittedTrees, NewExcludedTrees,
+ #path_validation_state{
+ permitted_subtrees = PermittedTrees,
+ excluded_subtrees = ExcludedTrees} =
+ ValidationState) ->
+ NewPermitted = subtree_intersection(NewPermittedTrees, PermittedTrees),
+ NewExcluded = subtree_union(NewExcludedTrees, ExcludedTrees),
+ ValidationState#path_validation_state{permitted_subtrees = NewPermitted,
+ excluded_subtrees = NewExcluded}.
+subtree_union(asn1_NOVALUE, Trees) ->
+ Trees;
+subtree_union(Trees1, Trees2) ->
+ Trees1 ++ Trees2.
+
+subtree_intersection(asn1_NOVALUE, Trees) ->
+ Trees;
+subtree_intersection(List, no_constraints) ->
+ List;
+subtree_intersection([Tree | Trees1], Trees2) ->
+ Trees = is_in_intersection(Tree, Trees2),
+ subtree_intersection(Trees1, Trees);
+subtree_intersection([], TreesInt) ->
+ TreesInt.
+
+is_in_intersection(#'GeneralSubtree'{base =
+ {directoryName, {rdnSequence, Name1}}}
+ = Name,
+ [#'GeneralSubtree'{base =
+ {directoryName, {rdnSequence, Name2}}}
+ | Trees]) ->
+ case is_dir_name(Name1, Name2, false) of
+ true ->
+ [Name|Trees];
+ false ->
+ [Name#'GeneralSubtree'{base =
+ {directoryName, {rdnSequence,[none]}}}
+ | Trees]
+ end;
+is_in_intersection(#'GeneralSubtree'{base = {ipAdress, Ip}},
+ Trees = [#'GeneralSubtree'{base = {ipAdress, Ip}} | _]) ->
+ %% BUGBUG
+ Trees;
+is_in_intersection(#'GeneralSubtree'{base = {x400Address, OrAddr1}} = Addr,
+ [#'GeneralSubtree'{base = {x400Address, OrAddr2}}
+ | Trees]) ->
+ case is_or_address(OrAddr1, OrAddr2) of
+ true ->
+ [Addr|Trees];
+ false ->
+ [#'GeneralSubtree'{base = {x400Address, ""}} | Trees]
+ end;
+
+is_in_intersection(#'GeneralSubtree'{base = {Type, Name1}} = Name,
+ [#'GeneralSubtree'{base = {Type, Name2}}
+ | Trees]) ->
+ case case_insensitive_match(Name1, Name2) of
+ true ->
+ [Name|Trees];
+ false ->
+ [#'GeneralSubtree'{base = {Type, ""}} | Trees]
+ end;
+is_in_intersection(New, []) ->
+ [New];
+is_in_intersection(Name, [Other | IntCandidates]) ->
+ [Other|is_in_intersection(Name, IntCandidates)].
+
+type_subtree_names(Type, SubTrees) ->
+ [Name || #'GeneralSubtree'{base = {TreeType, Name}} <- SubTrees,
+ TreeType =:= Type].
+
+match_name(rfc822Name, Name, [PermittedName | Rest]) ->
+ match_name(fun is_valid_host_or_domain/2, Name, PermittedName, Rest);
+
+match_name(directoryName, DirName, [PermittedName | Rest]) ->
+ match_name(fun is_rdnSeq/2, DirName, PermittedName, Rest);
+
+match_name(uniformResourceIdentifier, URI, [PermittedName | Rest]) ->
+ case split_uri(URI) of
+ incomplete ->
+ false;
+ {_, _, Host, _, _} ->
+ match_name(fun is_valid_host_or_domain/2, Host,
+ PermittedName, Rest)
+ end;
+
+match_name(emailAddress, Name, [PermittedName | Rest]) ->
+ Fun = fun(Email, PermittedEmail) ->
+ is_valid_email_address(Email, PermittedEmail,
+ string:tokens(PermittedEmail,"@"))
+ end,
+ match_name(Fun, Name, PermittedName, Rest);
+
+match_name(dNSName, Name, [PermittedName | Rest]) ->
+ Fun = fun(Domain, [$.|Domain]) -> true;
+ (Name1,Name2) ->
+ lists:suffix(string:to_lower(Name2),
+ string:to_lower(Name1))
+ end,
+ match_name(Fun, Name, [$.|PermittedName], Rest);
+
+match_name(x400Address, OrAddress, [PermittedAddr | Rest]) ->
+ match_name(fun is_or_address/2, OrAddress, PermittedAddr, Rest);
+
+match_name(ipAdress, IP, [PermittedIP | Rest]) ->
+ Fun = fun([IP1, IP2, IP3, IP4],
+ [IP5, IP6, IP7, IP8, M1, M2, M3, M4]) ->
+ is_permitted_ip([IP1, IP2, IP3, IP4],
+ [IP5, IP6, IP7, IP8],
+ [M1, M2, M3, M4]);
+ ([IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8,
+ IP9, IP10, IP11, IP12, IP13, IP14, IP15, IP16],
+ [IP17, IP18, IP19, IP20, IP21, IP22, IP23, IP24,
+ IP25, IP26, IP27, IP28, IP29, IP30, IP31, IP32,
+ M1, M2, M3, M4, M5, M6, M7, M8,
+ M9, M10, M11, M12, M13, M14, M15, M16]) ->
+ is_permitted_ip([IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8,
+ IP9, IP10, IP11, IP12, IP13,
+ IP14, IP15, IP16],
+ [IP17, IP18, IP19, IP20, IP21, IP22, IP23,
+ IP24,IP25, IP26, IP27, IP28, IP29, IP30,
+ IP31, IP32],
+ [M1, M2, M3, M4, M5, M6, M7, M8, M9, M10,
+ M11, M12, M13, M14, M15, M16]);
+ (_,_) ->
+ false
+ end,
+ match_name(Fun, IP, PermittedIP, Rest).
+
+match_name(Fun, Name, PermittedName, []) ->
+ Fun(Name, PermittedName);
+match_name(Fun, Name, PermittedName, [Head | Tail]) ->
+ case Fun(Name, PermittedName) of
+ true ->
+ true;
+ false ->
+ match_name(Fun, Name, Head, Tail)
+ end.
+
+is_permitted_ip([], [], []) ->
+ true;
+is_permitted_ip([CandidatIp | CandidatIpRest],
+ [PermittedIp | PermittedIpRest], [Mask | MaskRest] ) ->
+ case mask_cmp(CandidatIp, PermittedIp, Mask) of
+ true ->
+ is_permitted_ip(CandidatIpRest, PermittedIpRest, MaskRest);
+ false ->
+ false
+ end.
+
+mask_cmp(Canditate, Permitted, Mask) ->
+ (Canditate band Mask) == Permitted.
+
+is_valid_host_or_domain(Canditate, [$.|_] = Permitted) ->
+ is_suffix(Permitted, Canditate);
+is_valid_host_or_domain(Canditate, Permitted) ->
+ case string:tokens(Canditate,"@") of
+ [CanditateHost] ->
+ case_insensitive_match(CanditateHost, Permitted);
+ [_, CanditateHost] ->
+ case_insensitive_match(CanditateHost, Permitted)
+ end.
+is_valid_email_address(Canditate, [$.|Permitted], [_]) ->
+ is_suffix(Permitted, Canditate);
+
+is_valid_email_address(Canditate, PermittedHost, [_]) ->
+ [_ , CanditateHost] = string:tokens(Canditate,"@"),
+ case_insensitive_match(CanditateHost, PermittedHost);
+
+is_valid_email_address(Canditate, Permitted, [_, _]) ->
+ case_insensitive_match(Canditate, Permitted).
+
+is_suffix(Suffix, Str) ->
+ lists:suffix(string:to_lower(Suffix), string:to_lower(Str)).
+case_insensitive_match(Str1, Str2) ->
+ string:to_lower(Str1) == string:to_lower(Str2).
+
+is_or_address(Address, Canditate) ->
+ %% TODO: Is case_insensitive_match sufficient?
+ %% study rfc2156 probably need more a complex check.
+ is_double_quoted(Address) andalso
+ is_double_quoted(Canditate) andalso
+ case_insensitive_match(Address, Canditate).
+
+is_double_quoted(["\"" | Tail]) ->
+ is_double_quote(lists:last(Tail));
+is_double_quoted("%22" ++ Tail) ->
+ case lists:reverse(Tail) of
+ [A, B, C | _] ->
+ is_double_quote([C, B, A]);
+ _ ->
+ false
+ end;
+
+is_double_quoted(_) ->
+ false.
+
+is_double_quote("%22") ->
+ true;
+is_double_quote("\"") ->
+ true;
+is_double_quote(_) ->
+ false.
+
+add_policy_constraints(ExpPolicy, MapPolicy,
+ #path_validation_state{cert_num = CertNum,
+ explicit_policy = CurExpPolicy,
+ policy_mapping = CurMapPolicy} =
+ ValidationState) ->
+
+ NewExpPolicy = policy_constraint(CurExpPolicy, ExpPolicy, CertNum),
+ NewMapPolicy = policy_constraint(CurMapPolicy, MapPolicy, CertNum),
+
+ ValidationState#path_validation_state{explicit_policy = NewExpPolicy,
+ policy_mapping = NewMapPolicy}.
+
+policy_constraint(Current, asn1_NOVALUE, _) ->
+ Current;
+policy_constraint(Current, New, CertNum) ->
+ min(Current, New + CertNum).
+
+process_policy_tree(_,_, ?NULL) ->
+ ?NULL;
+process_policy_tree(_Id, _Qualifier, Tree) ->
+ %% TODO real imp.
+ Tree.
+
+policy_indicator(_, true) ->
+ 0;
+policy_indicator(N, false) ->
+ N + 1.
+
+is_fixed_dh_cert(PublicKeyInfo, Extensions) ->
+ AlgInfo = PublicKeyInfo#'OTPSubjectPublicKeyInfo'.algorithm,
+ Algorithm = AlgInfo#'PublicKeyAlgorithm'.algorithm,
+
+ case select_extension(?'id-ce-keyUsage', Extensions) of
+ undefined ->
+ is_dh(Algorithm);
+ #'Extension'{extnValue=KeyUse} ->
+ is_dh(Algorithm) andalso is_valid_key_usage(KeyUse, keyAgreement)
+ end.
+
+is_dh(?'dhpublicnumber')->
+ true;
+is_dh(_) ->
+ false.