Skip to content

Commit 27aede2

Browse files
committed
Test with otp 28 with path file
1 parent 1dc175c commit 27aede2

File tree

2 files changed

+287
-1
lines changed

2 files changed

+287
-1
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- uses: actions/checkout@v4
2020
- uses: erlef/setup-beam@v1
2121
with:
22-
otp-version: "27.0.0"
22+
otp-version: "28.0.0"
2323
gleam-version: "1.11.0"
2424
rebar3-version: "3"
2525
# elixir-version: "1"

src/rabbit_cert_info.erl

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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~cT~c~c:~c~c:~c~cZ",
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.html
234+
%%
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

Comments
 (0)