Skip to main content

src/vpn_identity.erl

%%%-------------------------------------------------------------------
%% @doc Certificate identity metadata loader.
%%%-------------------------------------------------------------------
-module(vpn_identity).

-include_lib("public_key/include/OTP-PUB-KEY.hrl").

-export([load/1, safe_info/1, verify_key_match/1]).

load(Config) when is_map(Config) ->
    case required_identity_key(Config) of
        none ->
            load_files(Config);
        {missing, Key} ->
            {error, {missing_identity_key, Key}}
    end;
load(_Config) ->
    {error, invalid_config}.

safe_info(Identity) ->
    maps:with([peer_id,
               certificate_path,
               private_key_path,
               certificate,
               trusted,
               key_match],
              Identity).

load_files(Config) ->
    CertPath = maps:get(certificate_path, Config),
    KeyPath = maps:get(private_key_path, Config),
    CaPath = maps:get(ca_certificate_path, Config),
    case file:read_file(CertPath) of
        {ok, CertPem} ->
            load_key(Config, CertPath, KeyPath, CaPath, CertPem);
        {error, Reason} ->
            {error, {certificate_read_failed, CertPath, Reason}}
    end.

load_key(Config, CertPath, KeyPath, CaPath, CertPem) ->
    case parse_certificate(CertPem) of
        {ok, Certificate, CertificateMetadata} ->
            verify_certificate(Config,
                               CertPath,
                               KeyPath,
                               CaPath,
                               CertPem,
                               Certificate,
                               CertificateMetadata);
        {error, Reason} ->
            {error, {certificate_parse_failed, CertPath, Reason}}
    end.

verify_certificate(Config,
                   CertPath,
                   KeyPath,
                   CaPath,
                   CertPem,
                   Certificate,
                   CertificateMetadata) ->
    case vpn_trust_store:load(CaPath) of
        {ok, TrustStore} ->
            case vpn_trust_store:verify(TrustStore, Certificate) of
                ok ->
                    load_key(Config,
                             CertPath,
                             KeyPath,
                             CaPath,
                             CertPem,
                             Certificate,
                             CertificateMetadata#{trusted => true});
                {error, Reason} ->
                    {error, {certificate_verification_failed, CertPath, Reason}}
            end;
        {error, Reason} ->
            {error, {ca_certificate_load_failed, CaPath, Reason}}
    end.

load_key(Config,
         CertPath,
         KeyPath,
         CaPath,
         CertPem,
         Certificate,
         CertificateMetadata) ->
    case file:read_file(KeyPath) of
        {ok, KeyPem} ->
            load_private_key(Config,
                             CertPath,
                             KeyPath,
                             CaPath,
                             CertPem,
                             Certificate,
                             CertificateMetadata,
                             KeyPem);
        {error, Reason} ->
            {error, {private_key_read_failed, KeyPath, Reason}}
    end.

load_private_key(Config,
                 CertPath,
                 KeyPath,
                 CaPath,
                 CertPem,
                 Certificate,
                 CertificateMetadata,
                 KeyPem) ->
    case parse_private_key_public_part(KeyPem) of
        {ok, PrivateKeyPublicPart} ->
            Identity = #{peer_id => maps:get(id, Config),
                         certificate_path => CertPath,
                         private_key_path => KeyPath,
                         ca_certificate_path => CaPath,
                         certificate_pem => CertPem,
                         private_key_pem => KeyPem,
                         x509_certificate => Certificate,
                         private_key_public_part => PrivateKeyPublicPart,
                         certificate => CertificateMetadata},
            case verify_key_match(Identity) of
                ok ->
                    {ok, Identity#{trusted => maps:get(trusted, CertificateMetadata, false),
                                   key_match => true}};
                {error, Reason} ->
                    {error, Reason}
            end;
        {error, Reason} ->
            {error, {private_key_parse_failed, KeyPath, Reason}}
    end.

verify_key_match(#{x509_certificate := Certificate,
                   private_key_public_part := PrivateKeyPublicPart}) ->
    case certificate_public_key(Certificate) of
        {ok, PrivateKeyPublicPart} ->
            ok;
        {ok, _CertificatePublicKey} ->
            {error, key_mismatch};
        {error, Reason} ->
            {error, Reason}
    end.

parse_certificate(CertPem) ->
    try
        case public_key:pem_decode(CertPem) of
            [{_, Der, _} | _] ->
                Certificate = public_key:pkix_decode_cert(Der, otp),
                case certificate_metadata(Certificate) of
                    {ok, Metadata} ->
                        {ok, Certificate, Metadata};
                    {error, MetadataReason} ->
                        {error, MetadataReason}
                end;
            [] ->
                {error, no_pem_entry}
        end
    catch
        Class:ParseReason ->
            {error, {Class, ParseReason}}
    end.

certificate_metadata(#'OTPCertificate'{tbsCertificate = Tbs}) ->
    #'OTPTBSCertificate'{issuer = Issuer,
                         serialNumber = SerialNumber,
                         subject = Subject,
                         validity = #'Validity'{notBefore = NotBefore,
                                                notAfter = NotAfter}} = Tbs,
    {ok, #{subject => Subject,
           issuer => Issuer,
           serial_number => SerialNumber,
           not_before => NotBefore,
           not_after => NotAfter}};
certificate_metadata(_Certificate) ->
    {error, invalid_certificate}.

parse_private_key_public_part(KeyPem) ->
    try
        case public_key:pem_decode(KeyPem) of
            [Entry | _] ->
                private_key_public_part(public_key:pem_entry_decode(Entry));
            [] ->
                {error, no_pem_entry}
        end
    catch
        Class:ParseReason ->
            {error, {Class, ParseReason}}
    end.

certificate_public_key(#'OTPCertificate'{tbsCertificate = #'OTPTBSCertificate'{
                                           subjectPublicKeyInfo = #'OTPSubjectPublicKeyInfo'{
                                             subjectPublicKey = PublicKey}}}) ->
    {ok, PublicKey};
certificate_public_key(_Certificate) ->
    {error, unsupported_certificate_key}.

private_key_public_part(#'RSAPrivateKey'{modulus = Modulus,
                                         publicExponent = PublicExponent}) ->
    {ok, #'RSAPublicKey'{modulus = Modulus,
                         publicExponent = PublicExponent}};
private_key_public_part(_PrivateKey) ->
    {error, unsupported_private_key}.

required_identity_key(Config) ->
    required_identity_key(Config, [certificate_path, private_key_path, ca_certificate_path]).

required_identity_key(_Config, []) ->
    none;
required_identity_key(Config, [Key | Rest]) ->
    case maps:is_key(Key, Config) of
        true ->
            required_identity_key(Config, Rest);
        false ->
            {missing, Key}
    end.