Closed korneel closed 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.
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
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
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.
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.
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.
I've implemented the getInfo()
call and in order to do that refactored the method _build_signature_message()
. See PR #8
getDomainNames() works fine. I tried implementing getInfo(domainName) but an error is returned:
What could be the problem?