1+ % % This Source Code Form is subject to the terms of the Mozilla Public
2+ % % License, v. 2.0. If a copy of the MPL was not distributed with this
3+ % % file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+ % %
5+ % % Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+ % %
7+ % % PATCHED VERSION: Fixed compilation errors in rabbit_common dependency
8+
9+ -module (rabbit_cert_info ).
10+
11+ -include_lib (" public_key/include/public_key.hrl" ).
12+
13+ -export ([issuer /1 ,
14+ subject /1 ,
15+ subject_alternative_names /1 ,
16+ validity /1 ,
17+ subject_items /2 ,
18+ extensions /1
19+ ]).
20+
21+ -export ([sanitize_other_name /1 ]).
22+
23+ % %--------------------------------------------------------------------------
24+
25+ -export_type ([certificate / 0 ]).
26+
27+ -type certificate () :: public_key :der_encoded ().
28+
29+ % % x.509 certificate extensions usually look like key/value pairs but can
30+ % % be just about any value
31+ -type certificate_extension_value () :: any ().
32+
33+ % %--------------------------------------------------------------------------
34+ % % High-level functions used by reader
35+ % %--------------------------------------------------------------------------
36+
37+ % % Return a string describing the certificate's issuer.
38+ -spec issuer (certificate ()) -> string ().
39+
40+ issuer (Cert ) ->
41+ cert_info (fun (# 'OTPCertificate' {
42+ tbsCertificate = # 'OTPTBSCertificate' {
43+ issuer = Issuer }}) ->
44+ format_rdn_sequence (Issuer )
45+ end , Cert ).
46+
47+ % % Return a string describing the certificate's subject, as per RFC4514.
48+ -spec subject (certificate ()) -> string ().
49+
50+ subject (Cert ) ->
51+ cert_info (fun (# 'OTPCertificate' {
52+ tbsCertificate = # 'OTPTBSCertificate' {
53+ subject = Subject }}) ->
54+ format_rdn_sequence (Subject )
55+ end , Cert ).
56+
57+ % % Return the parts of the certificate's subject.
58+ -spec subject_items
59+ (certificate (), tuple ()) -> [string ()] | 'not_found' .
60+
61+ subject_items (Cert , Type ) ->
62+ cert_info (fun (# 'OTPCertificate' {
63+ tbsCertificate = # 'OTPTBSCertificate' {
64+ subject = Subject }}) ->
65+ find_by_type (Type , Subject )
66+ end , Cert ).
67+
68+ -spec extensions (certificate ()) -> [# 'Extension' {}].
69+ extensions (Cert ) ->
70+ cert_info (fun (# 'OTPCertificate' {
71+ tbsCertificate = # 'OTPTBSCertificate' {
72+ extensions = Extensions }}) ->
73+ Extensions
74+ end , Cert ).
75+
76+ -spec subject_alternative_names (certificate ()) -> [certificate_extension_value ()].
77+ subject_alternative_names (Cert ) ->
78+ Extensions = extensions (Cert ),
79+ try lists :keyfind (?'id-ce-subjectAltName' , # 'Extension' .extnID , Extensions ) of
80+ false -> [];
81+ # 'Extension' {extnValue = Val } -> Val
82+ catch _ :_ -> []
83+ end .
84+
85+ % % Return a string describing the certificate's validity.
86+ -spec validity (certificate ()) -> string ().
87+
88+ validity (Cert ) ->
89+ cert_info (fun (# 'OTPCertificate' {
90+ tbsCertificate = # 'OTPTBSCertificate' {
91+ validity = {'Validity' , Start , End } }}) ->
92+ rabbit_misc :format (" ~ts - ~ts " , [format_asn1_value (Start ),
93+ format_asn1_value (End )])
94+ end , Cert ).
95+
96+ % %--------------------------------------------------------------------------
97+
98+ cert_info (F , Cert ) ->
99+ F (public_key :pkix_decode_cert (Cert , otp )).
100+
101+ find_by_type (Type , {rdnSequence , RDNs }) ->
102+ case [V || # 'AttributeTypeAndValue' {type = T , value = V }
103+ <- lists :flatten (RDNs ),
104+ T == Type ] of
105+ [] -> not_found ;
106+ L -> [format_asn1_value (V ) || V <- L ]
107+ end .
108+
109+ % %--------------------------------------------------------------------------
110+ % % Formatting functions
111+ % %--------------------------------------------------------------------------
112+
113+ sanitize_other_name (Bin ) when is_binary (Bin ) ->
114+ % % We make a wild assumption about the types here
115+ % % but ASN.1 decoding functions in OTP only offer so much and SAN values
116+ % % are expected to be "string-like" by RabbitMQ
117+ case 'OTP-PUB-KEY' :decode ('DirectoryString' , Bin ) of
118+ {ok , {_ , Val }} -> Val ;
119+ Other -> Other
120+ end .
121+
122+ % % Format and rdnSequence as a RFC4514 subject string.
123+ format_rdn_sequence ({rdnSequence , Seq }) ->
124+ string :join (lists :reverse ([format_complex_rdn (RDN ) || RDN <- Seq ]), " ," ).
125+
126+ % % Format an RDN set.
127+ format_complex_rdn (RDNs ) ->
128+ string :join ([format_rdn (RDN ) || RDN <- RDNs ], " +" ).
129+
130+ % % Format an RDN. If the type name is unknown, use the dotted decimal
131+ % % representation. See RFC4514, section 2.3.
132+ format_rdn (# 'AttributeTypeAndValue' {type = T , value = V }) ->
133+ FV = escape_rdn_value (format_asn1_value (V )),
134+ Fmts = [{?'id-at-surname' , " SN" },
135+ {?'id-at-givenName' , " GIVENNAME" },
136+ {?'id-at-initials' , " INITIALS" },
137+ {?'id-at-generationQualifier' , " GENERATIONQUALIFIER" },
138+ {?'id-at-commonName' , " CN" },
139+ {?'id-at-localityName' , " L" },
140+ {?'id-at-stateOrProvinceName' , " ST" },
141+ {?'id-at-organizationName' , " O" },
142+ {?'id-at-organizationalUnitName' , " OU" },
143+ {?'id-at-title' , " TITLE" },
144+ {?'id-at-countryName' , " C" },
145+ {?'id-at-serialNumber' , " SERIALNUMBER" },
146+ {?'id-at-pseudonym' , " PSEUDONYM" },
147+ {?'id-domainComponent' , " DC" },
148+ {?'id-emailAddress' , " EMAILADDRESS" },
149+ {{2 ,5 ,4 ,9 } , " STREET" }, % % FIXED: Use OID directly instead of macro
150+ {{0 ,9 ,2342 ,19200300 ,100 ,1 ,1 } , " UID" }], % % Not in public_key.hrl
151+ case proplists :lookup (T , Fmts ) of
152+ {_ , Fmt } ->
153+ rabbit_misc :format (Fmt ++ " =~ts " , [FV ]);
154+ none when is_tuple (T ) ->
155+ TypeL = [rabbit_misc :format (" ~w " , [X ]) || X <- tuple_to_list (T )],
156+ rabbit_misc :format (" ~ts =~ts " , [string :join (TypeL , " ." ), FV ]);
157+ none ->
158+ rabbit_misc :format (" ~tp =~ts " , [T , FV ])
159+ end .
160+
161+ % % Escape a string as per RFC4514.
162+ escape_rdn_value (V ) ->
163+ escape_rdn_value (V , start ).
164+
165+ escape_rdn_value ([], _ ) ->
166+ [];
167+ escape_rdn_value ([C | S ], start ) when C =:= $ ; C =:= $# ->
168+ [$\\ , C | escape_rdn_value (S , middle )];
169+ escape_rdn_value (S , start ) ->
170+ escape_rdn_value (S , middle );
171+ escape_rdn_value ([$ ], middle ) ->
172+ [$\\ , $ ];
173+ escape_rdn_value ([C | S ], middle ) when C =:= $" ; C =:= $+ ; C =:= $, ; C =:= $; ;
174+ C =:= $< ; C =:= $> ; C =:= $\\ ->
175+ [$\\ , C | escape_rdn_value (S , middle )];
176+ escape_rdn_value ([C | S ], middle ) when C < 32 ; C >= 126 ->
177+ % % Of ASCII characters only U+0000 needs escaping, but for display
178+ % % purposes it's handy to escape all non-printable chars. All non-ASCII
179+ % % characters get converted to UTF-8 sequences and then escaped. We've
180+ % % already got a UTF-8 sequence here, so just escape it.
181+ rabbit_misc :format (" \\ ~2.16.0B " , [C ]) ++ escape_rdn_value (S , middle );
182+ escape_rdn_value ([C | S ], middle ) ->
183+ [C | escape_rdn_value (S , middle )].
184+
185+ % % Get the string representation of an OTPCertificate field.
186+ format_asn1_value ({ST , S }) when ST =:= teletexString ; ST =:= printableString ;
187+ ST =:= universalString ; ST =:= utf8String ;
188+ ST =:= bmpString ->
189+ format_directory_string (ST , S );
190+ format_asn1_value ({utcTime , [Y1 , Y2 , M1 , M2 , D1 , D2 , H1 , H2 ,
191+ Min1 , Min2 , S1 , S2 , $Z ]}) ->
192+ rabbit_misc :format (" 20~c~c -~c~c -~c~c T~c~c :~c~c :~c~c Z" ,
193+ [Y1 , Y2 , M1 , M2 , D1 , D2 , H1 , H2 , Min1 , Min2 , S1 , S2 ]);
194+ % % We appear to get an untagged value back for an ia5string
195+ % % (e.g. domainComponent).
196+ format_asn1_value (V ) when is_list (V ) ->
197+ V ;
198+ format_asn1_value (V ) when is_binary (V ) ->
199+ % % OTP does not decode some values when combined with an unknown
200+ % % type. That's probably wrong, so as a last ditch effort let's
201+ % % try manually decoding. 'DirectoryString' is semi-arbitrary -
202+ % % but it is the type which covers the various string types we
203+ % % handle below.
204+ try
205+ {ST , S } = public_key :der_decode ('DirectoryString' , V ),
206+ format_directory_string (ST , S )
207+ catch _ :_ ->
208+ rabbit_misc :format (" ~tp " , [V ])
209+ end ;
210+ format_asn1_value (V ) ->
211+ rabbit_misc :format (" ~tp " , [V ]).
212+
213+ % % DirectoryString { INTEGER : maxSize } ::= CHOICE {
214+ % % teletexString TeletexString (SIZE (1..maxSize)),
215+ % % printableString PrintableString (SIZE (1..maxSize)),
216+ % % bmpString BMPString (SIZE (1..maxSize)),
217+ % % universalString UniversalString (SIZE (1..maxSize)),
218+ % % uTF8String UTF8String (SIZE (1..maxSize)) }
219+ % %
220+ % % Precise definitions of printable / teletexString are hard to come
221+ % % by. This is what I reconstructed:
222+ % %
223+ % % printableString:
224+ % % "intended to represent the limited character sets available to
225+ % % mainframe input terminals"
226+ % % A-Z a-z 0-9 ' ( ) + , - . / : = ? [space]
227+ % % https://msdn.microsoft.com/en-us/library/bb540814(v=vs.85).aspx
228+ % %
229+ % % teletexString:
230+ % % "a sizable volume of software in the world treats TeletexString
231+ % % (T61String) as a simple 8-bit string with mostly Windows Latin 1
232+ % % (superset of iso-8859-1) encoding"
233+ % % https://www.mail-archive.com/[email protected] /msg00460.html234+ % %
235+ % % (However according to that link X.680 actually defines
236+ % % TeletexString in some much more involved and crazy way. I suggest
237+ % % we treat it as ISO-8859-1 since Erlang does not support Windows
238+ % % Latin 1).
239+ % %
240+ % % bmpString:
241+ % % UCS-2 according to RFC 3641. Hence cannot represent Unicode
242+ % % characters above 65535 (outside the "Basic Multilingual Plane").
243+ % %
244+ % % universalString:
245+ % % UCS-4 according to RFC 3641.
246+ % %
247+ % % utf8String:
248+ % % UTF-8 according to RFC 3641.
249+ % %
250+ % % Within Rabbit we assume UTF-8 encoding. Since printableString is a
251+ % % subset of ASCII it is also a subset of UTF-8. The others need
252+ % % converting. Fortunately since the Erlang SSL library does the
253+ % % decoding for us (albeit into a weird format, see below), we just
254+ % % need to handle encoding into UTF-8. Note also that utf8Strings come
255+ % % back as binary.
256+ % %
257+ % % Note for testing: the default Ubuntu configuration for openssl will
258+ % % only create printableString or teletexString types no matter what
259+ % % you do. Edit string_mask in the [req] section of
260+ % % /etc/ssl/openssl.cnf to change this (see comments there). You
261+ % % probably also need to set utf8 = yes to get it to accept UTF-8 on
262+ % % the command line. Also note I could not get openssl to generate a
263+ % % universalString.
264+
265+ format_directory_string (printableString , S ) -> S ;
266+ format_directory_string (teletexString , S ) -> utf8_list_from (S );
267+ format_directory_string (bmpString , S ) -> utf8_list_from (S );
268+ format_directory_string (universalString , S ) -> utf8_list_from (S );
269+ format_directory_string (utf8String , S ) -> binary_to_list (S ).
270+
271+ utf8_list_from (S ) ->
272+ binary_to_list (
273+ unicode :characters_to_binary (flatten_ssl_list (S ), utf32 , utf8 )).
274+
275+ % % The Erlang SSL implementation invents its own representation for
276+ % % non-ascii strings - looking like [97,{0,0,3,187}] (that's LATIN
277+ % % SMALL LETTER A followed by GREEK SMALL LETTER LAMDA). We convert
278+ % % this into a list of unicode characters, which we can tell
279+ % % unicode:characters_to_binary is utf32.
280+
281+ flatten_ssl_list (L ) -> [flatten_ssl_list_item (I ) || I <- L ].
282+
283+ flatten_ssl_list_item ({A , B , C , D }) ->
284+ A * (1 bsl 24 ) + B * (1 bsl 16 ) + C * (1 bsl 8 ) + D ;
285+ flatten_ssl_list_item (N ) when is_number (N ) ->
286+ N .
0 commit comments