| 1 | 
 | 
%%% eturnal STUN/TURN server. | 
| 2 | 
 | 
%%% | 
| 3 | 
 | 
%%% Copyright (c) 2021 Holger Weiss <holger@zedat.fu-berlin.de>. | 
| 4 | 
 | 
%%% Copyright (c) 2021 ProcessOne, SARL. | 
| 5 | 
 | 
%%% All rights reserved. | 
| 6 | 
 | 
%%% | 
| 7 | 
 | 
%%% Licensed under the Apache License, Version 2.0 (the "License"); | 
| 8 | 
 | 
%%% you may not use this file except in compliance with the License. | 
| 9 | 
 | 
%%% You may obtain a copy of the License at | 
| 10 | 
 | 
%%% | 
| 11 | 
 | 
%%%     http://www.apache.org/licenses/LICENSE-2.0 | 
| 12 | 
 | 
%%% | 
| 13 | 
 | 
%%% Unless required by applicable law or agreed to in writing, software | 
| 14 | 
 | 
%%% distributed under the License is distributed on an "AS IS" BASIS, | 
| 15 | 
 | 
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
| 16 | 
 | 
%%% See the License for the specific language governing permissions and | 
| 17 | 
 | 
%%% limitations under the License. | 
| 18 | 
 | 
 | 
| 19 | 
 | 
-module(eturnal_cert). | 
| 20 | 
 | 
-export([create/1]). | 
| 21 | 
 | 
 | 
| 22 | 
 | 
-include_lib("public_key/include/public_key.hrl"). | 
| 23 | 
 | 
-define(ETURNAL_KEY_SIZE, 4096). | 
| 24 | 
 | 
 | 
| 25 | 
 | 
% Currently not exported by calendar: | 
| 26 | 
 | 
-type year() :: non_neg_integer(). | 
| 27 | 
 | 
-type month() :: 1..12. | 
| 28 | 
 | 
-type day() :: 1..31. | 
| 29 | 
 | 
 | 
| 30 | 
 | 
%% API. | 
| 31 | 
 | 
 | 
| 32 | 
 | 
-spec create(string() | binary()) -> binary(). | 
| 33 | 
 | 
create(Domain) when is_binary(Domain) -> | 
| 34 | 
2 | 
    create(binary_to_list(Domain)); | 
| 35 | 
 | 
create(Domain) -> | 
| 36 | 
2 | 
    Key = private_key(), | 
| 37 | 
2 | 
    Crt = certificate(Domain, Key), | 
| 38 | 
2 | 
    public_key:pem_encode([pem_entry(Key), pem_entry(Crt)]). | 
| 39 | 
 | 
 | 
| 40 | 
 | 
%% Internal functions. | 
| 41 | 
 | 
 | 
| 42 | 
 | 
-spec private_key() -> #'RSAPrivateKey'{}. | 
| 43 | 
 | 
private_key() -> | 
| 44 | 
2 | 
    public_key:generate_key({rsa, ?ETURNAL_KEY_SIZE, 65537}). | 
| 45 | 
 | 
 | 
| 46 | 
 | 
-spec certificate(string(), #'RSAPrivateKey'{}) -> public_key:der_encoded(). | 
| 47 | 
 | 
certificate(Domain, Key) -> | 
| 48 | 
2 | 
    TBS = #'OTPTBSCertificate'{ | 
| 49 | 
 | 
             serialNumber = serial_number(), | 
| 50 | 
 | 
             signature = signature(), | 
| 51 | 
 | 
             issuer = issuer(Domain), | 
| 52 | 
 | 
             validity = validity(), | 
| 53 | 
 | 
             subject = subject(Domain), | 
| 54 | 
 | 
             subjectPublicKeyInfo = subject_key_info(Key), | 
| 55 | 
 | 
             extensions = extensions(Domain)}, | 
| 56 | 
2 | 
    public_key:pkix_sign(TBS, Key). | 
| 57 | 
 | 
 | 
| 58 | 
 | 
-spec serial_number() -> pos_integer(). | 
| 59 | 
 | 
serial_number() -> | 
| 60 | 
2 | 
    rand:uniform(1000000000). | 
| 61 | 
 | 
 | 
| 62 | 
 | 
-spec signature() -> #'SignatureAlgorithm'{}. | 
| 63 | 
 | 
signature() -> | 
| 64 | 
2 | 
    #'SignatureAlgorithm'{ | 
| 65 | 
 | 
       algorithm = ?'sha256WithRSAEncryption'}. | 
| 66 | 
 | 
 | 
| 67 | 
 | 
-spec issuer(string()) -> {rdnSequence, [[#'AttributeTypeAndValue'{}]]}. | 
| 68 | 
 | 
issuer(Domain) -> % Self-signed. | 
| 69 | 
2 | 
    subject(Domain). | 
| 70 | 
 | 
 | 
| 71 | 
 | 
-spec validity() -> #'Validity'{}. | 
| 72 | 
 | 
validity() -> | 
| 73 | 
2 | 
    #'Validity'{ | 
| 74 | 
 | 
       notBefore = format_date(calendar:universal_time()), | 
| 75 | 
 | 
       notAfter = format_date(2038, 1, 1)}. | 
| 76 | 
 | 
 | 
| 77 | 
 | 
-spec subject(string()) -> {rdnSequence, [[#'AttributeTypeAndValue'{}]]}. | 
| 78 | 
 | 
subject(Domain) -> | 
| 79 | 
4 | 
    {rdnSequence, | 
| 80 | 
 | 
     [[#'AttributeTypeAndValue'{ | 
| 81 | 
 | 
          type = ?'id-at-commonName', | 
| 82 | 
 | 
          value = {printableString, Domain}}]]}. | 
| 83 | 
 | 
 | 
| 84 | 
 | 
-spec subject_key_info(#'RSAPrivateKey'{}) -> #'OTPSubjectPublicKeyInfo'{}. | 
| 85 | 
 | 
subject_key_info(#'RSAPrivateKey'{modulus = Modulus, publicExponent = Exp}) -> | 
| 86 | 
2 | 
    #'OTPSubjectPublicKeyInfo'{ | 
| 87 | 
 | 
       algorithm = | 
| 88 | 
 | 
           #'PublicKeyAlgorithm'{ | 
| 89 | 
 | 
              algorithm = ?'rsaEncryption'}, | 
| 90 | 
 | 
       subjectPublicKey = | 
| 91 | 
 | 
           #'RSAPublicKey'{ | 
| 92 | 
 | 
              modulus = Modulus, | 
| 93 | 
 | 
              publicExponent = Exp}}. | 
| 94 | 
 | 
 | 
| 95 | 
 | 
-spec extensions(string()) -> [#'Extension'{}]. | 
| 96 | 
 | 
extensions(Domain) -> | 
| 97 | 
2 | 
    [#'Extension'{ | 
| 98 | 
 | 
        extnID = ?'id-ce-subjectAltName', | 
| 99 | 
 | 
        extnValue = [{dNSName, Domain}], | 
| 100 | 
 | 
        critical = false}, | 
| 101 | 
 | 
     #'Extension'{ | 
| 102 | 
 | 
        extnID = ?'id-ce-basicConstraints', | 
| 103 | 
 | 
        extnValue = #'BasicConstraints'{cA = true}, | 
| 104 | 
 | 
        critical = false}]. | 
| 105 | 
 | 
 | 
| 106 | 
 | 
-spec pem_entry(#'RSAPrivateKey'{} | public_key:der_encoded()) | 
| 107 | 
 | 
      -> public_key:pem_entry(). | 
| 108 | 
 | 
pem_entry(#'RSAPrivateKey'{} = Key) -> | 
| 109 | 
2 | 
    public_key:pem_entry_encode('RSAPrivateKey', Key); | 
| 110 | 
 | 
pem_entry(Crt) when is_binary(Crt) -> | 
| 111 | 
2 | 
    {'Certificate', Crt, not_encrypted}. | 
| 112 | 
 | 
 | 
| 113 | 
 | 
-spec format_date(year(), month(), day()) -> {utcTime, string()}. | 
| 114 | 
 | 
format_date(Y, M, D) when Y >= 2000 -> | 
| 115 | 
4 | 
    {utcTime, | 
| 116 | 
 | 
     lists:flatten( | 
| 117 | 
 | 
       io_lib:format("~2.10.0B~2.10.0B~2.10.0B~2.10.0B~2.10.0B~2.10.0BZ", | 
| 118 | 
 | 
                     [Y - 2000, M, D, 0, 0, 0]))}. | 
| 119 | 
 | 
 | 
| 120 | 
 | 
-spec format_date(calendar:datetime()) -> {utcTime, string()}. | 
| 121 | 
 | 
format_date({{Y, M, D}, {_, _, _}}) -> | 
| 122 | 
2 | 
    format_date(Y, M, D). |