From ee2178b073e936760b405b338e473236a5df94ca Mon Sep 17 00:00:00 2001 From: Magnus Henoch Date: Tue, 8 Dec 2015 18:16:36 +0000 Subject: Function for generating OpenSSL-style name hashes OpenSSL has functions to generate short (eight hex digits) hashes of issuers of certificates and CRLs. These hashes are used by the "c_rehash" script to populate directories of CA certificates and CRLs, e.g. in the Apache web server. Adding this function lets an Erlang program find the right CRL for a given certificate in such a directory. --- lib/public_key/doc/src/public_key.xml | 21 +++++++- lib/public_key/src/public_key.erl | 85 +++++++++++++++++++++++++++++++- lib/public_key/test/public_key_SUITE.erl | 39 ++++++++++++++- 3 files changed, 142 insertions(+), 3 deletions(-) diff --git a/lib/public_key/doc/src/public_key.xml b/lib/public_key/doc/src/public_key.xml index becb5338e0..96901ed516 100644 --- a/lib/public_key/doc/src/public_key.xml +++ b/lib/public_key/doc/src/public_key.xml @@ -863,7 +863,26 @@ fun(#'DistributionPoint'{}, #'CertificateList'{},

Verifies a digital signature.

- + + + short_name_hash(Name) -> string() + + Name = issuer_name() + + +

Generates a short hash of an issuer name. The hash is + returned as a string containing eight hexadecimal digits.

+ +

The return value of this function is the same as the result + of the commands openssl crl -hash and + openssl x509 -issuer_hash, when passed the issuer name of + a CRL or a certificate, respectively. This hash is used by the + c_rehash tool to maintain a directory of symlinks to CRL + files, in order to facilitate looking up a CRL by its issuer + name.

+
+
+ diff --git a/lib/public_key/src/public_key.erl b/lib/public_key/src/public_key.erl index 27bf2093a1..d23abfe256 100644 --- a/lib/public_key/src/public_key.erl +++ b/lib/public_key/src/public_key.erl @@ -55,7 +55,8 @@ pkix_dist_points/1, pkix_match_dist_point/2, pkix_crl_verify/2, - pkix_crl_issuer/1 + pkix_crl_issuer/1, + short_name_hash/1 ]). -export_type([public_key/0, private_key/0, pem_entry/0, @@ -817,6 +818,17 @@ oid2ssh_curvename(?'secp256r1') -> <<"nistp256">>; oid2ssh_curvename(?'secp384r1') -> <<"nistp384">>; oid2ssh_curvename(?'secp521r1') -> <<"nistp521">>. +%%-------------------------------------------------------------------- +-spec short_name_hash({rdnSequence, [#'AttributeTypeAndValue'{}]}) -> + string(). + +%% Description: Generates OpenSSL-style hash of a name. +%%-------------------------------------------------------------------- +short_name_hash({rdnSequence, _Attributes} = Name) -> + HashThis = encode_name_for_short_hash(Name), + <> = crypto:hash(sha, HashThis), + string:to_lower(string:right(integer_to_list(HashValue, 16), 8, $0)). + %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- @@ -1080,3 +1092,74 @@ ec_key({PubKey, PrivateKey}, Params) -> parameters = Params, publicKey = PubKey}. +encode_name_for_short_hash({rdnSequence, Attributes0}) -> + Attributes = lists:map(fun normalise_attribute/1, Attributes0), + {Encoded, _} = 'OTP-PUB-KEY':'enc_RDNSequence'(Attributes, []), + Encoded. + +%% Normalise attribute for "short hash". If the attribute value +%% hasn't been decoded yet, decode it so we can normalise it. +normalise_attribute([#'AttributeTypeAndValue'{ + type = _Type, + value = Binary} = ATV]) when is_binary(Binary) -> + case pubkey_cert_records:transform(ATV, decode) of + #'AttributeTypeAndValue'{value = Binary} -> + %% Cannot decode attribute; return original. + [ATV]; + DecodedATV = #'AttributeTypeAndValue'{} -> + %% The new value will either be String or {Encoding,String}. + normalise_attribute([DecodedATV]) + end; +normalise_attribute([#'AttributeTypeAndValue'{ + type = _Type, + value = {Encoding, String}} = ATV]) + when + Encoding =:= utf8String; + Encoding =:= printableString; + Encoding =:= teletexString; + Encoding =:= ia5String -> + %% These string types all give us something that the unicode + %% module understands. + NewValue = normalise_attribute_value(String), + [ATV#'AttributeTypeAndValue'{value = NewValue}]; +normalise_attribute([#'AttributeTypeAndValue'{ + type = _Type, + value = String} = ATV]) when is_list(String) -> + %% A string returned by pubkey_cert_records:transform/2, for + %% certain attributes that commonly have incorrect value types. + NewValue = normalise_attribute_value(String), + [ATV#'AttributeTypeAndValue'{value = NewValue}]. + +normalise_attribute_value(String) -> + Converted = unicode:characters_to_binary(String), + NormalisedString = normalise_string(Converted), + %% We can't use the encoding function for the actual type of the + %% attribute, since some of them don't allow utf8Strings, which is + %% the required encoding when creating the hash. + {NewBinary, _} = 'OTP-PUB-KEY':'enc_X520CommonName'({utf8String, NormalisedString}, []), + NewBinary. + +normalise_string(String) -> + %% Normalise attribute values as required for "short hashes", as + %% implemented by OpenSSL. + + %% Remove ASCII whitespace from beginning and end. + TrimmedLeft = re:replace(String, "^[\s\f\n\r\t\v]+", "", [unicode, global]), + TrimmedRight = re:replace(TrimmedLeft, "[\s\f\n\r\t\v]+$", "", [unicode, global]), + %% Convert multiple whitespace characters to a single space. + Collapsed = re:replace(TrimmedRight, "[\s\f\n\r\t\v]+", "\s", [unicode, global]), + %% Convert ASCII characters to lowercase + Lower = ascii_to_lower(Collapsed), + %% And we're done! + Lower. + +ascii_to_lower(String) -> + %% Can't use string:to_lower/1, because that changes Latin-1 + %% characters as well. + << <<(if $A =< C, C =< $Z -> + C + ($a - $A); + true -> + C + end)>> + || + <> <= iolist_to_binary(String) >>. diff --git a/lib/public_key/test/public_key_SUITE.erl b/lib/public_key/test/public_key_SUITE.erl index 2fbccbfaa7..2462c17f80 100644 --- a/lib/public_key/test/public_key_SUITE.erl +++ b/lib/public_key/test/public_key_SUITE.erl @@ -43,7 +43,8 @@ all() -> encrypt_decrypt, {group, sign_verify}, pkix, pkix_countryname, pkix_emailaddress, pkix_path_validation, - pkix_iso_rsa_oid, pkix_iso_dsa_oid, pkix_crl]. + pkix_iso_rsa_oid, pkix_iso_dsa_oid, pkix_crl, + short_cert_issuer_hash, short_crl_issuer_hash]. groups() -> [{pem_decode_encode, [], [dsa_pem, rsa_pem, ec_pem, encrypted_pem, @@ -804,6 +805,42 @@ pkix_crl(Config) when is_list(Config) -> reasons = asn1_NOVALUE, distributionPoint = Point} = public_key:pkix_dist_point(OTPIDPCert). +%%-------------------------------------------------------------------- +short_cert_issuer_hash() -> + [{doc, "Test OpenSSL-style hash for certificate issuer"}]. + +short_cert_issuer_hash(Config) when is_list(Config) -> + Datadir = ?config(data_dir, Config), + [{'Certificate', CertDER, _}] = + erl_make_certs:pem_to_der(filename:join(Datadir, "client_cert.pem")), + + %% This hash value was obtained by running: + %% openssl x509 -in client_cert.pem -issuer_hash -noout + CertIssuerHash = "d4c8d7e5", + + #'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{issuer = Issuer}} = + public_key:pkix_decode_cert(CertDER, otp), + + CertIssuerHash = public_key:short_name_hash(Issuer). + +%%-------------------------------------------------------------------- +short_crl_issuer_hash() -> + [{doc, "Test OpenSSL-style hash for CRL issuer"}]. + +short_crl_issuer_hash(Config) when is_list(Config) -> + Datadir = ?config(data_dir, Config), + [{'CertificateList', CrlDER, _}] = + erl_make_certs:pem_to_der(filename:join(Datadir, "idp_crl.pem")), + + %% This hash value was obtained by running: + %% openssl crl -in idp_crl.pem -hash -noout + CrlIssuerHash = "d6134ed3", + + Issuer = public_key:pkix_crl_issuer(CrlDER), + + CrlIssuerHash = public_key:short_name_hash(Issuer). + + %%-------------------------------------------------------------------- %% Internal functions ------------------------------------------------ %%-------------------------------------------------------------------- -- cgit v1.2.3