Skip to content

Commit e6cc808

Browse files
authored
Merge pull request #7543 from u3s/kuba/ssl/client_ocsp/OTP-18606
Kuba/ssl/client ocsp/otp 18606
2 parents 35dfde2 + 8e20f46 commit e6cc808

33 files changed

+995
-865
lines changed

lib/public_key/doc/src/public_key.xml

+25
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,31 @@ fun(#'DistributionPoint'{}, #'CertificateList'{},
771771
</desc>
772772
</func>
773773

774+
<func>
775+
<name name="pkix_ocsp_validate" arity="5" since="OTP 27.0"/>
776+
<fsummary>Validate OCSP response.</fsummary>
777+
<desc>
778+
<p>Perform OCSP response validation according to RFC
779+
6960. Returns 'ok' when OCSP response is successfully
780+
validated and {error, {bad_cert, Reason}} otherwise.</p>
781+
782+
<p>Available options:</p>
783+
<taglist>
784+
<tag>{is_trusted_responder_fun, fun()}</tag>
785+
<item>
786+
<p>The fun has the following type specification:</p>
787+
<code> fun(#cert{}) ->
788+
boolean()</code>
789+
<p>The fun returns the <c>true</c> if certificate in the
790+
argument is trusted. If this fun is not specified, Public
791+
Key uses the default implementation:
792+
</p>
793+
<code> fun(_) -> false end</code>
794+
</item>
795+
</taglist>
796+
</desc>
797+
</func>
798+
774799
<func>
775800
<name name="pkix_sign" arity="2" since="OTP R14B"/>
776801
<fsummary>Signs certificate.</fsummary>

lib/public_key/src/pubkey_ocsp.erl

+135-72
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,18 @@
1919
%%
2020

2121
-module(pubkey_ocsp).
22+
-feature(maybe_expr,enable).
2223
-include("public_key.hrl").
2324

2425
-export([find_single_response/3,
2526
get_acceptable_response_types_extn/0,
2627
get_nonce_extn/1,
27-
get_ocsp_responder_id/1,
28-
ocsp_status/1,
29-
verify_ocsp_response/3,
30-
decode_ocsp_response/1]).
28+
status/1,
29+
verify_response/5,
30+
decode_response/1]).
3131
%% Tracing
3232
-export([handle_trace/3]).
3333

34-
-spec get_ocsp_responder_id(#'Certificate'{}) -> binary().
35-
get_ocsp_responder_id(#'Certificate'{tbsCertificate = TbsCert}) ->
36-
public_key:der_encode(
37-
'ResponderID', {byName, TbsCert#'TBSCertificate'.subject}).
38-
3934
-spec get_nonce_extn(undefined | binary()) -> undefined | #'Extension'{}.
4035
get_nonce_extn(undefined) ->
4136
undefined;
@@ -45,10 +40,29 @@ get_nonce_extn(Nonce) when is_binary(Nonce) ->
4540
extnValue = Nonce
4641
}.
4742

48-
-spec verify_ocsp_response(#'BasicOCSPResponse'{}, list(), undefined | binary()) ->
43+
-spec verify_response(#'BasicOCSPResponse'{}, list(), undefined | binary(),
44+
public_key:cert(), fun()) ->
4945
{ok, term()} | {error, term()}.
50-
verify_ocsp_response(OCSPResponse, ResponderCerts, Nonce) ->
51-
do_verify_ocsp_response(OCSPResponse, ResponderCerts, Nonce).
46+
verify_response(#'BasicOCSPResponse'{
47+
tbsResponseData = ResponseData,
48+
signatureAlgorithm = SignatureAlgo,
49+
signature = Signature},
50+
ResponderCerts, Nonce, IssuerCert,
51+
IsTrustedResponderFun) ->
52+
#'ResponseData'{responderID = ResponderID,
53+
producedAt = ProducedAt} = ResponseData,
54+
maybe
55+
ok ?= verify_past_timestamp(ProducedAt),
56+
ok ?= verify_signature(
57+
public_key:der_encode('ResponseData', ResponseData),
58+
SignatureAlgo#'AlgorithmIdentifier'.algorithm,
59+
Signature, ResponderCerts,
60+
ResponderID, IssuerCert, IsTrustedResponderFun),
61+
verify_nonce(ResponseData, Nonce)
62+
else
63+
{error, Reason} ->
64+
{error, Reason}
65+
end.
5266

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

70-
-spec ocsp_status({atom(), term()}) -> atom() | {atom(), {atom(), term()}}.
71-
ocsp_status({good, _}) ->
72-
valid;
73-
ocsp_status({unknown, Reason}) ->
74-
{bad_cert, {revocation_status_undetermined, Reason}};
75-
ocsp_status({revoked, Reason}) ->
76-
{bad_cert, {revoked, Reason}}.
84+
-spec status({atom(), term()}) -> ok | {error, {bad_cert, term()}}.
85+
status({good, _}) ->
86+
ok;
87+
status({unknown, Reason}) ->
88+
{error, {bad_cert, {revocation_status_undetermined, Reason}}};
89+
status({revoked, Reason}) ->
90+
{error, {bad_cert, {revoked, Reason}}}.
7791

78-
decode_ocsp_response(ResponseDer) ->
92+
decode_response(ResponseDer) ->
7993
Resp = public_key:der_decode('OCSPResponse', ResponseDer),
8094
case Resp#'OCSPResponse'.responseStatus of
8195
successful ->
@@ -92,16 +106,23 @@ match_single_response(_IssuerName, _IssuerKey, _SerialNum, []) ->
92106
match_single_response(IssuerName, IssuerKey, SerialNum,
93107
[#'SingleResponse'{
94108
certID = #'CertID'{hashAlgorithm = Algo} = CertID} =
95-
Response | Responses]) ->
109+
SingleResponse | Tail]) ->
110+
#'SingleResponse'{thisUpdate = ThisUpdate,
111+
nextUpdate = NextUpdate} = SingleResponse,
96112
HashType = public_key:pkix_hash_type(Algo#'AlgorithmIdentifier'.algorithm),
97113
case (SerialNum == CertID#'CertID'.serialNumber) andalso
98114
(crypto:hash(HashType, IssuerName) == CertID#'CertID'.issuerNameHash) andalso
99-
(crypto:hash(HashType, IssuerKey) == CertID#'CertID'.issuerKeyHash) of
115+
(crypto:hash(HashType, IssuerKey) == CertID#'CertID'.issuerKeyHash) andalso
116+
verify_past_timestamp(ThisUpdate) == ok andalso
117+
verify_next_update(NextUpdate) == ok of
100118
true ->
101-
{ok, Response};
119+
{ok, SingleResponse};
102120
false ->
103-
match_single_response(IssuerName, IssuerKey, SerialNum, Responses)
104-
end.
121+
match_single_response(IssuerName, IssuerKey, SerialNum, Tail)
122+
end;
123+
match_single_response(IssuerName, IssuerKey, SerialNum,
124+
[_BadSingleResponse | Tail]) ->
125+
match_single_response(IssuerName, IssuerKey, SerialNum, Tail).
105126

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

116-
do_verify_ocsp_response(#'BasicOCSPResponse'{
117-
tbsResponseData = ResponseData,
118-
signatureAlgorithm = SignatureAlgo,
119-
signature = Signature},
120-
ResponderCerts, Nonce) ->
121-
#'ResponseData'{responderID = ResponderID} = ResponseData,
122-
case verify_ocsp_signature(
123-
public_key:der_encode('ResponseData', ResponseData),
124-
SignatureAlgo#'AlgorithmIdentifier'.algorithm,
125-
Signature, ResponderCerts,
126-
ResponderID) of
127-
ok ->
128-
verify_ocsp_nonce(ResponseData, Nonce);
129-
{error, Reason} ->
130-
{error, Reason}
131-
end.
132-
133-
verify_ocsp_nonce(ResponseData, Nonce) ->
137+
verify_nonce(ResponseData, Nonce) ->
134138
#'ResponseData'{responses = Responses, responseExtensions = ResponseExtns} =
135139
ResponseData,
136140
case get_nonce_value(ResponseExtns) of
@@ -153,31 +157,91 @@ get_nonce_value([#'Extension'{
153157
get_nonce_value([_Extn | Rest]) ->
154158
get_nonce_value(Rest).
155159

156-
verify_ocsp_signature(ResponseDataDer, SignatureAlgo, Signature,
157-
Certs, ResponderID) ->
158-
case find_responder_cert(ResponderID, Certs) of
159-
{ok, Cert} ->
160-
do_verify_ocsp_signature(
161-
ResponseDataDer, Signature, SignatureAlgo, Cert);
162-
{error, Reason} ->
163-
{error, Reason}
160+
verify_signature(_, _, _, [], _, _, _) ->
161+
{error, ocsp_responder_cert_not_found};
162+
verify_signature(ResponseDataDer, SignatureAlgo, Signature,
163+
[ResponderCert | RCs], ResponderID, IssuerCert,
164+
IsTrustedResponderFun) ->
165+
maybe
166+
true ?= is_responder_cert(ResponderID, ResponderCert),
167+
true ?= is_authorized_responder(ResponderCert, IssuerCert,
168+
IsTrustedResponderFun),
169+
ok ?= do_verify_signature(ResponseDataDer, Signature, SignatureAlgo,
170+
ResponderCert)
171+
else
172+
_->
173+
verify_signature(ResponseDataDer, SignatureAlgo, Signature,
174+
RCs, ResponderID, IssuerCert,
175+
IsTrustedResponderFun)
164176
end.
165177

166-
find_responder_cert(_ResponderID, []) ->
167-
{error, ocsp_responder_cert_not_found};
168-
find_responder_cert(ResponderID, [Cert | TCerts]) ->
169-
case is_responder(ResponderID, Cert) of
178+
verify_past_timestamp(Timestamp) ->
179+
{Now, TimestampSec} = get_time_in_sec(Timestamp),
180+
verify_timestamp(Now, TimestampSec, past_timestamp).
181+
182+
verify_future_timestamp(Timestamp) ->
183+
{Now, TimestampSec} = get_time_in_sec(Timestamp),
184+
verify_timestamp(Now, TimestampSec, future_timestamp).
185+
186+
verify_timestamp(Now, Timestamp, past_timestamp) when Timestamp =< Now ->
187+
ok;
188+
verify_timestamp(Now, Timestamp, future_timestamp) when Now =< Timestamp ->
189+
ok;
190+
verify_timestamp(_, _, _) ->
191+
{error, ocsp_stale_response}.
192+
193+
get_time_in_sec(Timestamp) ->
194+
Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
195+
TimestampSec = pubkey_cert:time_str_2_gregorian_sec(
196+
{generalTime, Timestamp}),
197+
{Now, TimestampSec}.
198+
199+
verify_next_update(asn1_NOVALUE) ->
200+
ok;
201+
verify_next_update(NextUpdate) ->
202+
verify_future_timestamp(NextUpdate).
203+
204+
is_responder_cert({byName, Name}, #cert{otp = Cert}) ->
205+
public_key:der_encode('Name', Name) == get_subject_name(Cert);
206+
is_responder_cert({byKey, Key}, #cert{otp = Cert}) ->
207+
Key == crypto:hash(sha, get_public_key(Cert)).
208+
209+
is_authorized_responder(CombinedResponderCert = #cert{otp = ResponderCert},
210+
IssuerCert, IsTrustedResponderFun) ->
211+
Case1 =
212+
%% the CA who issued the certificate in question signed the
213+
%% response
214+
fun() ->
215+
ResponderCert == IssuerCert
216+
end,
217+
Case2 =
218+
%% a CA Designated Responder (Authorized Responder, defined in
219+
%% Section 4.2.2.2) who holds a specially marked certificate
220+
%% issued directly by the CA, indicating that the responder may
221+
%% issue OCSP responses for that CA (id-kp-OCSPSigning)
222+
fun() ->
223+
public_key:pkix_is_issuer(ResponderCert, IssuerCert) andalso
224+
designated_for_ocsp_signing(ResponderCert)
225+
end,
226+
Case3 =
227+
%% a Trusted Responder whose public key is trusted by the requestor
228+
fun() ->
229+
IsTrustedResponderFun(CombinedResponderCert)
230+
end,
231+
232+
case lists:any(fun(E) -> E() end, [Case1, Case2, Case3]) of
170233
true ->
171-
{ok, Cert};
234+
true;
172235
false ->
173-
find_responder_cert(ResponderID, TCerts)
236+
not_authorized_responder
174237
end.
175238

176-
do_verify_ocsp_signature(ResponseDataDer, Signature, AlgorithmID, Cert) ->
239+
do_verify_signature(ResponseDataDer, Signature, AlgorithmID,
240+
#cert{otp = ResponderCert}) ->
177241
{DigestType, _SignatureType} = public_key:pkix_sign_types(AlgorithmID),
178242
case public_key:verify(
179243
ResponseDataDer, DigestType, Signature,
180-
get_public_key_rec(Cert)) of
244+
get_public_key_rec(ResponderCert)) of
181245
true ->
182246
ok;
183247
false ->
@@ -188,11 +252,6 @@ get_public_key_rec(#'OTPCertificate'{tbsCertificate = TbsCert}) ->
188252
PKInfo = TbsCert#'OTPTBSCertificate'.subjectPublicKeyInfo,
189253
PKInfo#'OTPSubjectPublicKeyInfo'.subjectPublicKey.
190254

191-
is_responder({byName, Name}, Cert) ->
192-
public_key:der_encode('Name', Name) == get_subject_name(Cert);
193-
is_responder({byKey, Key}, Cert) ->
194-
Key == crypto:hash(sha, get_public_key(Cert)).
195-
196255
get_subject_name(#'OTPCertificate'{tbsCertificate = TbsCert}) ->
197256
public_key:pkix_encode('Name', TbsCert#'OTPTBSCertificate'.subject, otp).
198257

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

269+
designated_for_ocsp_signing(OtpCert) ->
270+
TBSCert = OtpCert#'OTPCertificate'.tbsCertificate,
271+
TBSExtensions = TBSCert#'OTPTBSCertificate'.extensions,
272+
Extensions = pubkey_cert:extensions_list(TBSExtensions),
273+
case pubkey_cert:select_extension(?'id-ce-extKeyUsage', Extensions) of
274+
undefined ->
275+
false;
276+
#'Extension'{extnValue = KeyUses} ->
277+
lists:member(?'id-kp-OCSPSigning', KeyUses)
278+
end.
279+
210280
%%%################################################################
211281
%%%#
212282
%%%# Tracing
213283
%%%#
214-
handle_trace(csp,
215-
{call, {?MODULE, do_verify_ocsp_response, [BasicOcspResponse | _]}}, Stack) ->
216-
#'BasicOCSPResponse'{
217-
tbsResponseData =
218-
#'ResponseData'{responderID = ResponderID,
219-
producedAt = ProducedAt}} = BasicOcspResponse,
220-
{io_lib:format("ResponderId = ~W producedAt = ~p", [ResponderID, 5, ProducedAt]), Stack};
221284
handle_trace(csp,
222285
{call, {?MODULE, match_single_response,
223286
[_IssuerName, _IssuerKey, _SerialNum,
224287
[#'SingleResponse'{thisUpdate = ThisUpdate,
225-
nextUpdate = NextUpdate}]]}}, Stack) ->
288+
nextUpdate = NextUpdate} | _]]}}, Stack) ->
226289
{io_lib:format("ThisUpdate = ~p NextUpdate = ~p", [ThisUpdate, NextUpdate]), Stack};
227290
handle_trace(csp,
228291
{call, {?MODULE, is_responder, [Id, Cert]}}, Stack) ->

0 commit comments

Comments
 (0)