signed_config 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 02a530752f0bb7d45a71a7e5b5c63a48c82ba866
4
+ data.tar.gz: 04ff0c5fccc77cefde01ff9d3e932e270d8bde02
5
+ SHA512:
6
+ metadata.gz: 2368c9dcad07fc216d00d046f5259cb9fcca43000347d5b286ad558d190b6eefcf2f4092da28daaebe52748ee9e0b723ee910655735a7d3f0f887fb5d77b2257
7
+ data.tar.gz: ba14d2d8a428d11406156136c19b4d4779a696e90c9bf1284f8e4f1a9054c6bfff45e18c6da1f2605b993efae9de52104119baca9a335929784ef2af6322f317
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2015 Kevin Trowbridge
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # SignedConfig
2
+
3
+ SignedConfig has two modes:
4
+
5
+ Writer mode : intended to be used by a sort of "licensing console", takes a config hash and a private key and generates
6
+ signed "license text." The licensed text contains a YML serialized "human readable" version of the config hash and
7
+ a matching signature.
8
+
9
+ Reader mode: intended to be used by the "licensed application" takes a public key (which should be distributed with the
10
+ application) and the "license text." It confirms the signature matches the config hash, and deserializes the license text
11
+ from YML back into a Ruby hash.
12
+
13
+ This gem was extracted from an enterprise Rails webapp. It's unusual for a Rails application in that, it's installed onsite
14
+ on clients' servers. (This is not unusual for an "enterprise application," however.)
15
+
16
+
17
+ ## Writing licenses
18
+
19
+ config_hash = {
20
+ :client_name: "Acme Inc."
21
+ :license_level: "high"
22
+ :licensed_features: ["widget_management", "foobar_creation"],
23
+ :expiration_date: 2099-01-01 00:00:00 UTC
24
+ :num_superusers: 1
25
+ :num_users: 20
26
+ }
27
+ org_name = "BRANDED"
28
+
29
+ writer = SignedConfig::Writer.new(SIGNED_CONFIG_PRIVATE_KEY, config_hash, org_name)
30
+ self.signed_config = writer.signed_config
31
+
32
+
33
+ ## Reading licenses
34
+
35
+ def verify
36
+ begin
37
+ reader = SignedConfig::Reader.new(SIGNED_CONFIG_PUBLIC_KEY, params[:license])
38
+ @config = reader.config
39
+ flash[:success] = 'License is valid.'
40
+ rescue SignedConfig::SignatureDoesNotMatch
41
+ flash[:error] = 'License is invalid.'
42
+ flash.discard
43
+ render :new
44
+ end
45
+ end
46
+
47
+
48
+
49
+ ## Example of generated license:
50
+ ```
51
+
52
+ =========================BEGIN BRANDED LICENSE=========================
53
+ ---
54
+ :config: |
55
+ ---
56
+ :client_name: Acme Inc.
57
+ :license_level: high
58
+ :licensed_features:
59
+ - widget_management
60
+ - foobar_creation
61
+ :expiration_date: 2099-01-01 00:00:00 UTC
62
+ :num_superusers: 1
63
+ :num_users: 20
64
+ :signature: |
65
+ VUiNY22N9cIs+RUJ1+w+T7pXEkBwOhGx2heeo3ekxybWjm+gEBXkt8n3Y8+s
66
+ pUyK0RfLM6NK9LMP6/Ke5IXL199hDLKXJnfy/nuZ+uIkNmoL1Bd7FhMqXCmd
67
+ HyRuuSkHTsQG4hk1VCS/mn/KaZWn1P+dyng4HWYWCAJYt2dtJqf/ZbLmIXdI
68
+ xX9GplIG5wCto7Dss7ksBR2x97QFe5P3OGQAjX0qw/jFWY8pOy+JCaJqB4b4
69
+ U/xy9p/+VWPhdmQPY8/hIowx9WC2UqbV5QKTEJzqWebG7xbvwmJfywE60+qO
70
+ rhEqh8fza8jagTbmzBtpsXyyTd4ZAusXuwc7uMTtFQ==
71
+
72
+ =========================END BRANDED LICENSE=========================
73
+ ```
74
+
75
+ ## How "secure" is this?
76
+
77
+ Since Ruby is not compiled and trivially easy to modify, it's very unsuited for being "hacker proof." Normally this doesn't
78
+ matter as, enforcing license compliance is trivial for a classic consumer webapp where users only interact with the app via
79
+ their web browsers.
80
+
81
+ However, for enterprise clients, it makes sense to just make it "slightly difficult" to modify the license to add users,
82
+ enable additional features, or extend the expiration date. The security that this gem provides raises the bar just from
83
+ the point where anyone could read and alter a plain text configuration file, to the point where any "programmer" type could
84
+ figure out modifications in an hour or two.
85
+
86
+ However, I believe this is sufficient to enforce 95% of business agreements.
87
+
88
+ Additionally SignedConfig only uses a SHA1 hashing algorithm which is not hacker-proof anymore.
89
+
90
+
91
+ ## Development
92
+
93
+ ### Running the tests
94
+
95
+ rake
@@ -0,0 +1,10 @@
1
+ require 'yaml'
2
+
3
+ require 'signed_config/crypto'
4
+ require 'signed_config/errors'
5
+ require 'signed_config/reader'
6
+ require 'signed_config/writer'
7
+
8
+ module SignedConfig
9
+ VERSION = "0.0.2"
10
+ end
@@ -0,0 +1,28 @@
1
+ # Adapted from: https://blog.atechmedia.com/encrypting-signing-data-ruby/
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+
6
+ module SignedConfig
7
+ module Crypto
8
+
9
+ class << self
10
+
11
+ # Return a signature for the string
12
+ def sign(key, string)
13
+ Base64.encode64(rsa_key(key).sign(OpenSSL::Digest::SHA1.new, string))
14
+ end
15
+
16
+ # Verify the string and signature
17
+ def verify(key, signature, string)
18
+ rsa_key(key).verify(OpenSSL::Digest::SHA1.new, Base64.decode64(signature), string)
19
+ end
20
+
21
+ private
22
+
23
+ def rsa_key(key)
24
+ OpenSSL::PKey::RSA.new(key)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module SignedConfig
2
+
3
+ class SignatureDoesNotMatch < StandardError
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ module SignedConfig
2
+ class Reader
3
+ def initialize(public_key, signed_config)
4
+ yml = signed_config.split("\n")[1..-2].join("\n")
5
+ hash = YAML.load(yml)
6
+ signature = hash[:signature]
7
+ @config_string = hash[:config]
8
+
9
+ unless Crypto.verify(public_key, signature, @config_string)
10
+ raise SignatureDoesNotMatch
11
+ end
12
+ end
13
+
14
+ def config
15
+ YAML.load(@config_string)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module SignedConfig
2
+ class Writer
3
+ def initialize(private_key, config_hash, org_name = nil)
4
+ config_string = YAML::dump(config_hash)
5
+ signature = Crypto.sign(private_key, config_string)
6
+ @org_name = org_name
7
+ @signed_config = YAML::dump({:config => config_string,
8
+ :signature => signature})
9
+ end
10
+
11
+ def signed_config
12
+ dramatic = '=' * 25
13
+ brand_txt = @org_name ? @org_name + " " : nil
14
+
15
+ "#{dramatic}BEGIN #{brand_txt}LICENSE#{dramatic}\n" +
16
+ @signed_config + "\n" +
17
+ "#{dramatic}END #{brand_txt}LICENSE#{dramatic}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpQIBAAKCAQEAvLGrsJK6IobzhJ288tFHLae6hqrRzeftQIBsJ7htIp8fmHbn
3
+ OfPdf9p+e4WJMQkM5YX8bdv/TJLmsd8qCKdZ/5FTJB4RsH7gm9ef/4edXuuH7tUH
4
+ QZuz+J87sq6UNGfpth7IsXkWcPqjmzZlpYAXL4kRZ1TtoXNt5WUMCyWRltGBnHuE
5
+ TZs4D+RBXNYLL1wb98/FBWUQUfUI2KPOnVkTcysGLhfpEEVGgsAWdwi2+Jn3KKZz
6
+ 6BInElRBrGYXIK5wPLigLebDUUGmNj8PL6LaJe7o2TO6cJXWLOSTIr1x3+giL5kX
7
+ 2w++kJcWyCmnffW/CscTX3em+qJX67q3p84vXQIDAQABAoIBAQCZMM0ofxaqbVFK
8
+ ex0pLQpSYHeoWQoX1pDg6uHjpXDEyNbH2tCCVh+fau2Arrrgmm5j8NEtB4xOyHyO
9
+ L5VajTMdrwgGrHrEBV2oZ/g3Zgw3QZSMK1rGwvfrgqret6kOmsY82uUoYBv+AR3O
10
+ Ju2C3Wj1aJw+fc6mYqX3tH+AlTGLdImO+wq7g40A1EDISHd0kaE4jTlljUiTf4Wn
11
+ 6lCSE+ie5z+s6pIYopnlwSX99X1VIClVXhD54rpngRF6ZrFgLtgIrfR4KOHfOuUa
12
+ euaa1Mnwjn/DV9KfS2q5wRytJhF+bJTf0fxBHC7gCwYXXj2meKVNC8HfvIGdOWsJ
13
+ LIIGmmkBAoGBAO1VwuMGu567IsUHkNv+GpHtiOSvQK9VrC3fT8/APS8uwmEXNcY2
14
+ eWlfjl1Eockf1jx79M8HjXfEjGfjdtFzgxwaf5v9lBGhzw0YMuVDRhXNkOsucclj
15
+ H/liNAmoAw4tSo9OUvqs3G5n0gcvZmeJRVAG/qTTXe+sJ0opcRwIeOKhAoGBAMuI
16
+ oEaO8roYcUyeqxVSJGHzMG+PpU6NwS9jrm4QnlFvm1/Z7iQHUbxv2Id/nost7ioQ
17
+ EsHo5r3S42CYgFvc4P/vWJpiM7ey/Db014fOayjDmn0ui9TwQSXQm8c4GsoQhpjz
18
+ PGALlaGRi5KXp4pWhkk+/2TSJO4QBclzxCkLwM89AoGBAOZaW/qv2UCaD4g+7Kmp
19
+ ey5x53FWbCkBtUkcusFAq+H0t3M27NmCm9rbhTkfWQv126D/CsA6a2N4oHJhAz0N
20
+ qY6IZZ0IxUNL0sO53gRJhGe7CtJJVOWLUUhiuXE54iVLblejRMTLLHP4TpDsv670
21
+ PBMNhvCBumXaqJPBn2f/DR2hAoGBAI/eSpS1dMOwhV3IhlwyzP7jvOgO3KWTM+wA
22
+ hQrN+mrL/kMZs+iJt/AAC0l0Hyh4VfIrnGau73Ncf1cHVSwaDH08vR+brLz76qYa
23
+ GcOoWDzfTvOPlE33acWGGKcQdHdCiTHSNWoapjEnxUXgpw/1K0TZIAQvOuWAM+b7
24
+ SrxMz4RtAoGANI/eHiSeMlRmRBAlpTXERqtHxcookrWkxMEpeqNsiBi0iiicfi4S
25
+ ECSu3rqmpFnwOx4rxPuXmP8+zCEdAg8CvlN/GOG5j1cAfQ5W8HhmGdlnWXhqxDvN
26
+ 2vrfCkomVTICJSn4tpIBfffwJzuIKXzJEZk23HmN5HcS0IsaCbQwGFk=
27
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,9 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvLGrsJK6IobzhJ288tFH
3
+ Lae6hqrRzeftQIBsJ7htIp8fmHbnOfPdf9p+e4WJMQkM5YX8bdv/TJLmsd8qCKdZ
4
+ /5FTJB4RsH7gm9ef/4edXuuH7tUHQZuz+J87sq6UNGfpth7IsXkWcPqjmzZlpYAX
5
+ L4kRZ1TtoXNt5WUMCyWRltGBnHuETZs4D+RBXNYLL1wb98/FBWUQUfUI2KPOnVkT
6
+ cysGLhfpEEVGgsAWdwi2+Jn3KKZz6BInElRBrGYXIK5wPLigLebDUUGmNj8PL6La
7
+ Je7o2TO6cJXWLOSTIr1x3+giL5kX2w++kJcWyCmnffW/CscTX3em+qJX67q3p84v
8
+ XQIDAQAB
9
+ -----END PUBLIC KEY-----
@@ -0,0 +1,27 @@
1
+ require 'minitest/autorun'
2
+ require 'signed_config'
3
+
4
+ class SignedConfigTest < Minitest::Test
5
+
6
+ def test_flow
7
+ private_key_path = File.expand_path("../support/test_private.key", __FILE__)
8
+ private_key = File.open(private_key_path, 'rb') { |f| f.read }
9
+ config_hash = {
10
+ :num_users => '1',
11
+ :functionality_enabled => false
12
+ }
13
+
14
+ writer = SignedConfig::Writer.new(private_key, config_hash)
15
+ signed_config = writer.signed_config
16
+
17
+ assert signed_config
18
+
19
+ public_key_path = File.expand_path("../support/test_private.key", __FILE__)
20
+ public_key = File.open(public_key_path, 'rb') { |f| f.read }
21
+ reader = SignedConfig::Reader.new(public_key, signed_config)
22
+
23
+ config = reader.config
24
+ assert config[:num_users] == '1'
25
+ assert config[:functionality_enabled] == false
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signed_config
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Trowbridge
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - kevinmtrowbridge@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - MIT_LICENSE
21
+ - README.md
22
+ - lib/signed_config.rb
23
+ - lib/signed_config/crypto.rb
24
+ - lib/signed_config/errors.rb
25
+ - lib/signed_config/reader.rb
26
+ - lib/signed_config/writer.rb
27
+ - test/support/test_private.key
28
+ - test/support/test_public.key
29
+ - test/test_signed_config.rb
30
+ homepage: https://github.com/kevinmtrowbridge/signed_config
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.5.0
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Cryptograpically sign a configuration hash. A way to enforce configuration
54
+ / licensing of an installed Ruby application.
55
+ test_files:
56
+ - test/support/test_private.key
57
+ - test/support/test_public.key
58
+ - test/test_signed_config.rb