benkonrath / transip-api

Python implementation for the TransIP API
https://transip-api.readthedocs.io/en/latest/
MIT License
23 stars 23 forks source link

Implement DomainService.getInfo(domainName) #4

Closed korneel closed 10 years ago

korneel commented 10 years ago

getDomainNames() works fine. I tried implementing getInfo(domainName) but an error is returned:

suds.WebFault: Server raised fault: 'Invalid API signature, signature does not match the request. (timestamp: 0.78335000 1392279517)'
getInfo(xs:string domainName, )
def get_info(self):
    cookie = self.build_cookie(mode=MODE_RO, method='getInfo')
    self.update_cookie(cookie)
    return self.soap_client.service.getInfo('test.com')

What could be the problem?

mhogerheijde commented 10 years ago

Hi Korneel,

I haven't looked into the other methods yet, but I think the cookie signature is different for the getInfo() then for the getDomainNames(). The parameter domainName should probably also be signed, otherwise a man-in-the-middle could easily change the domainName parameter and get info from a different domain.

Unfortunately TransIP doesn't have any documentation about how to build the signature, other than their example implementation in PHP (https://www.transip.eu/transip/api/). So I'll have to sort out the way they sign in PHP and port that to python.

korneel commented 10 years ago

PHP: https://github.com/linkorb/transip-php Ruby: https://github.com/joost/transip

This is how the signature is built in ruby: https://github.com/joost/transip/blob/1be466556cf3665919b33852e61c67880c756e1a/lib/transip.rb

# yes, i know, it smells bad
  def convert_array_to_hash(array)
    result = {}
    array.each_with_index do |value, index|
      result[index] = value
    end
    result
  end

  def urlencode(input)
    output = URI.encode_www_form_component(input)
    output.gsub!('+', '%20')
    output.gsub!('%7E', '~')
    output
  end

  def serialize_parameters(parameters, key_prefix=nil)
    parameters = parameters.to_hash.values.first if parameters.is_a? TransipStruct
    parameters = convert_array_to_hash(parameters) if parameters.is_a? Array
    if not parameters.is_a? Hash
      return urlencode(parameters)
    end

    encoded_parameters = []
    parameters.each do |key, value|
      next if key.to_s == '@xsi:type'
      encoded_key = (key_prefix.nil?) ? urlencode(key) : "#{key_prefix}[#{urlencode(key)}]"
      if value.is_a? Hash or value.is_a? Array or value.is_a? TransipStruct
        encoded_parameters << serialize_parameters(value, encoded_key)
      else
        encoded_value = urlencode(value)
        encoded_parameters << "#{encoded_key}=#{encoded_value}"
      end
    end

    encoded_parameters = encoded_parameters.join("&")
    #puts encoded_parameters.split('&').join("\n")
    encoded_parameters
  end

  # does all the techy stuff to calculate transip's sick authentication scheme:
  # a hash with all the request information is subsequently:
  # serialized like a www form
  # SHA512 digested
  # asn1 header added
  # private key encrypted
  # Base64 encoded
  # URL encoded
  # I think the guys at transip were trying to use their entire crypto-toolbox!
  def signature(method, parameters, time, nonce)
    formatted_method = method.to_s.lower_camelcase
    parameters ||= {}
    input = convert_array_to_hash(parameters.values)
    options = {
      '__method' => formatted_method,
      '__service' => SERVICE,
      '__hostname' => @endpoint,
      '__timestamp' => time,
      '__nonce' => nonce

    }
    input.merge!(options)
    raise "Invalid RSA key" unless @key =~ /-----BEGIN (RSA )?PRIVATE KEY-----(.*)-----END (RSA )?PRIVATE KEY-----/sim
    serialized_input = serialize_parameters(input)

    digest = Digest::SHA512.new.digest(serialized_input)
    asn_header = "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40"

    # convert asn_header literal to ASCII-8BIT
    if RUBY_VERSION.split('.')[0] == "2"
        asn = asn_header.b + digest
    else
        asn = asn_header + digest
    end
    private_key = OpenSSL::PKey::RSA.new(@key)
    encrypted_asn = private_key.private_encrypt(asn)
    readable_encrypted_asn = Base64.encode64(encrypted_asn)
    urlencode(readable_encrypted_asn)
  end

  def to_cookies(content)
    content.map do |item|
      HTTPI::Cookie.new item
    end
  end

  # Used for authentication
  def cookies(method, parameters)
    time = Time.new.to_i
    #strip out the -'s because transip requires the nonce to be between 6 and 32 chars
    nonce = SecureRandom.uuid.gsub("-", '')
    result = to_cookies [ "login=#{self.username}",
                 "mode=#{self.mode}",
                 "timestamp=#{time}",
                 "nonce=#{nonce}",
                 "clientVersion=#{API_VERSION}",
                 "signature=#{signature(method, parameters, time, nonce)}"

               ]
    #puts signature(method, parameters, time, nonce)
    result
  end
mhogerheijde commented 10 years ago

Bit of a crosspost, you were first with the ruby example. Below what I found out in the meanwhile.


What they seem to do is this:

self::_sign(
    array_merge(
    $parameters, 
        array(
            '__service'   => self::SERVICE,
            '__hostname'  => $endpoint,
            '__timestamp' => $timestamp,
            '__nonce'     => $nonce
        )
    )
)

And the parameters are passed along like this

public static function getInfo($domainName) {
    $parameters = array_merge(array($domainName), array('__method' => 'getInfo'))

This seems very odd to me; in python terms this would mean mixing a list with a dict. I

I've ran this:

<?php
// array.php
$domainName = "example.com";
$parameters = array_merge(array($domainName), array('__method' => 'getInfo'));
var_dump($parameters);
?>
$ php array.php
array(2) {
  [0]=>
  string(11) "example.com"
  ["__method"]=>
  string(7) "getInfo"
}

I've edited the PHP to print out the parameters array and the string that eventually gets signed:

Array
(
    [0] => sundayafternoon.nl
    [__method] => getInfo
    [__service] => DomainService
    [__hostname] => api.transip.nl
    [__timestamp] => 1392282075
    [__nonce] => 52fc89db1c8dd5.17807310
)

This results in this string to be signed:

0=sundayafternoon.nl&__method=getInfo&__service=DomainService&__hostname=api.transip.nl&__timestamp=1392282075&__nonce=52fc89db1c8dd5.178073100=sundayafternoon.nl&__method=getInfo&__service=DomainService&__hostname=api.transip.nl&__timestamp=1392282075&__nonce=52fc89db1c8dd5.17807310
mhogerheijde commented 10 years ago

The strange thing is, there seems to be no specified order in which the extra parameters should be.

There also is a notion of nested dicts

{ "foo" : { "bar": "baz", "qux": "quux" }, "lorem" : "ipsum" }

This seems to be serialised as

foo[bar]=baz&foo[qux]=quux&lorem=ipsum

But the DomainService does not use nested dicts (or arrays in PHP-terms) anywhere.

korneel commented 10 years ago

When I alter _build_signature_message in your code like this:

    def _build_signature_message(self, service_name, method_name,
            timestamp, nonce):
        """
        Builds the message that sould be signed. This message contains
        specific information about the request in a specific order.
        """
        sign = OrderedDict()
        sign[0] = 'test.com'
        sign['__method'] = method_name
        sign['__service'] = service_name
        sign['__hostname'] = self.endpoint
        sign['__timestamp'] = timestamp
        sign['__nonce'] = nonce

        return urllib.urlencode(sign)

then get_info in my code works.

mhogerheijde commented 10 years ago

Sweet!

Then 'all' we need to do is accept an optional list of arguments. I'd propose to take the shortest route first: implement it for a flat list, no nesting. When code gets added that needs nested arguments we'll add it then.

mhogerheijde commented 10 years ago

I've implemented the getInfo() call and in order to do that refactored the method _build_signature_message(). See PR #8