├── test ├── test_helper.exs ├── signatureDer.txt ├── publicKey.pem ├── message.txt ├── privateKey.pem ├── signature_test.exs ├── privatekey_test.exs ├── ecdsa_test.exs ├── openssl_test.exs └── publickey_test.exs ├── .gitignore ├── lib ├── utils │ ├── base64.ex │ ├── binary.ex │ ├── integer.ex │ └── der.ex ├── point.ex ├── ellipticcurve.ex ├── curve │ ├── curve.ex │ └── knownCurves.ex ├── ecdsa.ex ├── signature.ex ├── publicKey.ex ├── math.ex └── privateKey.ex ├── .travis.yml ├── CHANGELOG.md ├── mix.exs ├── LICENSE └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/signatureDer.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starkbank/ecdsa-elixir/HEAD/test/signatureDer.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | *.beam 9 | /config/*.secret.exs 10 | .elixir_ls/ 11 | .formatter.exs 12 | -------------------------------------------------------------------------------- /test/publicKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE/nvHu/SQQaos9TUljQsUuKI15Zr5SabP 3 | rbwtbfT/408rkVVzq8vAisbBRmpeRREXj5aog/Mq8RrdYy75W9q/Ig== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /test/message.txt: -------------------------------------------------------------------------------- 1 | { 2 | "transaction": { 3 | "amount": 50000, 4 | "receiver": "", 5 | "description": "Sample transfer between workspaces", 6 | "externalId": "123456", 7 | "tags": ["john", "smith"] 8 | } 9 | } -------------------------------------------------------------------------------- /lib/utils/base64.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Utils.Base64 do 2 | @moduledoc false 3 | 4 | def decode(string) do 5 | Base.decode64!(string) 6 | end 7 | 8 | def encode(string) do 9 | Base.encode64(string) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: elixir 4 | 5 | matrix: 6 | include: 7 | - elixir: '1.9.0' 8 | otp_release: '20.0' 9 | - elixir: '1.10.0' 10 | otp_release: '21.0' 11 | 12 | script: 13 | - mix test --trace 14 | -------------------------------------------------------------------------------- /test/privateKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQACg== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHQCAQEEIODvZuS34wFbt0X53+P5EnSj6tMjfVK01dD1dgDH02RzoAcGBSuBBAAK 6 | oUQDQgAE/nvHu/SQQaos9TUljQsUuKI15Zr5SabPrbwtbfT/408rkVVzq8vAisbB 7 | RmpeRREXj5aog/Mq8RrdYy75W9q/Ig== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /lib/point.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Point do 2 | @doc """ 3 | Holds point data. Is usually handled internally by the library and serves only as detailed information to the end-user. 4 | 5 | Parameters: 6 | - `:x` [integer]: first point coordinate; 7 | - `:y` [integer]: first point coordinate; 8 | - `:z` [integer]: first point coordinate (used only in Jacobian coordinates); 9 | """ 10 | defstruct [:x, :y, z: 0] 11 | 12 | def isAtInfinity?(p) do 13 | p.y == 0 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/ellipticcurve.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve do 2 | @moduledoc """ 3 | Pure Elixir package used to generate and read private/public key pairs in PEM and DER formats, sign and verify messages using the Elliptic Curve Digital Signature Algorithmn (ECDSA). 4 | 5 | Submodules: 6 | - Ecdsa: verifies and signs messages; 7 | - Signature: loads and dumps signatures; 8 | - PrivateKey: creates, loads and dumps private keys; also creates public keys from private keys; 9 | - PublicKey: loads and dumps public keys; 10 | """ 11 | end 12 | -------------------------------------------------------------------------------- /lib/utils/binary.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Utils.BinaryAscii do 2 | @moduledoc false 3 | 4 | def hexFromBinary(data) do 5 | Base.encode16(data) 6 | end 7 | 8 | def binaryFromHex(data) do 9 | Base.decode16!(data) 10 | end 11 | 12 | def numberFromString(string) do 13 | hexFromBinary(string) 14 | |> Integer.parse(16) 15 | |> (fn {parsedInt, ""} -> parsedInt end).() 16 | end 17 | 18 | def stringFromNumber(number, stringLength) do 19 | number 20 | |> Integer.to_string(16) 21 | |> fillNumberString(stringLength) 22 | |> binaryFromHex() 23 | end 24 | 25 | defp fillNumberString(string, stringLength) do 26 | String.duplicate("0", 2 * stringLength - byte_size(string)) <> string 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/signature_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SignatureTest do 2 | use ExUnit.Case 3 | 4 | alias EllipticCurve.{PrivateKey, Ecdsa, Signature} 5 | 6 | test "der conversion" do 7 | privateKey = PrivateKey.generate() 8 | message = "This is a text message" 9 | 10 | signature1 = Ecdsa.sign(message, privateKey) 11 | 12 | der = Signature.toDer(signature1) 13 | {:ok, signature2} = Signature.fromDer(der) 14 | 15 | assert signature1.r == signature2.r 16 | assert signature1.s == signature2.s 17 | end 18 | 19 | test "base64 conversion" do 20 | privateKey = PrivateKey.generate() 21 | message = "This is a text message" 22 | 23 | signature1 = Ecdsa.sign(message, privateKey) 24 | 25 | base64 = Signature.toBase64(signature1) 26 | 27 | {:ok, signature2} = Signature.fromBase64(base64) 28 | 29 | assert signature1.r == signature2.r 30 | assert signature1.s == signature2.s 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to the following versioning pattern: 7 | 8 | Given a version number MAJOR.MINOR.PATCH, increment: 9 | 10 | - MAJOR version when **breaking changes** are introduced; 11 | - MINOR version when **backwards compatible changes** are introduced; 12 | - PATCH version when backwards compatible bug **fixes** are implemented. 13 | 14 | 15 | ## [Unreleased] 16 | 17 | ## [1.1.0] - 2021-11-10 18 | ### Fixed 19 | - point at infinity verification in signature and public key 20 | - missing :crypto Erlang application reference to mix.exs 21 | ### Changed 22 | - internal files and modules structure 23 | - internal .Data structs to be integrated into the respective modules 24 | 25 | ## [1.0.1] - 2021-11-04 26 | ### Fixed 27 | - signature r and s range check 28 | 29 | ## [1.0.0] - 2020-04-14 30 | ### Added 31 | - first official version 32 | -------------------------------------------------------------------------------- /test/privatekey_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrivateKeyTest do 2 | use ExUnit.Case 3 | 4 | alias EllipticCurve.{PrivateKey} 5 | 6 | test "pem conversion" do 7 | privateKey1 = PrivateKey.generate() 8 | 9 | pem = PrivateKey.toPem(privateKey1) 10 | 11 | {:ok, privateKey2} = PrivateKey.fromPem(pem) 12 | 13 | assert privateKey1.secret == privateKey2.secret 14 | assert privateKey1.curve == privateKey2.curve 15 | end 16 | 17 | test "der conversion" do 18 | privateKey1 = PrivateKey.generate() 19 | 20 | der = PrivateKey.toDer(privateKey1) 21 | 22 | {:ok, privateKey2} = PrivateKey.fromDer(der) 23 | 24 | assert privateKey1.secret == privateKey2.secret 25 | assert privateKey1.curve == privateKey2.curve 26 | end 27 | 28 | test "string conversion" do 29 | privateKey1 = PrivateKey.generate() 30 | 31 | string = PrivateKey.toString(privateKey1) 32 | 33 | {:ok, privateKey2} = PrivateKey.fromString(string) 34 | 35 | assert privateKey1.secret == privateKey2.secret 36 | assert privateKey1.curve == privateKey2.curve 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ellipticcurve.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :starkbank_ecdsa, 7 | name: :starkbank_ecdsa, 8 | version: "1.1.0", 9 | homepage_url: "https://starkbank.com", 10 | source_url: "https://github.com/starkbank/ecdsa-elixir", 11 | description: description(), 12 | elixir: "~> 1.9", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | package: package() 16 | ] 17 | end 18 | 19 | defp package do 20 | [ 21 | maintainers: ["Stark Bank"], 22 | licenses: [:MIT], 23 | links: %{ 24 | "StarkBank" => "https://starkbank.com", 25 | "GitHub" => "https://github.com/starkbank/ecdsa-elixir" 26 | } 27 | ] 28 | end 29 | 30 | defp description do 31 | "A lightweight and fast pure Elixir ECDSA library." 32 | end 33 | 34 | def application do 35 | [extra_applications: [:crypto]] 36 | end 37 | 38 | defp deps do 39 | [ 40 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/ecdsa_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EcdsaTest do 2 | use ExUnit.Case 3 | 4 | alias EllipticCurve.{PrivateKey, Signature, Ecdsa} 5 | 6 | test "verify right message" do 7 | privateKey = PrivateKey.generate() 8 | publicKey = PrivateKey.getPublicKey(privateKey) 9 | 10 | message = "This is the right message" 11 | 12 | signature = Ecdsa.sign(message, privateKey) 13 | 14 | assert Ecdsa.verify?(message, signature, publicKey) 15 | end 16 | 17 | test "verify wrong message" do 18 | privateKey = PrivateKey.generate() 19 | publicKey = PrivateKey.getPublicKey(privateKey) 20 | 21 | message1 = "This is the right message" 22 | message2 = "This is the wrong message" 23 | 24 | signature = Ecdsa.sign(message1, privateKey) 25 | 26 | assert !Ecdsa.verify?(message2, signature, publicKey) 27 | end 28 | 29 | test "verify zero signature" do 30 | privateKey = PrivateKey.generate() 31 | publicKey = PrivateKey.getPublicKey(privateKey) 32 | 33 | message = "This is the wrong message" 34 | 35 | assert !Ecdsa.verify?(message, %Signature{r: 0, s: 0}, publicKey) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stark Bank S.A. 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 | -------------------------------------------------------------------------------- /test/openssl_test.exs: -------------------------------------------------------------------------------- 1 | defmodule OpenSslTest do 2 | use ExUnit.Case 3 | 4 | alias EllipticCurve.{PrivateKey, PublicKey, Ecdsa, Signature} 5 | 6 | test "sign" do 7 | # Generated by: openssl ecparam -name secp256k1 -genkey -out privateKey.pem 8 | {:ok, privateKeyPem} = File.read("test/privateKey.pem") 9 | 10 | {:ok, privateKey} = PrivateKey.fromPem(privateKeyPem) 11 | publicKey = PrivateKey.getPublicKey(privateKey) 12 | 13 | {:ok, message} = File.read("test/message.txt") 14 | 15 | signature = Ecdsa.sign(message, privateKey) 16 | 17 | assert Ecdsa.verify?(message, signature, publicKey) 18 | end 19 | 20 | test "verify signature" do 21 | # openssl ec -in privateKey.pem -pubout -out publicKey.pem 22 | 23 | {:ok, publicKeyPem} = File.read("test/publicKey.pem") 24 | 25 | # openssl dgst -sha256 -sign privateKey.pem -out signature.binary message.txt 26 | {:ok, signatureDer} = File.read("test/signatureDer.txt") 27 | 28 | {:ok, message} = File.read("test/message.txt") 29 | 30 | {:ok, publicKey} = PublicKey.fromPem(publicKeyPem) 31 | 32 | {:ok, signature} = Signature.fromDer(signatureDer) 33 | 34 | assert Ecdsa.verify?(message, signature, publicKey) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/publickey_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PublicKeyTest do 2 | use ExUnit.Case 3 | 4 | alias EllipticCurve.{PrivateKey, PublicKey} 5 | 6 | test "pem conversion" do 7 | privateKey = PrivateKey.generate() 8 | publicKey1 = PrivateKey.getPublicKey(privateKey) 9 | 10 | pem = PublicKey.toPem(publicKey1) 11 | 12 | {:ok, publicKey2} = PublicKey.fromPem(pem) 13 | 14 | assert publicKey1.point.x == publicKey2.point.x 15 | assert publicKey1.point.y == publicKey2.point.y 16 | assert publicKey1.curve.name == publicKey2.curve.name 17 | end 18 | 19 | test "der conversion" do 20 | privateKey = PrivateKey.generate() 21 | publicKey1 = PrivateKey.getPublicKey(privateKey) 22 | 23 | der = PublicKey.toDer(publicKey1) 24 | 25 | {:ok, publicKey2} = PublicKey.fromDer(der) 26 | 27 | assert publicKey1.point.x == publicKey2.point.x 28 | assert publicKey1.point.y == publicKey2.point.y 29 | assert publicKey1.curve.name == publicKey2.curve.name 30 | end 31 | 32 | test "string conversion" do 33 | privateKey = PrivateKey.generate() 34 | publicKey1 = PrivateKey.getPublicKey(privateKey) 35 | 36 | string = PublicKey.toString(publicKey1) 37 | 38 | {:ok, publicKey2} = PublicKey.fromString(string) 39 | 40 | assert publicKey1.point.x == publicKey2.point.x 41 | assert publicKey1.point.y == publicKey2.point.y 42 | assert publicKey1.curve.name == publicKey2.curve.name 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/curve/curve.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Curve do 2 | @moduledoc false 3 | 4 | alias EllipticCurve.Utils.Integer, as: IntegerUtils 5 | 6 | @doc """ 7 | Specific elliptic curve data. 8 | 9 | Parameters: 10 | - `:A` [number]: angular coefficient of x in the curve equation. ex: 123 11 | - `:B` [number]: linear coefficient of x in the curve equation. ex: 123 12 | - `:P` [number]: curve modulo. ex: 12345 13 | - `:N` [number]: curve order. ex: 12345 14 | - `:G` [EllipticCurve.Point]: EC Point corresponding to the public key. ex: %Point{x: 123, y: 456} 15 | - `:name` [string]: curve name. ex: "secp256k1" 16 | - `:oid` [list of numbers]: ASN.1 Object Identifier. ex: [1, 3, 132, 0, 10] 17 | """ 18 | defstruct [:A, :B, :P, :N, :G, :name, :oid] 19 | 20 | @doc """ 21 | Verifies if the point `p` is on the curve using the elliptic curve equation: 22 | y^2 = x^3 + A*x + B (mod P) 23 | 24 | Parameters: 25 | - `curve` [%EllipticCurve.Curve]: curve data 26 | - `p` [%EllipticCurve.Point]: curve point 27 | 28 | Returns: 29 | - `result` [boolean]: true if point is in curve, false otherwise 30 | """ 31 | def contains?(curve, p) do 32 | cond do 33 | p.x < 0 || p.x > curve."P" - 1 -> false 34 | p.y < 0 || p.y > curve."P" - 1 -> false 35 | IntegerUtils.ipow(p.y, 2) - (IntegerUtils.ipow(p.x, 3) + curve."A" * p.x + curve."B") 36 | |> IntegerUtils.modulo(curve."P") != 0 -> false 37 | true -> true 38 | end 39 | end 40 | 41 | @doc """ 42 | Gets the curve length 43 | 44 | Parameters: 45 | - `curve` [%EllipticCurve.Curve]: curve data 46 | 47 | Returns: 48 | - `length` [integer]: curve length 49 | """ 50 | def getLength(curve) do 51 | div(1 + String.length(Integer.to_string(curve."N", 16)), 2) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/curve/knownCurves.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Curve.KnownCurves do 2 | @moduledoc """ 3 | Describes the elliptic curves supported by the package 4 | """ 5 | 6 | alias EllipticCurve.{Curve, Point} 7 | 8 | @secp256k1Oid [1, 3, 132, 0, 10] 9 | @secp256k1name :secp256k1 10 | 11 | @prime256v1 [1, 2, 840, 10045, 3, 1, 7] 12 | @prime256v1name :prime256v1 13 | 14 | def getCurveByOid(oid) do 15 | case oid do 16 | @secp256k1Oid -> secp256k1() 17 | @prime256v1 -> prime256v1() 18 | end 19 | end 20 | 21 | def getCurveByName(name) do 22 | case name do 23 | @secp256k1name -> secp256k1() 24 | @prime256v1name -> prime256v1() 25 | end 26 | end 27 | 28 | def secp256k1() do 29 | %Curve{ 30 | name: @secp256k1name, 31 | A: 0x0000000000000000000000000000000000000000000000000000000000000000, 32 | B: 0x0000000000000000000000000000000000000000000000000000000000000007, 33 | P: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, 34 | N: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, 35 | G: %Point{ 36 | x: 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 37 | y: 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 38 | }, 39 | oid: @secp256k1Oid 40 | } 41 | end 42 | 43 | def prime256v1() do 44 | %Curve{ 45 | name: @prime256v1name, 46 | A: 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, 47 | B: 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, 48 | P: 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, 49 | N: 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, 50 | G: %Point{ 51 | x: 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, 52 | y: 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5 53 | }, 54 | oid: @prime256v1 55 | } 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/utils/integer.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Utils.Integer do 2 | @moduledoc false 3 | 4 | use Bitwise 5 | 6 | def modulo(x, n) do 7 | rem(x, n) 8 | |> correctNegativeModulo(n) 9 | end 10 | 11 | defp correctNegativeModulo(r, n) when r < 0 do 12 | r + n 13 | end 14 | 15 | defp correctNegativeModulo(r, _n) do 16 | r 17 | end 18 | 19 | def ipow(base, p, acc \\ 1) 20 | 21 | def ipow(base, p, acc) when p > 0 do 22 | ipow(base, p - 1, base * acc) 23 | end 24 | 25 | def ipow(_base, _p, acc) do 26 | acc 27 | end 28 | 29 | def between(minimum, maximum) when minimum < maximum do 30 | range = maximum - minimum + 1 31 | {bytesNeeded, mask} = calculateParameters(range) 32 | 33 | # We apply the mask to reduce the amount of attempts we might need 34 | # to make to get a number that is in range. This is somewhat like 35 | # the commonly used 'modulo trick', but without the bias: 36 | # 37 | # "Let's say you invoke secure_rand(0, 60). When the other code 38 | # generates a random integer, you might get 243. If you take 39 | # (243 & 63)-- noting that the mask is 63-- you get 51. Since 40 | # 51 is less than 60, we can return this without bias. If we 41 | # got 255, then 255 & 63 is 63. 63 > 60, so we try again. 42 | # 43 | # The purpose of the mask is to reduce the number of random 44 | # numbers discarded for the sake of ensuring an unbiased 45 | # distribution. In the example above, 243 would discard, but 46 | # (243 & 63) is in the range of 0 and 60." 47 | # 48 | # (Source: Scott Arciszewski) 49 | 50 | randomNumber = 51 | :crypto.strong_rand_bytes(bytesNeeded) 52 | |> :binary.bin_to_list() 53 | |> bytesToNumber &&& mask 54 | 55 | if randomNumber < range do 56 | minimum + randomNumber 57 | else 58 | # Outside of the acceptable range, throw it away and try again. 59 | # We don't try any modulo tricks, as this would introduce bias. 60 | between(minimum, maximum) 61 | end 62 | end 63 | 64 | defp bytesToNumber(randomBytes, randomNumber \\ 0, i \\ 0) 65 | 66 | defp bytesToNumber([randomByte | otherRandomBytes], randomNumber, i) do 67 | bytesToNumber( 68 | otherRandomBytes, 69 | randomNumber ||| randomByte <<< (8 * i), 70 | i + 1 71 | ) 72 | end 73 | 74 | defp bytesToNumber([], randomNumber, _i) do 75 | randomNumber 76 | end 77 | 78 | defp calculateParameters(range) do 79 | calculateParameters(range, 1, 0) 80 | end 81 | 82 | defp calculateParameters(range, mask, bitsNeeded) when range > 0 do 83 | calculateParameters( 84 | range >>> 1, 85 | mask <<< 1 ||| 1, 86 | bitsNeeded + 1 87 | ) 88 | end 89 | 90 | defp calculateParameters(_range, mask, bitsNeeded) do 91 | {div(bitsNeeded, 8) + 1, mask} 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/ecdsa.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Ecdsa do 2 | @moduledoc """ 3 | Used to sign and verify signatures using the Elliptic Curve Digital Signature Algorithm (ECDSA) 4 | 5 | Functions: 6 | - `sign()` 7 | - `verify?()` 8 | """ 9 | 10 | alias EllipticCurve.Utils.Integer, as: IntegerUtils 11 | alias EllipticCurve.Utils.BinaryAscii 12 | alias EllipticCurve.{Point, Signature, Math} 13 | 14 | @doc """ 15 | Generates a message signature based on a private key 16 | 17 | Parameters: 18 | - message [string]: message that will be signed 19 | - privateKey [%EllipticCurve.PrivateKey]: private key data associated with the signer 20 | - options [keyword list]: refines request 21 | - hashfunc [:method]: defines the hash function applied to the message. Must be compatible with :crypto.hash. Default: :sha256; 22 | 23 | Returns signature: 24 | - signature [string]: base-64 message signature; 25 | 26 | ## Example: 27 | 28 | iex> EllipticCurve.Ecdsa.sign("my message", privateKey) 29 | "MEQCIFp2TrQ6RlThbEOeYin2t+Dz3TAebeK/kinZaU0Iltm4AiBXyvyCTwgjOBo5eZNssw/3shTqn8eHZyoRiToSttrRFw==" 30 | """ 31 | def sign(message, privateKey, options \\ []) do 32 | %{hashfunc: hashfunc} = Enum.into(options, %{hashfunc: :sha256}) 33 | 34 | numberMessage = 35 | :crypto.hash(hashfunc, message) 36 | |> BinaryAscii.numberFromString() 37 | 38 | curveData = privateKey.curve 39 | 40 | randNum = IntegerUtils.between(1, curveData."N" - 1) 41 | 42 | r = 43 | Math.multiply(curveData."G", randNum, curveData."N", curveData."A", curveData."P").x 44 | |> IntegerUtils.modulo(curveData."N") 45 | 46 | s = 47 | ((numberMessage + r * privateKey.secret) * Math.inv(randNum, curveData."N")) 48 | |> IntegerUtils.modulo(curveData."N") 49 | 50 | %Signature{r: r, s: s} 51 | end 52 | 53 | @doc """ 54 | Verifies a message signature based on a public key 55 | 56 | Parameters: 57 | - `message` [string]: message that will be signed 58 | - `signature` [%EllipticCurve.Signature]: signature associated with the message 59 | - `publicKey` [%EllipticCurve.PublicKey]: public key associated with the message signer 60 | - `options` [keyword list]: refines request 61 | - `:hashfunc` [:method]: defines the hash function applied to the message. Must be compatible with :crypto.hash. Default: :sha256; 62 | 63 | Returns: 64 | - verified [bool]: true if message, public key and signature are compatible, false otherwise; 65 | 66 | ## Example: 67 | 68 | iex> EllipticCurve.Ecdsa.verify?(message, signature, publicKey) 69 | true 70 | iex> EllipticCurve.Ecdsa.verify?(wrongMessage, signature, publicKey) 71 | false 72 | iex> EllipticCurve.Ecdsa.verify?(message, wrongSignature, publicKey) 73 | false 74 | iex> EllipticCurve.Ecdsa.verify?(message, signature, wrongPublicKey) 75 | false 76 | """ 77 | def verify?(message, signature, publicKey, options \\ []) do 78 | %{hashfunc: hashfunc} = Enum.into(options, %{hashfunc: :sha256}) 79 | 80 | numberMessage = 81 | :crypto.hash(hashfunc, message) 82 | |> BinaryAscii.numberFromString() 83 | 84 | curveData = publicKey.curve 85 | 86 | inv = Math.inv(signature.s, curveData."N") 87 | 88 | v = Math.add( 89 | Math.multiply( 90 | curveData."G", 91 | IntegerUtils.modulo(numberMessage * inv, curveData."N"), 92 | curveData."N", 93 | curveData."A", 94 | curveData."P" 95 | ), 96 | Math.multiply( 97 | publicKey.point, 98 | IntegerUtils.modulo(signature.r * inv, curveData."N"), 99 | curveData."N", 100 | curveData."A", 101 | curveData."P" 102 | ), 103 | curveData."A", 104 | curveData."P" 105 | ) 106 | 107 | cond do 108 | signature.r < 1 || signature.r >= curveData."N" -> false 109 | signature.s < 1 || signature.s >= curveData."N" -> false 110 | Point.isAtInfinity?(v) -> false 111 | IntegerUtils.modulo(v.x, curveData."N") != signature.r -> false 112 | true -> true 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A lightweight and fast pure Elixir ECDSA 2 | 3 | ### Overview 4 | 5 | This is an Elixir 1.9+ translation of [Stark Bank]\`s ecdsa-python. It is compatible with OpenSSL and uses elegant math such as Jacobian Coordinates to speed up the ECDSA on pure Elixir. 6 | 7 | ### Installation 8 | 9 | To install [Stark Bank]\`s ECDSA-Elixir, add `starkbank_ecdsa` to your list of dependencies in `mix.exs`: 10 | 11 | ```elixir 12 | def deps do 13 | [ 14 | {:starkbank_ecdsa, "~> 1.1.0"} 15 | ] 16 | end 17 | ``` 18 | 19 | ### Curves 20 | 21 | We currently support `secp256k1` and `prime256v1`, but it's super easy to add more curves to the project. Just add them on `lib/curve/knownCurves.ex` 22 | 23 | ### Speed 24 | 25 | We ran a test on a MAC Pro i7 2017. The libraries were run 100 times and the averages displayed bellow were obtained: 26 | 27 | | Library | sign | verify | 28 | | ------------------ |:-------------:| -------:| 29 | | [crypto] | 1.0ms | 2.0ms | 30 | | starkbank_ecdsa | 1.9ms | 3.8ms | 31 | 32 | ### Sample Code 33 | 34 | How to sign a json message for [Stark Bank]: 35 | 36 | ```elixir 37 | # Generate privateKey from PEM string 38 | privateKey = 39 | EllipticCurve.PrivateKey.fromPem!(""" 40 | -----BEGIN EC PARAMETERS----- 41 | BgUrgQQACg== 42 | -----END EC PARAMETERS----- 43 | -----BEGIN EC PRIVATE KEY----- 44 | MHQCAQEEIODvZuS34wFbt0X53+P5EnSj6tMjfVK01dD1dgDH02RzoAcGBSuBBAAK 45 | oUQDQgAE/nvHu/SQQaos9TUljQsUuKI15Zr5SabPrbwtbfT/408rkVVzq8vAisbB 46 | RmpeRREXj5aog/Mq8RrdYy75W9q/Ig== 47 | -----END EC PRIVATE KEY----- 48 | """) 49 | 50 | # Create message from json (using external Jason package: https://hexdocs.pm/jason/Jason.html) 51 | message = 52 | Jason.encode!(%{ 53 | transfers: [ 54 | %{ 55 | amount: 100_000_000, 56 | taxId: "594.739.480-42", 57 | name: "Daenerys Targaryen Stormborn", 58 | bankCode: "341", 59 | branchCode: "2201", 60 | accountNumber: "76543-8", 61 | tags: ["daenerys", "targaryen", "transfer-1-external-id"] 62 | } 63 | ] 64 | }) 65 | 66 | signature = EllipticCurve.Ecdsa.sign(message, privateKey) 67 | 68 | # Generate Signature in base64. This result can be sent to Stark Bank in the request header as the Digital-Signature parameter. 69 | signature 70 | |> EllipticCurve.Signature.toBase64() 71 | |> IO.puts() 72 | 73 | # To double check if the message matches the signature, do this: 74 | publicKey = privateKey |> EllipticCurve.PrivateKey.getPublicKey() 75 | 76 | Ecdsa.verify?(message, signature, publicKey) |> IO.puts() 77 | ``` 78 | 79 | Simple use: 80 | 81 | ```elixir 82 | # Generate new Keys 83 | privateKey = EllipticCurve.PrivateKey.generate() 84 | publicKey = EllipticCurve.PrivateKey.getPublicKey(privateKey) 85 | 86 | message = "My test message" 87 | 88 | # Generate Signature 89 | signature = EllipticCurve.Ecdsa.sign(message, privateKey) 90 | 91 | # To verify if the signature is valid 92 | EllipticCurve.Ecdsa.verify?(message, signature, publicKey) |> IO.puts() 93 | ``` 94 | 95 | ### OpenSSL 96 | 97 | This library is compatible with OpenSSL, so you can use it to generate keys: 98 | 99 | ``` 100 | openssl ecparam -name secp256k1 -genkey -out privateKey.pem 101 | openssl ec -in privateKey.pem -pubout -out publicKey.pem 102 | ``` 103 | 104 | Create a message.txt file and sign it: 105 | 106 | ``` 107 | openssl dgst -sha256 -sign privateKey.pem -out signatureDer.txt message.txt 108 | ``` 109 | 110 | To verify, do this: 111 | 112 | ```elixir 113 | publicKeyPem = File.read!("publicKey.pem") 114 | signatureDer = File.read!("signatureDer.txt") 115 | message = File.read!("message.txt") 116 | 117 | publicKey = EllipticCurve.PublicKey.fromPem!(publicKeyPem) 118 | signature = EllipticCurve.Signature.fromDer!(signatureDer) 119 | 120 | EllipticCurve.Ecdsa.verify?(message, signature, publicKey) |> IO.puts() 121 | ``` 122 | 123 | You can also verify it on terminal: 124 | 125 | ``` 126 | openssl dgst -sha256 -verify publicKey.pem -signature signatureDer.txt message.txt 127 | ``` 128 | 129 | NOTE: If you want to create a Digital Signature to use with [Stark Bank], you need to convert the binary signature to base64. 130 | 131 | ``` 132 | openssl base64 -in signatureDer.txt -out signatureBase64.txt 133 | ``` 134 | 135 | You can do the same with this library: 136 | 137 | ```python 138 | signatureDer = File.read!("signatureDer.txt") 139 | 140 | signature = EllipticCurve.Signature.fromDer!(signatureDer) 141 | 142 | EllipticCurve.Signature.toBase64(signature) |> IO.puts() 143 | ``` 144 | 145 | ### Run unit tests 146 | 147 | ``` 148 | mix test 149 | ``` 150 | 151 | 152 | [crypto]: https://elixir-lang.org/getting-started/erlang-libraries.html#the-crypto-module 153 | [Stark Bank]: https://starkbank.com 154 | -------------------------------------------------------------------------------- /lib/signature.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Signature do 2 | @moduledoc """ 3 | Used to convert signature between struct (raw numbers r and s) and .der or .pem formats. 4 | 5 | Functions: 6 | - fromBase64() 7 | - fromBase64!() 8 | - fromDer() 9 | - fromDer!() 10 | - toBase64() 11 | - toDer() 12 | """ 13 | 14 | alias __MODULE__, as: Signature 15 | alias EllipticCurve.Utils.{Der, Base64, BinaryAscii} 16 | 17 | @doc """ 18 | Holds signature data. Is usually extracted from base64 strings. 19 | 20 | Parameters: 21 | - `:r` [integer]: first signature number; 22 | - `:s` [integer]: second signature number; 23 | """ 24 | defstruct [:r, :s] 25 | 26 | @doc """ 27 | Converts a base 64 signature into the decoded struct format 28 | 29 | Parameters: 30 | - `base64` [string]: message that will be signed 31 | 32 | Returns {:ok, signature}: 33 | - `signature` [%EllipticCurve.Signature]: decoded signature, exposing r and s; 34 | 35 | ## Example: 36 | 37 | iex> EllipticCurve.Ecdsa.fromBase64("MEYCIQD861pJq/fZE7GnDBycwAbb3YglVoSCVub6TwMkgFS0NgIhAJCEZTh1Mlp1cWCgMXABqh9nOQznEXnhGoSYmZK6T99T") 38 | {:ok, %EllipticCurve.Signature{r: 114398670046563728651181765316495176217036114587592994448444521545026466264118, s: 65366972607021398158454632864220554542282541376523937745916477386966386597715}} 39 | """ 40 | def fromBase64(base64) do 41 | {:ok, fromBase64!(base64)} 42 | rescue 43 | e in RuntimeError -> {:error, e} 44 | end 45 | 46 | @doc """ 47 | Converts a base 64 signature into the decoded struct format 48 | 49 | Parameters: 50 | - base64 [string]: signature in base 64 format 51 | 52 | Returns {:ok, signature}: 53 | - signature [%EllipticCurve.Signature]: decoded signature, exposing r and s; 54 | 55 | ## Example: 56 | 57 | iex> EllipticCurve.Ecdsa.fromBase64!("MEYCIQD861pJq/fZE7GnDBycwAbb3YglVoSCVub6TwMkgFS0NgIhAJCEZTh1Mlp1cWCgMXABqh9nOQznEXnhGoSYmZK6T99T") 58 | %EllipticCurve.Signature{r: 114398670046563728651181765316495176217036114587592994448444521545026466264118, s: 65366972607021398158454632864220554542282541376523937745916477386966386597715} 59 | """ 60 | def fromBase64!(base64String) do 61 | base64String 62 | |> Base64.decode() 63 | |> fromDer!() 64 | end 65 | 66 | @doc """ 67 | Converts a der signature (raw binary) into the decoded struct format 68 | 69 | Parameters: 70 | - `der` [string]: signature in der format (raw binary) 71 | 72 | Returns {:ok, signature}: 73 | - `signature` [%EllipticCurve.Signature]: decoded signature, exposing r and s; 74 | 75 | ## Example: 76 | 77 | iex> EllipticCurve.Ecdsa.fromDer(<<48, 69, 2, 33, 0, 211, 243, 12, 93, ...>>) 78 | {:ok, %EllipticCurve.Signature{r: 95867440227398247533351136059968563162267771464707645727187625451839377520639, s: 35965164910442916948460815891253401171705649249124379540577916592403246631835}} 79 | """ 80 | def fromDer(der) do 81 | {:ok, fromDer!(der)} 82 | rescue 83 | e in RuntimeError -> {:error, e} 84 | end 85 | 86 | @doc """ 87 | Converts a der signature (raw binary) into the decoded struct format 88 | 89 | Parameters: 90 | - `der` [string]: signature in der format (raw binary) 91 | 92 | Returns: 93 | - `signature` [%EllipticCurve.Signature]: decoded signature, exposing r and s; 94 | 95 | ## Example: 96 | 97 | iex> EllipticCurve.Ecdsa.fromDer!(<<48, 69, 2, 33, 0, 211, 243, 12, 93, ...>>) 98 | %EllipticCurve.Signature{r: 95867440227398247533351136059968563162267771464707645727187625451839377520639, s: 35965164910442916948460815891253401171705649249124379540577916592403246631835} 99 | """ 100 | def fromDer!(der) do 101 | {rs, firstEmpty} = Der.removeSequence(der) 102 | 103 | if byte_size(firstEmpty) > 0 do 104 | raise "trailing junk after DER signature: " <> BinaryAscii.hexFromBinary(firstEmpty) 105 | end 106 | 107 | {r, rest} = Der.removeInteger(rs) 108 | {s, secondEmpty} = Der.removeInteger(rest) 109 | 110 | if byte_size(secondEmpty) > 0 do 111 | raise "trailing junk after DER numbers: " <> BinaryAscii.hexFromBinary(secondEmpty) 112 | end 113 | 114 | %Signature{r: r, s: s} 115 | end 116 | 117 | @doc """ 118 | Converts a signature in decoded struct format into a base 64 string 119 | 120 | Parameters: 121 | - `signature` [%EllipticCurve.Signature]: decoded signature struct; 122 | 123 | Returns: 124 | - `base64` [string]: signature in base 64 format 125 | 126 | ## Example: 127 | 128 | iex> EllipticCurve.Ecdsa.toBase64(%EllipticCurve.Signature{r: 123, s: 456}) 129 | "YXNvZGlqYW9pZGphb2lkamFvaWRqc2Fpb3NkamE=" 130 | """ 131 | def toBase64(signature) do 132 | signature 133 | |> toDer() 134 | |> Base64.encode() 135 | end 136 | 137 | @doc """ 138 | Converts a signature in decoded struct format into der format (raw binary) 139 | 140 | Parameters: 141 | - `signature` [%EllipticCurve.Signature]: decoded signature struct; 142 | 143 | Returns: 144 | - `der` [string]: signature in der format 145 | 146 | ## Example: 147 | 148 | iex> EllipticCurve.Ecdsa.toDer(%EllipticCurve.Signature{r: 95867440227398247533351136059968563162267771464707645727187625451839377520639, s: 35965164910442916948460815891253401171705649249124379540577916592403246631835}) 149 | <<48, 69, 2, 33, 0, 211, 243, 12, 93, 107, 214, 149, 243, ...>> 150 | """ 151 | def toDer(signature) do 152 | Der.encodeSequence([ 153 | Der.encodeInteger(signature.r), 154 | Der.encodeInteger(signature.s) 155 | ]) 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/publicKey.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.PublicKey do 2 | @moduledoc """ 3 | Used to convert public keys between struct and .der or .pem formats. 4 | 5 | Functions: 6 | - toPem() 7 | - toDer() 8 | - fromPem() 9 | - fromPem!() 10 | - fromDer() 11 | - fromDer!() 12 | """ 13 | 14 | alias __MODULE__, as: PublicKey 15 | alias EllipticCurve.Utils.{Der, BinaryAscii} 16 | alias EllipticCurve.{Point, Curve, Math} 17 | 18 | @doc """ 19 | Holds public key data. Is usually extracted from .pem files or from the private key itself. 20 | 21 | Parameters: 22 | - `:point` [%EllipticCurve.Utils.Point]: public key point data; 23 | - `:curve` [%EllipticCurve.Curve]: public key curve information; 24 | """ 25 | defstruct [:point, :curve] 26 | 27 | @doc """ 28 | Converts a public key in decoded struct format into a pem string 29 | 30 | Parameters: 31 | - `publicKey` [%EllipticCurve.PublicKey]: decoded public key struct; 32 | 33 | Returns: 34 | - `pem` [string]: public key in pem format 35 | 36 | ## Example: 37 | 38 | iex> EllipticCurve.PublicKey.toPem(%EllipticCurve.PublicKey{...}) 39 | "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAErp2I78X4cqHscCRWMT4rhouyO197iQXR\nfdGgsgfS/UGaIviYiqnG3SSa9dsOHU/NkVSTLkBPCI0RQLF3554dZg==\n-----END PUBLIC KEY-----\n" 40 | """ 41 | def toPem(publicKey) do 42 | publicKey 43 | |> toDer() 44 | |> Der.toPem("PUBLIC KEY") 45 | end 46 | 47 | @doc """ 48 | Converts a public key in decoded struct format into a der string (raw binary) 49 | 50 | Parameters: 51 | - `publicKey` [%EllipticCurve.PublicKey]: decoded public key struct; 52 | 53 | Returns: 54 | - `der` [string]: public key in der format 55 | 56 | ## Example: 57 | 58 | iex> EllipticCurve.PublicKey.toDer(%EllipticCurve.PublicKey{...}) 59 | <<48, 86, 48, 16, 6, 7, 42, 134, 72, 206, 61, ...>> 60 | """ 61 | def toDer(publicKey) do 62 | Der.encodeSequence([ 63 | Der.encodeSequence([ 64 | Der.encodeOid([1, 2, 840, 10045, 2, 1]), 65 | Der.encodeOid(publicKey.curve.oid) 66 | ]), 67 | Der.encodeBitString(toString(publicKey, true)) 68 | ]) 69 | end 70 | 71 | @doc false 72 | def toString(publicKey, encoded \\ false) do 73 | curveLength = Curve.getLength(publicKey.curve) 74 | 75 | xString = 76 | BinaryAscii.stringFromNumber( 77 | publicKey.point.x, 78 | curveLength 79 | ) 80 | 81 | yString = 82 | BinaryAscii.stringFromNumber( 83 | publicKey.point.y, 84 | curveLength 85 | ) 86 | 87 | if encoded do 88 | "\x00\x04" <> xString <> yString 89 | else 90 | xString <> yString 91 | end 92 | end 93 | 94 | @doc """ 95 | Converts a public key in pem format into decoded struct format 96 | 97 | Parameters: 98 | - `pem` [string]: public key in pem format 99 | 100 | Returns {:ok, publicKey}: 101 | - `publicKey` [%EllipticCurve.PublicKey]: decoded public key struct; 102 | 103 | ## Example: 104 | 105 | iex> EllipticCurve.PublicKey.fromPem("-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAErp2I78X4cqHscCRWMT4rhouyO197iQXR\nfdGgsgfS/UGaIviYiqnG3SSa9dsOHU/NkVSTLkBPCI0RQLF3554dZg==\n-----END PUBLIC KEY-----\n") 106 | {:ok, %EllipticCurve.PublicKey{...}} 107 | """ 108 | def fromPem(pem) do 109 | {:ok, fromPem!(pem)} 110 | rescue 111 | e in RuntimeError -> {:error, e} 112 | end 113 | 114 | @doc """ 115 | Converts a public key in pem format into decoded struct format 116 | 117 | Parameters: 118 | - `pem` [string]: public key in pem format 119 | 120 | Returns: 121 | - `publicKey` [%EllipticCurve.PublicKey]: decoded public key struct; 122 | 123 | ## Example: 124 | 125 | iex> EllipticCurve.PublicKey.fromPem!("-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAErp2I78X4cqHscCRWMT4rhouyO197iQXR\nfdGgsgfS/UGaIviYiqnG3SSa9dsOHU/NkVSTLkBPCI0RQLF3554dZg==\n-----END PUBLIC KEY-----\n") 126 | %EllipticCurve.PublicKey{...} 127 | """ 128 | def fromPem!(pem) do 129 | pem 130 | |> Der.fromPem() 131 | |> fromDer!() 132 | end 133 | 134 | @doc """ 135 | Converts a public key in der (raw binary) format into decoded struct format 136 | 137 | Parameters: 138 | - `der` [string]: public key in der format 139 | 140 | Returns {:ok, publicKey}: 141 | - `publicKey` [%EllipticCurve.PublicKey]: decoded public key struct; 142 | 143 | ## Example: 144 | 145 | iex> EllipticCurve.PublicKey.fromDer(<<48, 86, 48, 16, 6, 7, 42, 134, ...>>) 146 | {:ok, %EllipticCurve.PublicKey{...}} 147 | """ 148 | def fromDer(der) do 149 | {:ok, fromDer!(der)} 150 | rescue 151 | e in RuntimeError -> {:error, e} 152 | end 153 | 154 | @doc """ 155 | Converts a public key in der (raw binary) format into decoded struct format 156 | 157 | Parameters: 158 | - `der` [string]: public key in der format 159 | 160 | Returns: 161 | - `publicKey` [%EllipticCurve.PublicKey]: decoded public key struct; 162 | 163 | ## Example: 164 | 165 | iex> EllipticCurve.PublicKey.fromDer!(<<48, 86, 48, 16, 6, 7, 42, 134, ...>>) 166 | %EllipticCurve.PublicKey{...} 167 | """ 168 | def fromDer!(der) do 169 | {s1, empty} = Der.removeSequence(der) 170 | 171 | if byte_size(empty) != 0 do 172 | raise "trailing junk after DER public key: #{BinaryAscii.hexFromBinary(empty)}" 173 | end 174 | 175 | {s2, pointBitString} = Der.removeSequence(s1) 176 | 177 | {_oidPublicKey, rest} = Der.removeObject(s2) 178 | 179 | {oidCurve, empty} = Der.removeObject(rest) 180 | 181 | if byte_size(empty) != 0 do 182 | raise "trailing junk after DER public key objects: #{BinaryAscii.hexFromBinary(empty)}" 183 | end 184 | 185 | curve = Curve.KnownCurves.getCurveByOid(oidCurve) 186 | 187 | {pointString, empty} = Der.removeBitString(pointBitString) 188 | 189 | if byte_size(empty) != 0 do 190 | raise "trailing junk after public key point-string: #{BinaryAscii.hexFromBinary(empty)}" 191 | end 192 | 193 | binary_part(pointString, 2, byte_size(pointString) - 2) 194 | |> fromString!(curve.name) 195 | end 196 | 197 | @doc false 198 | def fromString(string, curve \\ :secp256k1, validatePoint \\ true) do 199 | {:ok, fromString!(string, curve, validatePoint)} 200 | rescue 201 | e in RuntimeError -> {:error, e} 202 | end 203 | 204 | @doc false 205 | def fromString!(string, curve \\ :secp256k1, validatePoint \\ true) do 206 | curve = Curve.KnownCurves.getCurveByName(curve) 207 | baseLength = Curve.getLength(curve) 208 | 209 | xs = binary_part(string, 0, baseLength) 210 | ys = binary_part(string, baseLength, byte_size(string) - baseLength) 211 | 212 | point = %Point{ 213 | x: BinaryAscii.numberFromString(xs), 214 | y: BinaryAscii.numberFromString(ys) 215 | } 216 | 217 | publicKey = %PublicKey{point: point, curve: curve} 218 | 219 | cond do 220 | validatePoint == false -> publicKey 221 | Point.isAtInfinity?(point) -> 222 | raise "Public Key point is at infinity" 223 | Curve.contains?(curve, point) == false -> 224 | raise "Point (#{point.x},#{point.y}) is not valid for curve #{curve.name}" 225 | Point.isAtInfinity?(Math.multiply(point, curve."N", curve."N", curve."A", curve."P")) == false -> 226 | raise "Point (#{point.x},#{point.y}) * #{curve.name}.N is not at infinity" 227 | true -> publicKey 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /lib/math.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Math do 2 | @moduledoc false 3 | 4 | alias EllipticCurve.Utils.Integer, as: IntegerUtils 5 | alias EllipticCurve.Point 6 | 7 | @doc """ 8 | Fast way to multily point and scalar in elliptic curves 9 | 10 | - `p` [integer]: First Point to mutiply 11 | - `n` [integer]: Scalar to mutiply 12 | - `cN` [integer]: Order of the elliptic curve 13 | - `cP` [integer]: Prime number in the module of the equation Y^2 = X^3 + cA*X + B (mod p) 14 | - `cA` [integer]: Coefficient of the first-order term of the equation Y^2 = X^3 + cA*X + B (mod p) 15 | 16 | Returns: 17 | - `point` [%EllipticCurve.Point]: point that represents the sum of First and Second Point 18 | """ 19 | def multiply(p, n, cN, cA, cP) do 20 | p 21 | |> toJacobian() 22 | |> jacobianMultiply(n, cN, cA, cP) 23 | |> fromJacobian(cP) 24 | end 25 | 26 | @doc """ 27 | Fast way to add two points in elliptic curves 28 | 29 | - `p` [integer]: First Point you want to add 30 | - `q` [integer]: Second Point you want to add 31 | - `cP` [integer]: Prime number in the module of the equation Y^2 = X^3 + cA*X + B (mod p) 32 | - `cA` [integer]: Coefficient of the first-order term of the equation Y^2 = X^3 + cA*X + B (mod p) 33 | 34 | Returns: 35 | - `point` [%EllipticCurve.Point]: point that represents the sum of First and Second Point 36 | """ 37 | def add(p, q, cA, cP) do 38 | jacobianAdd(toJacobian(p), toJacobian(q), cA, cP) 39 | |> fromJacobian(cP) 40 | end 41 | 42 | @doc """ 43 | Extended Euclidean Algorithm. It's the 'division' in elliptic curves 44 | 45 | - `x` [integer]: Divisor 46 | - `n` [integer]: Mod for division 47 | 48 | Returns: 49 | - `value` [integer]: value representing the division 50 | """ 51 | def inv(x, _n) when x == 0 do 52 | 0 53 | end 54 | 55 | def inv(x, n) do 56 | invOperator(1, 0, IntegerUtils.modulo(x, n), n) 57 | |> IntegerUtils.modulo(n) 58 | end 59 | 60 | defp invOperator(lm, hm, low, high) when low > 1 do 61 | r = div(high, low) 62 | 63 | invOperator( 64 | hm - lm * r, 65 | lm, 66 | high - low * r, 67 | low 68 | ) 69 | end 70 | 71 | defp invOperator(lm, _hm, _low, _high) do 72 | lm 73 | end 74 | 75 | # Converts point back from Jacobian coordinates 76 | 77 | # - `p` [integer]: First Point you want to add 78 | # - `cP` [integer]: Prime number in the module of the equation Y^2 = X^3 + cA*X + B (mod p) 79 | 80 | # Returns: 81 | # - `point` [%EllipticCurve.Point]: point in default coordinates 82 | defp toJacobian(p) do 83 | %Point{x: p.x, y: p.y, z: 1} 84 | end 85 | 86 | defp fromJacobian(p, cP) do 87 | z = inv(p.z, cP) 88 | 89 | %Point{ 90 | x: 91 | IntegerUtils.modulo( 92 | p.x * IntegerUtils.ipow(z, 2), 93 | cP 94 | ), 95 | y: 96 | IntegerUtils.modulo( 97 | p.y * IntegerUtils.ipow(z, 3), 98 | cP 99 | ) 100 | } 101 | end 102 | 103 | # Doubles a point in elliptic curves 104 | 105 | # - `p` [integer]: Point you want to double 106 | # - `cP` [integer]: Prime number in the module of the equation Y^2 = X^3 + cA*X + B (mod p) 107 | # - `cA` [integer]: Coefficient of the first-order term of the equation Y^2 = X^3 + cA*X + B (mod p) 108 | 109 | # Returns: 110 | # - `point` [%EllipticCurve.Point]: point that represents the sum of First and Second Point 111 | defp jacobianDouble(p, cA, cP) do 112 | if p.y == 0 do 113 | %Point{x: 0, y: 0, z: 0} 114 | else 115 | ysq = 116 | IntegerUtils.ipow(p.y, 2) 117 | |> IntegerUtils.modulo(cP) 118 | 119 | s = 120 | (4 * p.x * ysq) 121 | |> IntegerUtils.modulo(cP) 122 | 123 | m = 124 | (3 * IntegerUtils.ipow(p.x, 2) + cA * IntegerUtils.ipow(p.z, 4)) 125 | |> IntegerUtils.modulo(cP) 126 | 127 | nx = 128 | (IntegerUtils.ipow(m, 2) - 2 * s) 129 | |> IntegerUtils.modulo(cP) 130 | 131 | ny = 132 | (m * (s - nx) - 8 * IntegerUtils.ipow(ysq, 2)) 133 | |> IntegerUtils.modulo(cP) 134 | 135 | nz = 136 | (2 * p.y * p.z) 137 | |> IntegerUtils.modulo(cP) 138 | 139 | %Point{x: nx, y: ny, z: nz} 140 | end 141 | end 142 | 143 | # Adds two points in the elliptic curve 144 | # - `p` [integer]: First Point you want to add 145 | # - `q` [integer]: Second Point you want to add 146 | # - `cP` [integer]: Prime number in the module of the equation Y^2 = X^3 + cA*X + B (mod p) 147 | # - `cA` [integer]: Coefficient of the first-order term of the equation Y^2 = X^3 + cA*X + B (mod p) 148 | 149 | # Returns: 150 | # - `point` [%EllipticCurve.Point]: point that represents the sum of first and second Point 151 | defp jacobianAdd(p, q, cA, cP) do 152 | if p.y == 0 do 153 | q 154 | else 155 | if q.y == 0 do 156 | p 157 | else 158 | u1 = 159 | (p.x * IntegerUtils.ipow(q.z, 2)) 160 | |> IntegerUtils.modulo(cP) 161 | 162 | u2 = 163 | (q.x * IntegerUtils.ipow(p.z, 2)) 164 | |> IntegerUtils.modulo(cP) 165 | 166 | s1 = 167 | (p.y * IntegerUtils.ipow(q.z, 3)) 168 | |> IntegerUtils.modulo(cP) 169 | 170 | s2 = 171 | (q.y * IntegerUtils.ipow(p.z, 3)) 172 | |> IntegerUtils.modulo(cP) 173 | 174 | if u1 == u2 do 175 | if s1 != s2 do 176 | %Point{x: 0, y: 0, z: 1} 177 | else 178 | jacobianDouble(p, cA, cP) 179 | end 180 | else 181 | h = u2 - u1 182 | 183 | r = s2 - s1 184 | 185 | h2 = 186 | (h * h) 187 | |> IntegerUtils.modulo(cP) 188 | 189 | h3 = 190 | (h * h2) 191 | |> IntegerUtils.modulo(cP) 192 | 193 | u1h2 = 194 | (u1 * h2) 195 | |> IntegerUtils.modulo(cP) 196 | 197 | nx = 198 | (IntegerUtils.ipow(r, 2) - h3 - 2 * u1h2) 199 | |> IntegerUtils.modulo(cP) 200 | 201 | ny = 202 | (r * (u1h2 - nx) - s1 * h3) 203 | |> IntegerUtils.modulo(cP) 204 | 205 | nz = 206 | (h * p.z * q.z) 207 | |> IntegerUtils.modulo(cP) 208 | 209 | %Point{x: nx, y: ny, z: nz} 210 | end 211 | end 212 | end 213 | end 214 | 215 | # Multily point and scalar in elliptic curves 216 | 217 | # - `p` [integer]: First Point to mutiply 218 | # - `n` [integer]: Scalar to mutiply 219 | # - `cN` [integer]: Order of the elliptic curve 220 | # - `cP` [integer]: Prime number in the module of the equation Y^2 = X^3 + cA*X + B (mod p) 221 | # - `cA` [integer]: Coefficient of the first-order term of the equation Y^2 = X^3 + cA*X + B (mod p) 222 | 223 | # Returns: 224 | # - `point` [%EllipticCurve.Point]: point that represents the sum of First and Second Point 225 | defp jacobianMultiply(_p, n, _cN, _cA, _cP) when n == 0 do 226 | %Point{x: 0, y: 0, z: 1} 227 | end 228 | 229 | defp jacobianMultiply(p, n, _cN, _cA, _cP) when n == 1 do 230 | if p.y == 0 do 231 | %Point{x: 0, y: 0, z: 1} 232 | else 233 | p 234 | end 235 | end 236 | 237 | defp jacobianMultiply(p, n, cN, cA, cP) when n < 0 or n >= cN do 238 | if p.y == 0 do 239 | %Point{x: 0, y: 0, z: 1} 240 | else 241 | jacobianMultiply(p, IntegerUtils.modulo(n, cN), cN, cA, cP) 242 | end 243 | end 244 | 245 | defp jacobianMultiply(p, _n, _cN, _cA, _cP) when p.y == 0 do 246 | %Point{x: 0, y: 0, z: 1} 247 | end 248 | 249 | defp jacobianMultiply(p, n, cN, cA, cP) when rem(n, 2) == 0 do 250 | jacobianMultiply(p, div(n, 2), cN, cA, cP) 251 | |> jacobianDouble(cA, cP) 252 | end 253 | 254 | defp jacobianMultiply(p, n, cN, cA, cP) do 255 | jacobianMultiply(p, div(n, 2), cN, cA, cP) 256 | |> jacobianDouble(cA, cP) 257 | |> jacobianAdd(p, cA, cP) 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/utils/der.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.Utils.Der do 2 | @moduledoc false 3 | 4 | use Bitwise 5 | 6 | @hexAt "\x00" 7 | @hexB "\x02" 8 | @hexC "\x03" 9 | @hexD "\x04" 10 | @hexF "\x06" 11 | @hex0 "\x30" 12 | 13 | @hex31 0x1F 14 | @hex127 0x7F 15 | @hex129 0xA0 16 | @hex160 0x80 17 | @hex224 0xE0 18 | 19 | alias EllipticCurve.Utils.{BinaryAscii, Base64} 20 | 21 | def encodeSequence(encodedPieces) do 22 | Enum.sum(for piece <- encodedPieces, do: byte_size(piece)) 23 | |> (fn totalLength -> <<@hex0>> <> encodeLength(totalLength) <> Enum.join(encodedPieces) end).() 24 | end 25 | 26 | def encodeInteger(x) when x >= 0 do 27 | bin = 28 | x 29 | |> Integer.to_string(16) 30 | |> complementIntegerString() 31 | |> BinaryAscii.binaryFromHex() 32 | 33 | if getFirstByte(bin) <= @hex127 do 34 | @hexB <> <> <> bin 35 | else 36 | @hexB <> <> <> @hexAt <> bin 37 | end 38 | end 39 | 40 | defp complementIntegerString(x) when rem(byte_size(x), 2) == 1 do 41 | "0" <> x 42 | end 43 | 44 | defp complementIntegerString(x) do 45 | x 46 | end 47 | 48 | def encodeOid([first | [second | pieces]]) when first <= 2 and second <= 39 do 49 | ([<<40 * first + second>>] ++ for(piece <- pieces, do: encodeNumber(piece))) 50 | |> Enum.join() 51 | |> (fn body -> @hexF <> encodeLength(byte_size(body)) <> body end).() 52 | end 53 | 54 | def encodeBitString(t) do 55 | @hexC <> encodeLength(byte_size(t)) <> t 56 | end 57 | 58 | def encodeOctetString(t) do 59 | @hexD <> encodeLength(byte_size(t)) <> t 60 | end 61 | 62 | def encodeConstructed(tag, value) do 63 | <<@hex129 + tag>> <> encodeLength(byte_size(value)) <> value 64 | end 65 | 66 | def removeSequence(string) do 67 | trimmedString = checkSequenceError(string, @hex0) 68 | 69 | splitOnLength(trimmedString) 70 | end 71 | 72 | def removeInteger(string) do 73 | trimmedString = checkSequenceError(string, @hexB) 74 | 75 | {numberBytes, rest} = splitOnLength(trimmedString) 76 | 77 | if getFirstByte(numberBytes) >= @hex160 do 78 | throw("nBytes #{getFirstByte(numberBytes)} >= #{@hex160}") 79 | end 80 | 81 | {parsed, ""} = 82 | Integer.parse( 83 | BinaryAscii.hexFromBinary(numberBytes), 84 | 16 85 | ) 86 | 87 | { 88 | parsed, 89 | rest 90 | } 91 | end 92 | 93 | def removeObject(string) do 94 | trimmedString = checkSequenceError(string, @hexF) 95 | 96 | {body, rest} = splitOnLength(trimmedString) 97 | 98 | [n0 | numbers] = removeObjectRecursion(body) 99 | 100 | first = div(n0, 40) 101 | second = n0 - 40 * first 102 | 103 | {[first, second] ++ numbers, rest} 104 | end 105 | 106 | defp removeObjectRecursion(body) when byte_size(body) == 0 do 107 | [] 108 | end 109 | 110 | defp removeObjectRecursion(body) do 111 | {n, lengthLength} = readNumber(body) 112 | 113 | numbers = 114 | binary_part(body, lengthLength, byte_size(body) - lengthLength) 115 | |> removeObjectRecursion() 116 | 117 | [n | numbers] 118 | end 119 | 120 | def removeBitString(string) do 121 | trimmedString = checkSequenceError(string, @hexC) 122 | 123 | splitOnLength(trimmedString) 124 | end 125 | 126 | def removeOctetString(string) do 127 | trimmedString = checkSequenceError(string, @hexD) 128 | 129 | splitOnLength(trimmedString) 130 | end 131 | 132 | def removeConstructed(<> <> trimmedString) do 133 | if (s0 &&& @hex224) != @hex129 do 134 | throw("wanted constructed tag (0xa0-0xbf), got #{Integer.to_string(s0, 16)}") 135 | end 136 | 137 | {body, rest} = splitOnLength(trimmedString) 138 | 139 | { 140 | s0 &&& @hex31, 141 | body, 142 | rest 143 | } 144 | end 145 | 146 | def fromPem(pem) do 147 | pem 148 | |> :binary.split(["\r", "\n", "\r\n"], [:global]) 149 | |> filterPemLine() 150 | |> Enum.join() 151 | |> Base64.decode() 152 | end 153 | 154 | defp filterPemLine([line | rest]) do 155 | lines = filterPemLine(rest) 156 | cleanLine = line |> String.trim() 157 | 158 | if byte_size(cleanLine) == 0 or String.starts_with?(cleanLine, "-----") do 159 | lines 160 | else 161 | [cleanLine | lines] 162 | end 163 | end 164 | 165 | defp filterPemLine([]) do 166 | [] 167 | end 168 | 169 | def toPem(der, name) do 170 | b64 = 171 | der 172 | |> Base64.encode() 173 | |> makeLines() 174 | 175 | (["-----BEGIN #{name}-----\n"] ++ b64 ++ ["-----END #{name}-----\n"]) 176 | |> Enum.join() 177 | end 178 | 179 | defp makeLines(content) when byte_size(content) > 64 do 180 | [ 181 | binary_part(content, 0, 64) <> "\n" 182 | | makeLines(binary_part(content, 64, byte_size(content) - 64)) 183 | ] 184 | end 185 | 186 | defp makeLines(content) do 187 | [content <> "\n"] 188 | end 189 | 190 | def encodeLength(lengthValue) when lengthValue > 0 and lengthValue < @hex160 do 191 | <> 192 | end 193 | 194 | def encodeLength(lengthValue) when lengthValue > 0 do 195 | lengthValue 196 | |> Integer.to_string(16) 197 | |> checkOddity() 198 | |> BinaryAscii.binaryFromHex() 199 | |> (fn s -> <<@hex160 ||| byte_size(s)>> <> s end).() 200 | end 201 | 202 | defp checkOddity(s) when rem(byte_size(s), 2) == 1 do 203 | "0" <> s 204 | end 205 | 206 | defp checkOddity(s) do 207 | s 208 | end 209 | 210 | def encodeNumber(n) do 211 | encodeNumberRecursive(n) 212 | |> finishEncoding() 213 | end 214 | 215 | defp encodeNumberRecursive(n) when n > 0 do 216 | encodeNumberRecursive(n >>> 7) <> <<(n &&& @hex127) ||| @hex160>> 217 | end 218 | 219 | defp encodeNumberRecursive(_n) do 220 | <<>> 221 | end 222 | 223 | defp finishEncoding(<<>>) do 224 | <<0>> 225 | end 226 | 227 | defp finishEncoding(<> <> rest) when byte_size(rest) > 0 do 228 | <> <> finishEncoding(rest) 229 | end 230 | 231 | defp finishEncoding(<>) do 232 | <> 233 | end 234 | 235 | defp readNumber(string, number \\ 0, lengthLength \\ 0) do 236 | if lengthLength > byte_size(string) do 237 | throw("ran out of length bytes") 238 | end 239 | 240 | if lengthLength > 0 and 241 | (getFirstByte(binary_part(string, lengthLength - 1, 1)) &&& @hex160) == 0 do 242 | {number, lengthLength} 243 | else 244 | readNumber( 245 | string, 246 | (number <<< 7) + (getFirstByte(binary_part(string, lengthLength, 1)) &&& @hex127), 247 | lengthLength + 1 248 | ) 249 | end 250 | end 251 | 252 | defp splitOnLength(string) do 253 | {bodyLength, lengthLength} = readLength(string) 254 | 255 | { 256 | binary_part(string, lengthLength, bodyLength), 257 | binary_part( 258 | string, 259 | bodyLength + lengthLength, 260 | byte_size(string) - lengthLength - bodyLength 261 | ) 262 | } 263 | end 264 | 265 | defp readLength(string) do 266 | num = getFirstByte(string) 267 | 268 | if (num &&& @hex160) == 0 do 269 | {num &&& @hex127, 1} 270 | else 271 | lengthLength = num &&& @hex127 272 | 273 | if lengthLength > byte_size(string) - 1 do 274 | throw("ran out of length bytes") 275 | end 276 | 277 | {parsed, ""} = 278 | Integer.parse( 279 | BinaryAscii.hexFromBinary(binary_part(string, 1, lengthLength)), 280 | 16 281 | ) 282 | 283 | { 284 | parsed, 285 | 1 + lengthLength 286 | } 287 | end 288 | end 289 | 290 | defp checkSequenceError(<> <> rest, start) do 291 | if <> != start do 292 | throw("wanted sequence #{Base.encode16(start)}, got #{Base.encode16(<>)}") 293 | end 294 | 295 | rest 296 | end 297 | 298 | defp getFirstByte(<> <> _rest) do 299 | first 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /lib/privateKey.ex: -------------------------------------------------------------------------------- 1 | defmodule EllipticCurve.PrivateKey do 2 | @moduledoc """ 3 | Used to create private keys or convert them between struct and .der or .pem formats. Also allows creations of public keys from private keys. 4 | 5 | Functions: 6 | - generate() 7 | - toPem() 8 | - toDer() 9 | - fromPem() 10 | - fromPem!() 11 | - fromDer() 12 | - fromDer!() 13 | """ 14 | 15 | alias __MODULE__, as: PrivateKey 16 | alias EllipticCurve.Utils.Integer, as: IntegerUtils 17 | alias EllipticCurve.Utils.{Der, BinaryAscii} 18 | alias EllipticCurve.{PublicKey, Curve, Math} 19 | 20 | @doc """ 21 | Holds private key data. Is usually extracted from .pem files. 22 | 23 | Parameters: 24 | - `:secret` [integer]: private key secret number; 25 | - `:curve` [%EllipticCurve.Curve]: private key curve information; 26 | """ 27 | defstruct [:secret, :curve] 28 | 29 | @hexAt "\x00" 30 | 31 | @doc """ 32 | Creates a new private key 33 | 34 | Parameters: 35 | - `secret` [integer]: private key secret; Default: nil -> random key will be generated; 36 | - `curve` [atom]: curve name; Default: :secp256k1; 37 | 38 | Returns: 39 | - `privateKey` [%EllipticCurve.PrivateKey]: private key struct 40 | 41 | ## Example: 42 | 43 | iex> EllipticCurve.PrivateKey.generate() 44 | %EllipticCurve.PrivateKey{...} 45 | """ 46 | def generate(secret \\ nil, curve \\ :secp256k1) 47 | 48 | def generate(secret, curve) when is_nil(secret) do 49 | generate( 50 | IntegerUtils.between( 51 | 1, 52 | Curve.KnownCurves.getCurveByName(curve)."N" - 1 53 | ), 54 | curve 55 | ) 56 | end 57 | 58 | def generate(secret, curve) do 59 | %PrivateKey{ 60 | secret: secret, 61 | curve: Curve.KnownCurves.getCurveByName(curve) 62 | } 63 | end 64 | 65 | @doc """ 66 | Gets the public associated with a private key 67 | 68 | Parameters: 69 | - `privateKey` [%EllipticCurve.PrivateKey]: private key struct 70 | 71 | Returns: 72 | - `publicKey` [%EllipticCurve.PublicKey]: public key struct 73 | 74 | ## Example: 75 | 76 | iex> EllipticCurve.PrivateKey.getPublicKey(privateKey) 77 | %EllipticCurve.PublicKey{...} 78 | """ 79 | def getPublicKey(privateKey) do 80 | curve = privateKey.curve 81 | %PublicKey{ 82 | point: 83 | Math.multiply( 84 | curve."G", 85 | privateKey.secret, 86 | curve."N", 87 | curve."A", 88 | curve."P" 89 | ), 90 | curve: curve 91 | } 92 | end 93 | 94 | @doc """ 95 | Converts a private key in decoded struct format into a pem string 96 | 97 | Parameters: 98 | - `privateKey` [%EllipticCurve.PrivateKey]: decoded private key struct; 99 | 100 | Returns: 101 | - `pem` [string]: private key in pem format 102 | 103 | ## Example: 104 | 105 | iex> EllipticCurve.PrivateKey.toPem(%EllipticCurve.PrivateKey{...}) 106 | "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIDvS/RddF6iYa/q4oVSrGa3Kbd7aSooNpwhv9puJVv1loAcGBSuBBAAK\noUQDQgAErp2I78X4cqHscCRWMT4rhouyO197iQXRfdGgsgfS/UGaIviYiqnG3SSa\n9dsOHU/NkVSTLkBPCI0RQLF3554dZg==\n-----END EC PRIVATE KEY-----\n" 107 | """ 108 | def toPem(privateKey) do 109 | Der.toPem( 110 | toDer(privateKey), 111 | "EC PRIVATE KEY" 112 | ) 113 | end 114 | 115 | @doc """ 116 | Converts a private key in decoded struct format into a der string (raw binary) 117 | 118 | Parameters: 119 | - `privateKey` [$EllipticCurve.PrivateKey]: decoded private key struct; 120 | 121 | Returns: 122 | - `der` [string]: private key in der format 123 | 124 | ## Example: 125 | 126 | iex> EllipticCurve.PrivateKey.toDer(%EllipticCurve.PrivateKey{...}) 127 | <<48, 116, 2, 1, 1, 4, 32, 59, 210, 253, 23, 93, 23, ...>> 128 | """ 129 | def toDer(privateKey) do 130 | Der.encodeSequence([ 131 | Der.encodeInteger(1), 132 | Der.encodeOctetString(toString(privateKey)), 133 | Der.encodeConstructed(0, Der.encodeOid(privateKey.curve.oid)), 134 | Der.encodeConstructed( 135 | 1, 136 | Der.encodeBitString(PublicKey.toString(getPublicKey(privateKey), true)) 137 | ) 138 | ]) 139 | end 140 | 141 | @doc false 142 | def toString(privateKey) do 143 | BinaryAscii.stringFromNumber(privateKey.secret, Curve.getLength(privateKey.curve)) 144 | end 145 | 146 | @doc """ 147 | Converts a private key in pem format into decoded struct format 148 | 149 | Parameters: 150 | - `pem` [string]: private key in pem format 151 | 152 | Returns {:ok, privateKey}: 153 | - `privateKey` [%EllipticCurve.PrivateKey]: decoded private key struct; 154 | 155 | ## Example: 156 | 157 | iex> EllipticCurve.PrivateKey.fromPem("-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIDvS/RddF6iYa/q4oVSrGa3Kbd7aSooNpwhv9puJVv1loAcGBSuBBAAK\noUQDQgAErp2I78X4cqHscCRWMT4rhouyO197iQXRfdGgsgfS/UGaIviYiqnG3SSa\n9dsOHU/NkVSTLkBPCI0RQLF3554dZg==\n-----END EC PRIVATE KEY-----\n") 158 | {:ok, %EllipticCurve.PrivateKey{...}} 159 | """ 160 | def fromPem(pem) do 161 | {:ok, fromPem!(pem)} 162 | rescue 163 | e in RuntimeError -> {:error, e} 164 | end 165 | 166 | @doc """ 167 | Converts a private key in pem format into decoded struct format 168 | 169 | Parameters: 170 | - `pem` [string]: private key in pem format 171 | 172 | Returns: 173 | - `privateKey` [%EllipticCurve.PrivateKey]: decoded private key struct; 174 | 175 | ## Example: 176 | 177 | iex> EllipticCurve.PrivateKey.fromPem!("-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIDvS/RddF6iYa/q4oVSrGa3Kbd7aSooNpwhv9puJVv1loAcGBSuBBAAK\noUQDQgAErp2I78X4cqHscCRWMT4rhouyO197iQXRfdGgsgfS/UGaIviYiqnG3SSa\n9dsOHU/NkVSTLkBPCI0RQLF3554dZg==\n-----END EC PRIVATE KEY-----\n") 178 | %EllipticCurve.PrivateKey{...} 179 | """ 180 | def fromPem!(pem) do 181 | String.split(pem, "-----BEGIN EC PRIVATE KEY-----") 182 | |> List.last() 183 | |> Der.fromPem() 184 | |> fromDer! 185 | end 186 | 187 | @doc """ 188 | Converts a private key in der format into decoded struct format 189 | 190 | Parameters: 191 | - `der` [string]: private key in der format 192 | 193 | Returns {:ok, privateKey}: 194 | - `privateKey` [%EllipticCurve.PrivateKey]: decoded private key struct; 195 | 196 | ## Example: 197 | 198 | iex> EllipticCurve.PrivateKey.fromDer(<<48, 116, 2, 1, 1, 4, 32, 59, 210, 253, 23, 93, 23, ...>>) 199 | {:ok, %EllipticCurve.PrivateKey{...}} 200 | """ 201 | def fromDer(der) do 202 | {:ok, fromDer!(der)} 203 | rescue 204 | e in RuntimeError -> {:error, e} 205 | end 206 | 207 | @doc """ 208 | Converts a private key in der format into decoded struct format 209 | 210 | Parameters: 211 | - `der` [string]: private key in der format 212 | 213 | Returns: 214 | - `privateKey` [%EllipticCurve.PrivateKey]: decoded private key struct; 215 | 216 | ## Example: 217 | 218 | iex> EllipticCurve.PrivateKey.fromDer!(<<48, 116, 2, 1, 1, 4, 32, 59, 210, 253, 23, 93, 23, ...>>) 219 | %EllipticCurve.PrivateKey{...} 220 | """ 221 | def fromDer!(der) do 222 | {bytes1, empty} = Der.removeSequence(der) 223 | 224 | if byte_size(empty) != 0 do 225 | throw("trailing junk after DER private key: #{BinaryAscii.hexFromBinary(empty)}") 226 | end 227 | 228 | {one, bytes2} = Der.removeInteger(bytes1) 229 | 230 | if one != 1 do 231 | throw("expected '1' at start of DER private key, got #{one}") 232 | end 233 | 234 | {privateKeyString, bytes3} = Der.removeOctetString(bytes2) 235 | {tag, curveOidString, _bytes4} = Der.removeConstructed(bytes3) 236 | 237 | if tag != 0 do 238 | throw("expected tag 0 in DER private key, got #{tag}") 239 | end 240 | 241 | {oidCurve, empty} = Der.removeObject(curveOidString) 242 | 243 | if byte_size(empty) != 0 do 244 | throw("trailing junk after DER private key curve_oid: #{BinaryAscii.hexFromBinary(empty)}") 245 | end 246 | 247 | privateKeyStringLength = byte_size(privateKeyString) 248 | curve = Curve.KnownCurves.getCurveByOid(oidCurve) 249 | curveLength = Curve.getLength(curve) 250 | 251 | if privateKeyStringLength < curveLength do 252 | (String.duplicate(@hexAt, curveLength - privateKeyStringLength) <> privateKeyString) 253 | |> fromString(curve) 254 | else 255 | fromString!(privateKeyString, curve.name) 256 | end 257 | end 258 | 259 | @doc false 260 | def fromString(string, curve \\ :secp256k1) do 261 | {:ok, fromString!(string, curve)} 262 | rescue 263 | e in RuntimeError -> {:error, e} 264 | end 265 | 266 | @doc false 267 | def fromString!(string, curve \\ :secp256k1) do 268 | %PrivateKey{ 269 | secret: BinaryAscii.numberFromString(string), 270 | curve: Curve.KnownCurves.getCurveByName(curve) 271 | } 272 | end 273 | end 274 | --------------------------------------------------------------------------------