Skip to content

Commit

Permalink
Merge pull request #7543 from u3s/kuba/ssl/client_ocsp/OTP-18606
Browse files Browse the repository at this point in the history
Kuba/ssl/client ocsp/otp 18606
  • Loading branch information
u3s authored Jan 29, 2024
2 parents 35dfde2 + 8e20f46 commit e6cc808
Show file tree
Hide file tree
Showing 33 changed files with 995 additions and 865 deletions.
25 changes: 25 additions & 0 deletions lib/public_key/doc/src/public_key.xml
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,31 @@ fun(#'DistributionPoint'{}, #'CertificateList'{},
</desc>
</func>

<func>
<name name="pkix_ocsp_validate" arity="5" since="OTP 27.0"/>
<fsummary>Validate OCSP response.</fsummary>
<desc>
<p>Perform OCSP response validation according to RFC
6960. Returns 'ok' when OCSP response is successfully
validated and {error, {bad_cert, Reason}} otherwise.</p>

<p>Available options:</p>
<taglist>
<tag>{is_trusted_responder_fun, fun()}</tag>
<item>
<p>The fun has the following type specification:</p>
<code> fun(#cert{}) ->
boolean()</code>
<p>The fun returns the <c>true</c> if certificate in the
argument is trusted. If this fun is not specified, Public
Key uses the default implementation:
</p>
<code> fun(_) -> false end</code>
</item>
</taglist>
</desc>
</func>

<func>
<name name="pkix_sign" arity="2" since="OTP R14B"/>
<fsummary>Signs certificate.</fsummary>
Expand Down
207 changes: 135 additions & 72 deletions lib/public_key/src/pubkey_ocsp.erl
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,18 @@
%%

-module(pubkey_ocsp).
-feature(maybe_expr,enable).
-include("public_key.hrl").

-export([find_single_response/3,
get_acceptable_response_types_extn/0,
get_nonce_extn/1,
get_ocsp_responder_id/1,
ocsp_status/1,
verify_ocsp_response/3,
decode_ocsp_response/1]).
status/1,
verify_response/5,
decode_response/1]).
%% Tracing
-export([handle_trace/3]).

-spec get_ocsp_responder_id(#'Certificate'{}) -> binary().
get_ocsp_responder_id(#'Certificate'{tbsCertificate = TbsCert}) ->
public_key:der_encode(
'ResponderID', {byName, TbsCert#'TBSCertificate'.subject}).

-spec get_nonce_extn(undefined | binary()) -> undefined | #'Extension'{}.
get_nonce_extn(undefined) ->
undefined;
Expand All @@ -45,10 +40,29 @@ get_nonce_extn(Nonce) when is_binary(Nonce) ->
extnValue = Nonce
}.

-spec verify_ocsp_response(#'BasicOCSPResponse'{}, list(), undefined | binary()) ->
-spec verify_response(#'BasicOCSPResponse'{}, list(), undefined | binary(),
public_key:cert(), fun()) ->
{ok, term()} | {error, term()}.
verify_ocsp_response(OCSPResponse, ResponderCerts, Nonce) ->
do_verify_ocsp_response(OCSPResponse, ResponderCerts, Nonce).
verify_response(#'BasicOCSPResponse'{
tbsResponseData = ResponseData,
signatureAlgorithm = SignatureAlgo,
signature = Signature},
ResponderCerts, Nonce, IssuerCert,
IsTrustedResponderFun) ->
#'ResponseData'{responderID = ResponderID,
producedAt = ProducedAt} = ResponseData,
maybe
ok ?= verify_past_timestamp(ProducedAt),
ok ?= verify_signature(
public_key:der_encode('ResponseData', ResponseData),
SignatureAlgo#'AlgorithmIdentifier'.algorithm,
Signature, ResponderCerts,
ResponderID, IssuerCert, IsTrustedResponderFun),
verify_nonce(ResponseData, Nonce)
else
{error, Reason} ->
{error, Reason}
end.

-spec get_acceptable_response_types_extn() -> #'Extension'{}.
get_acceptable_response_types_extn() ->
Expand All @@ -67,15 +81,15 @@ find_single_response(Cert, IssuerCert, SingleResponseList) ->
SerialNum = get_serial_num(Cert),
match_single_response(IssuerName, IssuerKey, SerialNum, SingleResponseList).

-spec ocsp_status({atom(), term()}) -> atom() | {atom(), {atom(), term()}}.
ocsp_status({good, _}) ->
valid;
ocsp_status({unknown, Reason}) ->
{bad_cert, {revocation_status_undetermined, Reason}};
ocsp_status({revoked, Reason}) ->
{bad_cert, {revoked, Reason}}.
-spec status({atom(), term()}) -> ok | {error, {bad_cert, term()}}.
status({good, _}) ->
ok;
status({unknown, Reason}) ->
{error, {bad_cert, {revocation_status_undetermined, Reason}}};
status({revoked, Reason}) ->
{error, {bad_cert, {revoked, Reason}}}.

decode_ocsp_response(ResponseDer) ->
decode_response(ResponseDer) ->
Resp = public_key:der_decode('OCSPResponse', ResponseDer),
case Resp#'OCSPResponse'.responseStatus of
successful ->
Expand All @@ -92,16 +106,23 @@ match_single_response(_IssuerName, _IssuerKey, _SerialNum, []) ->
match_single_response(IssuerName, IssuerKey, SerialNum,
[#'SingleResponse'{
certID = #'CertID'{hashAlgorithm = Algo} = CertID} =
Response | Responses]) ->
SingleResponse | Tail]) ->
#'SingleResponse'{thisUpdate = ThisUpdate,
nextUpdate = NextUpdate} = SingleResponse,
HashType = public_key:pkix_hash_type(Algo#'AlgorithmIdentifier'.algorithm),
case (SerialNum == CertID#'CertID'.serialNumber) andalso
(crypto:hash(HashType, IssuerName) == CertID#'CertID'.issuerNameHash) andalso
(crypto:hash(HashType, IssuerKey) == CertID#'CertID'.issuerKeyHash) of
(crypto:hash(HashType, IssuerKey) == CertID#'CertID'.issuerKeyHash) andalso
verify_past_timestamp(ThisUpdate) == ok andalso
verify_next_update(NextUpdate) == ok of
true ->
{ok, Response};
{ok, SingleResponse};
false ->
match_single_response(IssuerName, IssuerKey, SerialNum, Responses)
end.
match_single_response(IssuerName, IssuerKey, SerialNum, Tail)
end;
match_single_response(IssuerName, IssuerKey, SerialNum,
[_BadSingleResponse | Tail]) ->
match_single_response(IssuerName, IssuerKey, SerialNum, Tail).

get_serial_num(#'OTPCertificate'{tbsCertificate = TbsCert}) ->
TbsCert#'OTPTBSCertificate'.serialNumber.
Expand All @@ -113,24 +134,7 @@ decode_response_bytes(#'ResponseBytes'{
decode_response_bytes(#'ResponseBytes'{responseType = RespType}) ->
{error, {ocsp_response_type_not_supported, RespType}}.

do_verify_ocsp_response(#'BasicOCSPResponse'{
tbsResponseData = ResponseData,
signatureAlgorithm = SignatureAlgo,
signature = Signature},
ResponderCerts, Nonce) ->
#'ResponseData'{responderID = ResponderID} = ResponseData,
case verify_ocsp_signature(
public_key:der_encode('ResponseData', ResponseData),
SignatureAlgo#'AlgorithmIdentifier'.algorithm,
Signature, ResponderCerts,
ResponderID) of
ok ->
verify_ocsp_nonce(ResponseData, Nonce);
{error, Reason} ->
{error, Reason}
end.

verify_ocsp_nonce(ResponseData, Nonce) ->
verify_nonce(ResponseData, Nonce) ->
#'ResponseData'{responses = Responses, responseExtensions = ResponseExtns} =
ResponseData,
case get_nonce_value(ResponseExtns) of
Expand All @@ -153,31 +157,91 @@ get_nonce_value([#'Extension'{
get_nonce_value([_Extn | Rest]) ->
get_nonce_value(Rest).

verify_ocsp_signature(ResponseDataDer, SignatureAlgo, Signature,
Certs, ResponderID) ->
case find_responder_cert(ResponderID, Certs) of
{ok, Cert} ->
do_verify_ocsp_signature(
ResponseDataDer, Signature, SignatureAlgo, Cert);
{error, Reason} ->
{error, Reason}
verify_signature(_, _, _, [], _, _, _) ->
{error, ocsp_responder_cert_not_found};
verify_signature(ResponseDataDer, SignatureAlgo, Signature,
[ResponderCert | RCs], ResponderID, IssuerCert,
IsTrustedResponderFun) ->
maybe
true ?= is_responder_cert(ResponderID, ResponderCert),
true ?= is_authorized_responder(ResponderCert, IssuerCert,
IsTrustedResponderFun),
ok ?= do_verify_signature(ResponseDataDer, Signature, SignatureAlgo,
ResponderCert)
else
_->
verify_signature(ResponseDataDer, SignatureAlgo, Signature,
RCs, ResponderID, IssuerCert,
IsTrustedResponderFun)
end.

find_responder_cert(_ResponderID, []) ->
{error, ocsp_responder_cert_not_found};
find_responder_cert(ResponderID, [Cert | TCerts]) ->
case is_responder(ResponderID, Cert) of
verify_past_timestamp(Timestamp) ->
{Now, TimestampSec} = get_time_in_sec(Timestamp),
verify_timestamp(Now, TimestampSec, past_timestamp).

verify_future_timestamp(Timestamp) ->
{Now, TimestampSec} = get_time_in_sec(Timestamp),
verify_timestamp(Now, TimestampSec, future_timestamp).

verify_timestamp(Now, Timestamp, past_timestamp) when Timestamp =< Now ->
ok;
verify_timestamp(Now, Timestamp, future_timestamp) when Now =< Timestamp ->
ok;
verify_timestamp(_, _, _) ->
{error, ocsp_stale_response}.

get_time_in_sec(Timestamp) ->
Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
TimestampSec = pubkey_cert:time_str_2_gregorian_sec(
{generalTime, Timestamp}),
{Now, TimestampSec}.

verify_next_update(asn1_NOVALUE) ->
ok;
verify_next_update(NextUpdate) ->
verify_future_timestamp(NextUpdate).

is_responder_cert({byName, Name}, #cert{otp = Cert}) ->
public_key:der_encode('Name', Name) == get_subject_name(Cert);
is_responder_cert({byKey, Key}, #cert{otp = Cert}) ->
Key == crypto:hash(sha, get_public_key(Cert)).

is_authorized_responder(CombinedResponderCert = #cert{otp = ResponderCert},
IssuerCert, IsTrustedResponderFun) ->
Case1 =
%% the CA who issued the certificate in question signed the
%% response
fun() ->
ResponderCert == IssuerCert
end,
Case2 =
%% a CA Designated Responder (Authorized Responder, defined in
%% Section 4.2.2.2) who holds a specially marked certificate
%% issued directly by the CA, indicating that the responder may
%% issue OCSP responses for that CA (id-kp-OCSPSigning)
fun() ->
public_key:pkix_is_issuer(ResponderCert, IssuerCert) andalso
designated_for_ocsp_signing(ResponderCert)
end,
Case3 =
%% a Trusted Responder whose public key is trusted by the requestor
fun() ->
IsTrustedResponderFun(CombinedResponderCert)
end,

case lists:any(fun(E) -> E() end, [Case1, Case2, Case3]) of
true ->
{ok, Cert};
true;
false ->
find_responder_cert(ResponderID, TCerts)
not_authorized_responder
end.

do_verify_ocsp_signature(ResponseDataDer, Signature, AlgorithmID, Cert) ->
do_verify_signature(ResponseDataDer, Signature, AlgorithmID,
#cert{otp = ResponderCert}) ->
{DigestType, _SignatureType} = public_key:pkix_sign_types(AlgorithmID),
case public_key:verify(
ResponseDataDer, DigestType, Signature,
get_public_key_rec(Cert)) of
get_public_key_rec(ResponderCert)) of
true ->
ok;
false ->
Expand All @@ -188,11 +252,6 @@ get_public_key_rec(#'OTPCertificate'{tbsCertificate = TbsCert}) ->
PKInfo = TbsCert#'OTPTBSCertificate'.subjectPublicKeyInfo,
PKInfo#'OTPSubjectPublicKeyInfo'.subjectPublicKey.

is_responder({byName, Name}, Cert) ->
public_key:der_encode('Name', Name) == get_subject_name(Cert);
is_responder({byKey, Key}, Cert) ->
Key == crypto:hash(sha, get_public_key(Cert)).

get_subject_name(#'OTPCertificate'{tbsCertificate = TbsCert}) ->
public_key:pkix_encode('Name', TbsCert#'OTPTBSCertificate'.subject, otp).

Expand All @@ -207,22 +266,26 @@ enc_pub_key({DsaInt, #'Dss-Parms'{}}) when is_integer(DsaInt) ->
enc_pub_key({#'ECPoint'{point = Key}, _ECParam}) ->
Key.

designated_for_ocsp_signing(OtpCert) ->
TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
TBSExtensions = TBSCert#'OTPTBSCertificate'.extensions,
Extensions = pubkey_cert:extensions_list(TBSExtensions),
case pubkey_cert:select_extension(?'id-ce-extKeyUsage', Extensions) of
undefined ->
false;
#'Extension'{extnValue = KeyUses} ->
lists:member(?'id-kp-OCSPSigning', KeyUses)
end.

%%%################################################################
%%%#
%%%# Tracing
%%%#
handle_trace(csp,
{call, {?MODULE, do_verify_ocsp_response, [BasicOcspResponse | _]}}, Stack) ->
#'BasicOCSPResponse'{
tbsResponseData =
#'ResponseData'{responderID = ResponderID,
producedAt = ProducedAt}} = BasicOcspResponse,
{io_lib:format("ResponderId = ~W producedAt = ~p", [ResponderID, 5, ProducedAt]), Stack};
handle_trace(csp,
{call, {?MODULE, match_single_response,
[_IssuerName, _IssuerKey, _SerialNum,
[#'SingleResponse'{thisUpdate = ThisUpdate,
nextUpdate = NextUpdate}]]}}, Stack) ->
nextUpdate = NextUpdate} | _]]}}, Stack) ->
{io_lib:format("ThisUpdate = ~p NextUpdate = ~p", [ThisUpdate, NextUpdate]), Stack};
handle_trace(csp,
{call, {?MODULE, is_responder, [Id, Cert]}}, Stack) ->
Expand Down
Loading

0 comments on commit e6cc808

Please sign in to comment.