summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormh <mh@immerda.ch>2016-10-21 17:54:08 +0200
committermh <mh@immerda.ch>2016-10-21 18:10:19 +0200
commit826fd077aca94acf6a8d41d643b8f06ed7fb7090 (patch)
tree0b1419e7288b1707eec11af65a900c1675f3acb4
parent0329bdf2c0b2c157cf80c15f7dde580fbd243e3c (diff)
add functions to support dealing with keys for onion addresses
-rw-r--r--README17
-rw-r--r--lib/puppet/parser/functions/generate_onion_key.rb40
-rw-r--r--lib/puppet/parser/functions/onion_address.rb28
-rw-r--r--spec/functions/generate_onion_key_spec.rb73
-rw-r--r--spec/functions/onion_address_spec.rb48
5 files changed, 205 insertions, 1 deletions
diff --git a/README b/README
index 188acca..bf2968d 100644
--- a/README
+++ b/README
@@ -40,7 +40,8 @@ Dependencies
This module needs:
-- the concat module: git://labs.riseup.net/shared-concat
+- the concat module: https://gitlab.com/shared-puppet-modules-group/concat
+- the apt module https://gitlab.com/shared-puppet-modules-group/apt or https://github.com/puppetlabs/puppetlabs-apt/
Usage
=====
@@ -212,3 +213,17 @@ If you are using munin, and have the puppet munin module installed, you can set
the use_munin parameter to true when defining the tor::daemon class to have
graphs setup for you.
+Functions
+=========
+
+This module comes with 2 functions specific to tor support. They require the base32 gem to be installed on the master or wherever they are executed.
+
+onion_address
+-------------
+
+This function takes a 1024bit RSA private key as an argument and returns the onion address for a hidden service for that key.
+
+generate_onion_key
+------------------
+
+This function takes a path (on the puppetmaster!) and an identifier for a key and returns an array containing the matching onion address and the private key. The private key either exists under the supplied `path/key_identifier` or is being generated on the fly and stored under that path for the next execution.
diff --git a/lib/puppet/parser/functions/generate_onion_key.rb b/lib/puppet/parser/functions/generate_onion_key.rb
new file mode 100644
index 0000000..2964268
--- /dev/null
+++ b/lib/puppet/parser/functions/generate_onion_key.rb
@@ -0,0 +1,40 @@
+module Puppet::Parser::Functions
+ newfunction(:generate_onion_key, :type => :rvalue, :doc => <<-EOS
+Generates or loads a rsa private key for an onion service, returns they onion
+onion address and the private key content.
+
+Requires a location to load and store the private key, as well an identifier, which will be used as a filename in the location.
+
+Example:
+
+ res = generate_onion_key('/tmp','my_secrect_key')
+ notice "Onion Address: \${res[0]"
+ notice "Priavte Key: \${res[1]"
+
+
+If /tmp/my_secrect_key.key exists, it will be loaded and the onion address will be generated from it.
+
+ EOS
+ ) do |args|
+ location = args.shift
+ identifier = args.shift
+
+ raise(Puppet::ParseError, "generate_onion_key(): requires 2 arguments") unless [location,identifier].all?{|i| !i.nil? }
+
+ raise(Puppet::ParseError, "generate_onion_key(): requires location (#{location}) to be a directory") unless File.directory?(location)
+ path = File.join(location,identifier)
+
+ private_key = if File.exists?(path)
+ pk = OpenSSL::PKey::RSA.new(File.read(path))
+ raise(Puppet::ParseError, "generate_onion_key(): key in path #{path} must have a length of 1024bit") unless (pk.n.num_bytes * 8) == 1024
+ pk
+ else
+ # 1024 is hardcoded by tor
+ pk = OpenSSL::PKey::RSA.generate(1024)
+ File.open(path,'w'){|f| f << pk.to_s }
+ pk
+ end
+
+ [ function_onion_address([private_key]), private_key.to_s ]
+ end
+end
diff --git a/lib/puppet/parser/functions/onion_address.rb b/lib/puppet/parser/functions/onion_address.rb
new file mode 100644
index 0000000..a3db2f4
--- /dev/null
+++ b/lib/puppet/parser/functions/onion_address.rb
@@ -0,0 +1,28 @@
+require 'base32'
+module Puppet::Parser::Functions
+ newfunction(:onion_address, :type => :rvalue, :doc => <<-EOS
+Generates an onion address from a 1024-bit RSA private key.
+
+Example:
+
+ onion_address("-----BEGIN RSA PRIVATE KEY-----
+MII....
+-----END RSA PRIVATE KEY-----")
+
+Returns the onionadress for that key, *without* the .onion suffix.
+ EOS
+ ) do |args|
+ key = args.shift
+ raise(Puppet::ParseError, "onion_address(): requires 1 argument") unless key && args.empty?
+ private_key = key.is_a?(OpenSSL::PKey::RSA) ? key : OpenSSL::PKey::RSA.new(key)
+
+ # the onion address are a base32 encoded string of the first half of the sha1 over the
+ # der format of the public key
+ # https://trac.torproject.org/projects/tor/wiki/doc/HiddenServiceNames#Howare.onionnamescreated
+ # We can skip the first 22 bits of the der format as they are ignored by tor
+ # https://timtaubert.de/blog/2014/11/using-the-webcrypto-api-to-generate-onion-names-for-tor-hidden-services/
+ # https://gitweb.torproject.org/torspec.git/tree/rend-spec.txt#n525
+ public_key_der = private_key.public_key.to_der
+ Base32.encode(Digest::SHA1.digest(public_key_der[22..-1]))[0..15].downcase
+ end
+end
diff --git a/spec/functions/generate_onion_key_spec.rb b/spec/functions/generate_onion_key_spec.rb
new file mode 100644
index 0000000..07a9f91
--- /dev/null
+++ b/spec/functions/generate_onion_key_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+require 'fileutils'
+
+describe 'generate_onion_key' do
+ before(:all) do
+ @tmp_path = File.expand_path(File.join(File.dirname(__FILE__),'..','fixtures','tmp'))
+ @test_path = File.join(@tmp_path,'test')
+ @drpsyff5srkctr7h_str = "-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC9OUBOkL73n43ogC/Jma54/ZZDEpoisqpkGJHgbcRGJIxcqqfL
+PbnT3hD5SUCVXxLnzWDCTwTe2VOzIUlBXmslwVXnCJh/XGZg9NHiNU3EAZTwu1g9
+8gNmmG1bymaoEBkuC1osijOj+CN+gzLzApiMbDxddpxTn70LWaSqMDbfdQIDAQAB
+An88nBn9EGAa8QCDeIvWB2PbXV7EHTFB6/ioFzairIYx8YMEK6WTdDIRqw/EybHm
+Jo3nseFMXAMzXmlw9zh/t76ZzE7ooYocSPIEzpu4gDRsa5/mqRCGajs8A8ooiHN5
+Tc9cHzIfhjOYhu3VxF0G9LTAC8nKdWQkHm+h+J6A6+wBAkEA2E6GcIdPGTSfaNRS
+BHOpKUUSvH7W0e5fyYe221EhESdTFjVkaO5YN9HvcqYh27nik0azKgNj6PiE01FC
+0q4fgQJBAN/ycGS3dX5WRXEOpbQ04LKyxCFMVgS+tN5ueDgbv/SxWAxidLYcVfbg
+CcUA+L2OaQ95S97CxYlCLda10vIPOfUCQQCUvQJzFIgOlAHdqsovJ3011Lp6hVmg
+h6K0SK8zhkkPq5PVnKdMBEEDOUfG9XgoyFyF20LN7ADirSlgyesCRhuBAkEAmuCE
+MmNecn0fkUzb9IENVQik85JjeuyZEau8oLEwU/3CMu50YO2/1fijSQee/xlaN0Vf
+3zM8geyu3urodFdrcQJBAMBcecMvo4ddZ/GnwpKJuXEhKSwQfPOeb8lK12NvKuVE
+znq+qT/KbJlwy/27X/auCAzD5rJ9VVzyWiu8nnwICS8=
+-----END RSA PRIVATE KEY-----\n"
+ end
+ describe 'signature validation' do
+ it { is_expected.not_to eq(nil) }
+ it { is_expected.to run.with_params().and_raise_error(Puppet::ParseError, /requires 2 arguments/) }
+ it { is_expected.to run.with_params(1).and_raise_error(Puppet::ParseError, /requires 2 arguments/) }
+ it { is_expected.to run.with_params('/etc/passwd','test').and_raise_error(Puppet::ParseError, /requires location \(\/etc\/passwd\) to be a directory/) }
+ describe 'with a key bigger than 1024' do
+ before(:each) do
+ FileUtils.mkdir_p(@tmp_path) unless File.directory?(@tmp_path)
+ File.open(@test_path,'w'){|f| f << OpenSSL::PKey::RSA.generate(2048) }
+ end
+ it { is_expected.to run.with_params(@tmp_path,'test').and_raise_error(Puppet::ParseError, /must have a length of 1024bit/) }
+ end
+ end
+
+ describe 'normal operation' do
+ before(:all) do
+ FileUtils.rm_rf(@tmp_path) if File.exists?(@tmp_path)
+ FileUtils.mkdir_p(@tmp_path)
+ end
+ after(:all) do
+ FileUtils.rm_rf(@tmp_path) if File.exists?(@tmp_path)
+ end
+ let(:return_value) {
+ scope.function_generate_onion_key([@tmp_path,'test'])
+ }
+ context 'without an existing key' do
+ it 'returns an onion address and a key ' do
+ expect(return_value.size).to be(2)
+ end
+ it 'creates and stores the key' do
+ expect(return_value.last).to be_eql(File.read(File.join(@tmp_path,'test')))
+ end
+ it 'returns a proper onion address' do
+ expect(return_value.first).to be_eql(scope.function_onion_address([File.read(File.join(@tmp_path,'test'))]))
+ end
+ it 'does not recreate a key once created' do
+ expect(scope.function_generate_onion_key([@tmp_path,'test'])).to be_eql(scope.function_generate_onion_key([@tmp_path,'test']))
+ end
+ it 'creates to different keys for different names' do
+ expect(scope.function_generate_onion_key([@tmp_path,'test']).first).to_not be_eql(scope.function_generate_onion_key([@tmp_path,'test2']))
+ end
+ end
+ context 'with an existing key' do
+ before(:all) do
+ File.open(@test_path,'w'){|f| f << @drpsyff5srkctr7h_str }
+ end
+ it { is_expected.to run.with_params(@tmp_path,'test').and_return(['drpsyff5srkctr7h',@drpsyff5srkctr7h_str]) }
+ end
+ end
+end
diff --git a/spec/functions/onion_address_spec.rb b/spec/functions/onion_address_spec.rb
new file mode 100644
index 0000000..942d022
--- /dev/null
+++ b/spec/functions/onion_address_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe 'onion_address' do
+ describe 'signature validation' do
+ it { is_expected.not_to eq(nil) }
+ it { is_expected.to run.with_params().and_raise_error(Puppet::ParseError, /requires 1 argument/) }
+ it { is_expected.to run.with_params(1,2).and_raise_error(Puppet::ParseError, /requires 1 argument/) }
+ end
+
+ describe 'normal operation' do
+ it { is_expected.to run.with_params(
+"-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC9OUBOkL73n43ogC/Jma54/ZZDEpoisqpkGJHgbcRGJIxcqqfL
+PbnT3hD5SUCVXxLnzWDCTwTe2VOzIUlBXmslwVXnCJh/XGZg9NHiNU3EAZTwu1g9
+8gNmmG1bymaoEBkuC1osijOj+CN+gzLzApiMbDxddpxTn70LWaSqMDbfdQIDAQAB
+An88nBn9EGAa8QCDeIvWB2PbXV7EHTFB6/ioFzairIYx8YMEK6WTdDIRqw/EybHm
+Jo3nseFMXAMzXmlw9zh/t76ZzE7ooYocSPIEzpu4gDRsa5/mqRCGajs8A8ooiHN5
+Tc9cHzIfhjOYhu3VxF0G9LTAC8nKdWQkHm+h+J6A6+wBAkEA2E6GcIdPGTSfaNRS
+BHOpKUUSvH7W0e5fyYe221EhESdTFjVkaO5YN9HvcqYh27nik0azKgNj6PiE01FC
+0q4fgQJBAN/ycGS3dX5WRXEOpbQ04LKyxCFMVgS+tN5ueDgbv/SxWAxidLYcVfbg
+CcUA+L2OaQ95S97CxYlCLda10vIPOfUCQQCUvQJzFIgOlAHdqsovJ3011Lp6hVmg
+h6K0SK8zhkkPq5PVnKdMBEEDOUfG9XgoyFyF20LN7ADirSlgyesCRhuBAkEAmuCE
+MmNecn0fkUzb9IENVQik85JjeuyZEau8oLEwU/3CMu50YO2/1fijSQee/xlaN0Vf
+3zM8geyu3urodFdrcQJBAMBcecMvo4ddZ/GnwpKJuXEhKSwQfPOeb8lK12NvKuVE
+znq+qT/KbJlwy/27X/auCAzD5rJ9VVzyWiu8nnwICS8=
+-----END RSA PRIVATE KEY-----"
+ ).and_return("drpsyff5srkctr7h")}
+ end
+ describe 'by getting an RSA key' do
+ it { is_expected.to run.with_params(OpenSSL::PKey::RSA.new(
+"-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDbvYjbtJB9vTnEygyq4Bzp0xxtTl3ZYKC6JbxgRzP8uLv1HoxX
+20EmQUZ/LNBXHebc6frlObhtpKULFuBzAy5LpdKI9CUErkl3D3AigFgP3XP/PtdP
+m11TuxdBoKL6Jbo54NpUVOGQ5SJJaNEOfhmgMSCtlyyI9DBni3PLO2P0sQIDAQAB
+AoGAPTlt7Gk+6QnUErSJGwMeize67+mp1GtL3RGujtTH8141YHKGf+QjHtmJHt4J
+nnxCWsMGmN+gN0xsf8578w+r0fvDjZ3e5lVUpR/8ds90a654Lr/pgqLc3H1EZ9Pr
+GDFjPdaMtdTSX5hSAB2EDLfDUU19bdFRK+k71mglrMLpdQECQQDmJt3mmX67kAzH
+w2I/BEbmOlonmn3c98VyawoNrk0fKAluoYWHxxk9SuCu2ZDQyyPKPQuZbgdPnUNp
+kV3PuQ6ZAkEA9GtTjMfceX8ArLTmOMIMVP2t8yzbcK2uqukMG79JiPZbYKIstjho
+XUpO/jZhTb9p8M4NV/09z091gMTOF6Fd2QJBAM1I7bS6ROhX3I5yIDfFQNgqRC//
+BTULa/par2T0i6W2uHMNb2VkmYaqOy66sQkLqKjDOo1oLu08gNyw5NRbZEECQQCr
+FDR25a28nNisCjLap3haRPXssAko5WjM2DJReaLO6yEqklkZcoIaSljgNtAEy2Yr
+1w4f+HG7GbL1XsuiXqCBAkAeYljaIVhqGOOez0ORaCm0FCLoTJ6/fn7009os/qgr
+n2xsVGUNm+E0pvAMT0LIx2KvpLxe2Y0Xx497/vyM6e7G
+-----END RSA PRIVATE KEY-----")
+ ).and_return("d3ep6pcs4to4hbwo") }
+ end
+end