├── README.md ├── .formatter.exs ├── .gitignore ├── lib ├── hash.ex └── ed25519.ex ├── test ├── test_helper.exs └── ed25519_test.exs ├── LICENSE ├── mix.exs └── mix.lock /README.md: -------------------------------------------------------------------------------- 1 | # Ed25519 2 | 3 | Digital signatures in Elixir 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | erl_crash.dump 6 | *.ez 7 | *.beam 8 | *.swp 9 | -------------------------------------------------------------------------------- /lib/hash.ex: -------------------------------------------------------------------------------- 1 | defmodule Ed25519.Hash do 2 | @moduledoc false 3 | defmacro __using__(_) do 4 | {mod, fun, pre_args, post_args} = 5 | Application.get_env(:ed25519, :hash_fn, {:crypto, :hash, [:sha512], []}) 6 | 7 | quote do 8 | defp hash(unquote(Macro.var(:m, __MODULE__))) do 9 | unquote(mod).unquote(fun)( 10 | unquote_splicing(pre_args), 11 | unquote(Macro.var(:m, __MODULE__)), 12 | unquote_splicing(post_args) 13 | ) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule VectorHelper do 4 | def from_hex(<<>>), do: "" 5 | 6 | def from_hex(s) do 7 | size = div(byte_size(s), 2) 8 | {n, ""} = s |> Integer.parse(16) 9 | zero_pad(:binary.encode_unsigned(n), size) 10 | end 11 | 12 | def zero_pad(s, size) when byte_size(s) == size, do: s 13 | def zero_pad(s, size) when byte_size(s) < size, do: zero_pad(<<0>> <> s, size) 14 | end 15 | 16 | defmodule CryptoVectors do 17 | def testcases do 18 | # Reworked from http://ed25519.cr.yp.to/python/sign.input 19 | # sk+pk"," pk"," m"," sig+m 20 | "test/sign.input.txt" 21 | |> File.stream!() 22 | |> Stream.map(fn s -> String.split(s, ":") |> Enum.take(4) |> List.to_tuple() end) 23 | |> Enum.to_list() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matt Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ed25519.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ed25519, 7 | version: "1.4.4", 8 | elixir: "~> 1.7", 9 | name: "Ed25519", 10 | source_url: "https://github.com/mwmiller/ed25519_ex", 11 | build_embedded: Mix.env() == :prod, 12 | start_permanent: Mix.env() == :prod, 13 | description: description(), 14 | package: package(), 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [extra_applications: [:crypto]] 21 | end 22 | 23 | defp deps do 24 | [ 25 | {:ex_doc, "~> 0.23", only: :dev}, 26 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 27 | {:dialyxir, "~> 1.4", only: [:dev], runtime: false} 28 | ] 29 | end 30 | 31 | defp description do 32 | """ 33 | Ed25519 signature functions 34 | """ 35 | end 36 | 37 | defp package do 38 | [ 39 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 40 | maintainers: ["Matt Miller"], 41 | licenses: ["MIT"], 42 | links: %{ 43 | "GitHub" => "https://github.com/mwmiller/ed25519_ex", 44 | "Info" => "http://ed25519.cr.yp.to" 45 | } 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/ed25519_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ed25519Test do 2 | use ExUnit.Case 3 | import VectorHelper 4 | doctest Ed25519 5 | 6 | test "keys" do 7 | {sk, pk} = Ed25519.generate_key_pair() 8 | 9 | assert byte_size(sk) == 32, "Proper sized secret key" 10 | assert byte_size(pk) == 32, "Proper sized public key" 11 | 12 | # test key generation with provided secret using the random sk 13 | {sk, pk2} = Ed25519.generate_key_pair(sk) 14 | assert pk2 == pk 15 | 16 | assert Ed25519.derive_public_key(sk) == pk, "Can re-derive the public key from the secret key" 17 | end 18 | 19 | test "to_curve25519" do 20 | sk = 21 | <<244, 62, 48, 200, 177, 103, 228, 134, 216, 53, 71, 1, 105, 127, 46, 210, 56, 38, 17, 114, 22 | 171, 83, 82, 29, 106, 115, 58, 178, 237, 213, 10, 226>> 23 | 24 | curve_sk = 25 | <<208, 75, 48, 29, 66, 212, 83, 245, 40, 51, 19, 213, 150, 216, 65, 96, 165, 206, 255, 140, 26 | 179, 10, 215, 92, 134, 155, 30, 80, 229, 104, 104, 76>> 27 | 28 | pk = 29 | <<70, 55, 170, 144, 189, 49, 220, 167, 226, 113, 150, 15, 53, 138, 156, 39, 230, 211, 77, 30 | 195, 100, 174, 112, 112, 204, 9, 154, 19, 165, 70, 133, 80>> 31 | 32 | curve_pk = 33 | <<70, 145, 87, 124, 161, 125, 23, 116, 180, 121, 44, 30, 41, 206, 43, 88, 241, 75, 104, 65, 34 | 12, 215, 105, 123, 62, 226, 228, 124, 106, 111, 39, 48>> 35 | 36 | assert Ed25519.to_curve25519(sk, :secret) == curve_sk 37 | 38 | assert Ed25519.to_curve25519(pk, :public) == curve_pk 39 | 40 | assert_raise RuntimeError, "Point off Edwards curve", fn -> 41 | Ed25519.to_curve25519(curve_pk, :public) 42 | end 43 | 44 | assert_raise RuntimeError, "Provided value not a key", fn -> 45 | Ed25519.to_curve25519(<<>>, :public) 46 | end 47 | end 48 | 49 | @tag timeout: :infinity 50 | test "cr.yp.to examples" do 51 | test_em = fn 52 | [], _fun -> 53 | :noop 54 | 55 | [e | xamples], fun -> 56 | {<>, dp, m, <>} = 57 | e 58 | 59 | assert p == dp, "Duplicate public key: " <> dp 60 | assert m == dm, "Duplicate message: " <> dm 61 | 62 | sk = from_hex(s) 63 | pk = from_hex(p) 64 | ms = from_hex(m) 65 | si = from_hex(sig) 66 | 67 | assert Ed25519.derive_public_key(sk) == pk, "SK: " <> s 68 | assert Ed25519.signature(ms, sk, pk) == si, "SIG: " <> sig 69 | assert Ed25519.valid_signature?(si, ms, pk), "MSG:" <> m 70 | fun.(xamples, fun) 71 | end 72 | 73 | test_em.(CryptoVectors.testcases(), test_em) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, 4 | "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 6 | "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, 7 | "ex_doc": {:hex, :ex_doc, "0.39.2", "da5549bbce34c5fb0811f829f9f6b7a13d5607b222631d9e989447096f295c57", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "62665526a88c207653dbcee2aac66c2c229d7c18a70ca4ffc7f74f9e01324daa"}, 8 | "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, 9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 14 | } 15 | -------------------------------------------------------------------------------- /lib/ed25519.ex: -------------------------------------------------------------------------------- 1 | defmodule Ed25519 do 2 | import Bitwise 3 | 4 | @moduledoc """ 5 | Ed25519 signature functions 6 | 7 | This is mostly suitable as part of a pure Elixir solution. 8 | 9 | ## Configuration 10 | 11 | *No configuration is needed* in most cases. However, if needed, a custom hash 12 | function can be configured. As per the specification - `sha512` is the default. 13 | 14 | `config/config.exs` 15 | 16 | import Config 17 | 18 | # The hash function will be invoked as 'Blake2.hash2b(payload, 16)' 19 | config :ed25519, 20 | hash_fn: {Blake2, :hash2b, [], [16]} 21 | 22 | # The hash function will be invoked as ':crypto.hash(:sha256, payload)' 23 | config :ed25519, 24 | hash_fn: {:crypto, :hash, [:sha256], []} 25 | 26 | """ 27 | @typedoc """ 28 | public or secret key 29 | """ 30 | @type key :: binary 31 | 32 | @typedoc """ 33 | computed signature 34 | """ 35 | @type signature :: binary 36 | 37 | @p 57_896_044_618_658_097_711_785_492_504_343_953_926_634_992_332_820_282_019_728_792_003_956_564_819_949 38 | @l 7_237_005_577_332_262_213_973_186_563_042_994_240_857_116_359_379_907_606_001_950_938_285_454_250_989 39 | @d -4_513_249_062_541_557_337_682_894_930_092_624_173_785_641_285_191_125_241_628_941_591_882_900_924_598_840_740 40 | @i 19_681_161_376_707_505_956_807_079_304_988_542_015_446_066_515_923_890_162_744_021_073_123_829_784_752 41 | @t254 28_948_022_309_329_048_855_892_746_252_171_976_963_317_496_166_410_141_009_864_396_001_978_282_409_984 42 | @base {15_112_221_349_535_400_772_501_151_409_588_531_511_454_012_693_041_857_206_046_113_283_949_847_762_202, 43 | 46_316_835_694_926_478_169_428_394_003_475_163_141_307_993_866_256_225_615_783_033_603_165_251_855_960} 44 | 45 | defp xrecover(y) do 46 | xx = (y * y - 1) * inv(@d * y * y + 1) 47 | x = expmod(xx, div(@p + 3, 8), @p) 48 | 49 | x = 50 | case (x * x - xx) |> mod(@p) do 51 | 0 -> x 52 | _ -> mod(x * @i, @p) 53 | end 54 | 55 | case x |> mod(2) do 56 | 0 -> @p - x 57 | _ -> x 58 | end 59 | end 60 | 61 | defp mod(x, _y) when x == 0, do: 0 62 | defp mod(x, y) when x > 0, do: rem(x, y) 63 | defp mod(x, y) when x < 0, do: rem(y + rem(x, y), y) 64 | 65 | # __using__ Macro generates the hash function at compile time, which allows the 66 | # hashing function to be configurable without runtime overhead 67 | use Ed25519.Hash 68 | defp hashint(m), do: m |> hash |> :binary.decode_unsigned(:little) 69 | 70 | defp expmod(b, e, m) when b > 0 do 71 | b |> :crypto.mod_pow(e, m) |> :binary.decode_unsigned() 72 | end 73 | 74 | defp expmod(b, e, m) do 75 | i = b |> abs() |> :crypto.mod_pow(e, m) |> :binary.decode_unsigned() 76 | 77 | cond do 78 | mod(e, 2) == 0 -> i 79 | i == 0 -> i 80 | true -> m - i 81 | end 82 | end 83 | 84 | defp inv(x), do: x |> expmod(@p - 2, @p) 85 | 86 | defp edwards({x1, y1}, {x2, y2}) do 87 | x = (x1 * y2 + x2 * y1) * inv(1 + @d * x1 * x2 * y1 * y2) 88 | y = (y1 * y2 + x1 * x2) * inv(1 - @d * x1 * x2 * y1 * y2) 89 | {mod(x, @p), mod(y, @p)} 90 | end 91 | 92 | defp encodepoint({x, y}) do 93 | val = 94 | y 95 | |> band(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) 96 | |> bor((x &&& 1) <<< 255) 97 | 98 | <> 99 | end 100 | 101 | defp decodepoint(<>) do 102 | xc = n |> bsr(255) 103 | y = n |> band(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) 104 | x = xrecover(y) 105 | 106 | point = 107 | case x &&& 1 do 108 | ^xc -> {x, y} 109 | _ -> {@p - x, y} 110 | end 111 | 112 | if isoncurve(point), do: point, else: raise("Point off Edwards curve") 113 | end 114 | 115 | defp decodepoint(_), do: raise("Provided value not a key") 116 | 117 | defp isoncurve({x, y}), do: (-x * x + y * y - 1 - @d * x * x * y * y) |> mod(@p) == 0 118 | 119 | @doc """ 120 | Returns whether a given `key` lies on the ed25519 curve. 121 | """ 122 | @spec on_curve?(key) :: boolean 123 | def on_curve?(key) do 124 | try do 125 | decodepoint(key) 126 | true 127 | rescue 128 | _error -> false 129 | end 130 | end 131 | 132 | @doc """ 133 | Sign a message 134 | 135 | If only the secret key is provided, the public key will be derived therefrom. 136 | This adds significant overhead. 137 | """ 138 | @spec signature(binary, key, key | nil) :: signature 139 | def signature(m, sk, pk \\ nil) 140 | def signature(m, sk, nil), do: signature(m, sk, derive_public_key(sk)) 141 | 142 | def signature(m, sk, pk) do 143 | h = hash(sk) 144 | a = a_from_hash(h) 145 | r = hashint(:binary.part(h, 32, 32) <> m) 146 | bigr = r |> scalarmult(@base) |> encodepoint 147 | s = mod(r + hashint(bigr <> pk <> m) * a, @l) 148 | bigr <> <> 149 | end 150 | 151 | defp a_from_hash(<>) do 152 | @t254 + 153 | (h 154 | |> band(0xF3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8)) 155 | end 156 | 157 | defp scalarmult(0, _pair), do: {0, 1} 158 | 159 | defp scalarmult(e, p) do 160 | q = e |> div(2) |> scalarmult(p) 161 | q = edwards(q, q) 162 | 163 | case e &&& 1 do 164 | 1 -> edwards(q, p) 165 | _ -> q 166 | end 167 | end 168 | 169 | defp clamp(c) do 170 | c 171 | |> band(bnot(7)) 172 | |> band(bnot(128 <<< (8 * 31))) 173 | |> bor(64 <<< (8 * 31)) 174 | end 175 | 176 | @doc """ 177 | validate a signed message 178 | """ 179 | @spec valid_signature?(signature, binary, key) :: boolean 180 | def valid_signature?(<>, m, pk) 181 | when byte_size(pk) == 32 do 182 | r = decodepoint(for_r) 183 | a = decodepoint(pk) 184 | h = hashint(encodepoint(r) <> pk <> m) 185 | scalarmult(s, @base) == edwards(r, scalarmult(h, a)) 186 | end 187 | 188 | def valid_signature?(_s, _m_, _pk), do: false 189 | 190 | @doc """ 191 | Generate a secret/public key pair 192 | 193 | Returned tuple contains `{random_secret_key, derived_public_key}` 194 | """ 195 | @spec generate_key_pair :: {key, key} 196 | def generate_key_pair do 197 | secret = :crypto.strong_rand_bytes(32) 198 | {secret, derive_public_key(secret)} 199 | end 200 | 201 | @doc """ 202 | Generate a secret/public key pair from supplied secret 203 | 204 | Returned tuple contains `{secret_key, derived_public_key}` 205 | """ 206 | @spec generate_key_pair(key) :: {key, key} 207 | def generate_key_pair(secret) do 208 | {secret, derive_public_key(secret)} 209 | end 210 | 211 | @doc """ 212 | derive the public signing key from the secret key 213 | """ 214 | @spec derive_public_key(key) :: key 215 | def derive_public_key(sk) do 216 | sk 217 | |> hash 218 | |> a_from_hash 219 | |> scalarmult(@base) 220 | |> encodepoint 221 | end 222 | 223 | @doc """ 224 | Derive the x25519/curve25519 encryption key from the ed25519 signing key 225 | 226 | 227 | By converting an `EdwardsPoint` on the Edwards model to the corresponding `MontgomeryPoint` on the Montgomery model 228 | 229 | Handles either `:secret` or `:public` keys as indicated in the call 230 | 231 | May `raise` on an invalid input key or unknown atom 232 | 233 | See: https://blog.filippo.io/using-ed25519-keys-for-encryption 234 | """ 235 | @spec to_curve25519(key, atom) :: key 236 | def to_curve25519(key, which) 237 | 238 | def to_curve25519(ed_public_key, :public) do 239 | {_, y} = decodepoint(ed_public_key) 240 | u = mod((1 + y) * inv(1 - y), @p) 241 | <> 242 | end 243 | 244 | def to_curve25519(ed_secret_key, :secret) do 245 | <> = :crypto.hash(:sha512, ed_secret_key) 246 | <> 247 | end 248 | end 249 | --------------------------------------------------------------------------------