%%%-------------------------------------------------------------------
%% @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.