diff --git a/lib/puppet/functions/tor/generate_onion_key.rb b/lib/puppet/functions/tor/generate_onion_key.rb new file mode 100644 index 0000000..ac17eec --- /dev/null +++ b/lib/puppet/functions/tor/generate_onion_key.rb @@ -0,0 +1,76 @@ +# This is an autogenerated function, ported from the original legacy version. +# It /should work/ as is, but will not have all the benefits of the modern +# function API. You should see the function docs to learn how to add function +# signatures for type safety and to document this function using puppet-strings. +# +# https://puppet.com/docs/puppet/latest/custom_functions_ruby.html +# +# ---- original file header ---- + +# ---- original file header ---- +# +# @summary +# 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_secret_key') +# notice "Onion Address: ${res[0]" +# notice "Priavte Key: ${res[1]" +# +# +#It will also store the onion address under /tmp/my_secret_key.hostname. +#If /tmp/my_secret_key.key exists, but not the hostname file. Then the function will be loaded and the onion address will be generated from it. +# +# +# +Puppet::Functions.create_function(:'tor::generate_onion_key') do + # @param args + # The original array of arguments. Port this to individually managed params + # to get the full benefit of the modern function API. + # + # @return [Data type] + # Describe what the function returns here + # + dispatch :default_impl do + # Call the method named 'default_impl' when this is matched + # Port this to match individual params for better type safety + repeated_param 'Any', :args + end + + + def default_impl(*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?(kf="#{path}.key") + pk = OpenSSL::PKey::RSA.new(File.read(kf)) + raise(Puppet::ParseError, "generate_onion_key(): key in path #{kf} 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(kf,'w'){|f| f << pk.to_s } + pk + end + onion_address = if File.exists?(hf="#{path}.hostname") + File.read(hf) + else + oa = function_onion_address([private_key]) + File.open(hf,'w'){|f| f << oa.to_s } + oa + end + + [ onion_address, private_key.to_s ] + + end +end diff --git a/lib/puppet/functions/tor/onion_address.rb b/lib/puppet/functions/tor/onion_address.rb new file mode 100644 index 0000000..6c1c092 --- /dev/null +++ b/lib/puppet/functions/tor/onion_address.rb @@ -0,0 +1,57 @@ +# This is an autogenerated function, ported from the original legacy version. +# It /should work/ as is, but will not have all the benefits of the modern +# function API. You should see the function docs to learn how to add function +# signatures for type safety and to document this function using puppet-strings. +# +# https://puppet.com/docs/puppet/latest/custom_functions_ruby.html +# +# ---- original file header ---- +require 'base32' +# ---- original file header ---- +# +# @summary +# 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. +# +# +Puppet::Functions.create_function(:'tor::onion_address') do + # @param args + # The original array of arguments. Port this to individually managed params + # to get the full benefit of the modern function API. + # + # @return [Data type] + # Describe what the function returns here + # + dispatch :default_impl do + # Call the method named 'default_impl' when this is matched + # Port this to match individual params for better type safety + repeated_param 'Any', :args + end + + + def default_impl(*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 + # Except for Ruby 1.8.7 where the first 22 are not present at all + start = RUBY_VERSION.to_f < 1.9 ? 0 : 22 + public_key_der = private_key.public_key.to_der + Base32.encode(Digest::SHA1.digest(public_key_der[start..-1]))[0..15].downcase + + end +end diff --git a/spec/functions/tor_generate_onion_key_spec.rb b/spec/functions/tor_generate_onion_key_spec.rb new file mode 100644 index 0000000..b0dbe50 --- /dev/null +++ b/spec/functions/tor_generate_onion_key_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'tor::generate_onion_key' do + # without knowing details about the implementation, this is the only test + # case that we can autogenerate. You should add more examples below! + it { is_expected.not_to eq(nil) } + +################################# +# Below are some example test cases. You may uncomment and modify them to match +# your needs. Notice that they all expect the base error class of `StandardError`. +# This is because the autogenerated function uses an untyped array for parameters +# and relies on your implementation to do the validation. As you convert your +# function to proper dispatches and typed signatures, you should change the +# expected error of the argument validation examples to `ArgumentError`. +# +# Other error types you might encounter include +# +# * StandardError +# * ArgumentError +# * Puppet::ParseError +# +# Read more about writing function unit tests at https://rspec-puppet.com/documentation/functions/ +# +# it 'raises an error if called with no argument' do +# is_expected.to run.with_params.and_raise_error(StandardError) +# end +# +# it 'raises an error if there is more than 1 arguments' do +# is_expected.to run.with_params({ 'foo' => 1 }, 'bar' => 2).and_raise_error(StandardError) +# end +# +# it 'raises an error if argument is not the proper type' do +# is_expected.to run.with_params('foo').and_raise_error(StandardError) +# end +# +# it 'returns the proper output' do +# is_expected.to run.with_params(123).and_return('the expected output') +# end +################################# + +end diff --git a/spec/functions/tor_onion_address_spec.rb b/spec/functions/tor_onion_address_spec.rb new file mode 100644 index 0000000..cfdda76 --- /dev/null +++ b/spec/functions/tor_onion_address_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'tor::onion_address' do + # without knowing details about the implementation, this is the only test + # case that we can autogenerate. You should add more examples below! + it { is_expected.not_to eq(nil) } + +################################# +# Below are some example test cases. You may uncomment and modify them to match +# your needs. Notice that they all expect the base error class of `StandardError`. +# This is because the autogenerated function uses an untyped array for parameters +# and relies on your implementation to do the validation. As you convert your +# function to proper dispatches and typed signatures, you should change the +# expected error of the argument validation examples to `ArgumentError`. +# +# Other error types you might encounter include +# +# * StandardError +# * ArgumentError +# * Puppet::ParseError +# +# Read more about writing function unit tests at https://rspec-puppet.com/documentation/functions/ +# +# it 'raises an error if called with no argument' do +# is_expected.to run.with_params.and_raise_error(StandardError) +# end +# +# it 'raises an error if there is more than 1 arguments' do +# is_expected.to run.with_params({ 'foo' => 1 }, 'bar' => 2).and_raise_error(StandardError) +# end +# +# it 'raises an error if argument is not the proper type' do +# is_expected.to run.with_params('foo').and_raise_error(StandardError) +# end +# +# it 'returns the proper output' do +# is_expected.to run.with_params(123).and_return('the expected output') +# end +################################# + +end