250 lines
7.3 KiB
Ruby
250 lines
7.3 KiB
Ruby
require "openid/kvform"
|
|
require "openid/util"
|
|
require "openid/cryptutil"
|
|
require "openid/message"
|
|
|
|
module OpenID
|
|
|
|
def self.get_secret_size(assoc_type)
|
|
if assoc_type == 'HMAC-SHA1'
|
|
return 20
|
|
elsif assoc_type == 'HMAC-SHA256'
|
|
return 32
|
|
else
|
|
raise ArgumentError("Unsupported association type: #{assoc_type}")
|
|
end
|
|
end
|
|
|
|
# An Association holds the shared secret between a relying party and
|
|
# an OpenID provider.
|
|
class Association
|
|
attr_reader :handle, :secret, :issued, :lifetime, :assoc_type
|
|
|
|
FIELD_ORDER =
|
|
[:version, :handle, :secret, :issued, :lifetime, :assoc_type,]
|
|
|
|
# Load a serialized Association
|
|
def self.deserialize(serialized)
|
|
parsed = Util.kv_to_seq(serialized)
|
|
parsed_fields = parsed.map{|k, v| k.to_sym}
|
|
if parsed_fields != FIELD_ORDER
|
|
raise ProtocolError, 'Unexpected fields in serialized association'\
|
|
" (Expected #{FIELD_ORDER.inspect}, got #{parsed_fields.inspect})"
|
|
end
|
|
version, handle, secret64, issued_s, lifetime_s, assoc_type =
|
|
parsed.map {|field, value| value}
|
|
if version != '2'
|
|
raise ProtocolError, "Attempted to deserialize unsupported version "\
|
|
"(#{parsed[0][1].inspect})"
|
|
end
|
|
|
|
self.new(handle,
|
|
Util.from_base64(secret64),
|
|
Time.at(issued_s.to_i),
|
|
lifetime_s.to_i,
|
|
assoc_type)
|
|
end
|
|
|
|
# Create an Association with an issued time of now
|
|
def self.from_expires_in(expires_in, handle, secret, assoc_type)
|
|
issued = Time.now
|
|
self.new(handle, secret, issued, expires_in, assoc_type)
|
|
end
|
|
|
|
def initialize(handle, secret, issued, lifetime, assoc_type)
|
|
@handle = handle
|
|
@secret = secret
|
|
@issued = issued
|
|
@lifetime = lifetime
|
|
@assoc_type = assoc_type
|
|
end
|
|
|
|
# Serialize the association to a form that's consistent across
|
|
# JanRain OpenID libraries.
|
|
def serialize
|
|
data = {
|
|
:version => '2',
|
|
:handle => handle,
|
|
:secret => Util.to_base64(secret),
|
|
:issued => issued.to_i.to_s,
|
|
:lifetime => lifetime.to_i.to_s,
|
|
:assoc_type => assoc_type,
|
|
}
|
|
|
|
Util.assert(data.length == FIELD_ORDER.length)
|
|
|
|
pairs = FIELD_ORDER.map{|field| [field.to_s, data[field]]}
|
|
return Util.seq_to_kv(pairs, strict=true)
|
|
end
|
|
|
|
# The number of seconds until this association expires
|
|
def expires_in(now=nil)
|
|
if now.nil?
|
|
now = Time.now.to_i
|
|
else
|
|
now = now.to_i
|
|
end
|
|
time_diff = (issued.to_i + lifetime) - now
|
|
if time_diff < 0
|
|
return 0
|
|
else
|
|
return time_diff
|
|
end
|
|
end
|
|
|
|
# Generate a signature for a sequence of [key, value] pairs
|
|
def sign(pairs)
|
|
kv = Util.seq_to_kv(pairs)
|
|
case assoc_type
|
|
when 'HMAC-SHA1'
|
|
CryptUtil.hmac_sha1(@secret, kv)
|
|
when 'HMAC-SHA256'
|
|
CryptUtil.hmac_sha256(@secret, kv)
|
|
else
|
|
raise ProtocolError, "Association has unknown type: "\
|
|
"#{assoc_type.inspect}"
|
|
end
|
|
end
|
|
|
|
# Generate the list of pairs that form the signed elements of the
|
|
# given message
|
|
def make_pairs(message)
|
|
signed = message.get_arg(OPENID_NS, 'signed')
|
|
if signed.nil?
|
|
raise ProtocolError, 'Missing signed list'
|
|
end
|
|
signed_fields = signed.split(',', -1)
|
|
data = message.to_post_args
|
|
signed_fields.map {|field| [field, data.fetch('openid.'+field,'')] }
|
|
end
|
|
|
|
# Return whether the message's signature passes
|
|
def check_message_signature(message)
|
|
message_sig = message.get_arg(OPENID_NS, 'sig')
|
|
if message_sig.nil?
|
|
raise ProtocolError, "#{message} has no sig."
|
|
end
|
|
calculated_sig = get_message_signature(message)
|
|
return calculated_sig == message_sig
|
|
end
|
|
|
|
# Get the signature for this message
|
|
def get_message_signature(message)
|
|
Util.to_base64(sign(make_pairs(message)))
|
|
end
|
|
|
|
def ==(other)
|
|
(other.class == self.class and
|
|
other.handle == self.handle and
|
|
other.secret == self.secret and
|
|
|
|
# The internals of the time objects seemed to differ
|
|
# in an opaque way when serializing/unserializing.
|
|
# I don't think this will be a problem.
|
|
other.issued.to_i == self.issued.to_i and
|
|
|
|
other.lifetime == self.lifetime and
|
|
other.assoc_type == self.assoc_type)
|
|
end
|
|
|
|
# Add a signature (and a signed list) to a message.
|
|
def sign_message(message)
|
|
if (message.has_key?(OPENID_NS, 'sig') or
|
|
message.has_key?(OPENID_NS, 'signed'))
|
|
raise ArgumentError, 'Message already has signed list or signature'
|
|
end
|
|
|
|
extant_handle = message.get_arg(OPENID_NS, 'assoc_handle')
|
|
if extant_handle and extant_handle != self.handle
|
|
raise ArgumentError, "Message has a different association handle"
|
|
end
|
|
|
|
signed_message = message.copy()
|
|
signed_message.set_arg(OPENID_NS, 'assoc_handle', self.handle)
|
|
message_keys = signed_message.to_post_args.keys()
|
|
|
|
signed_list = []
|
|
message_keys.each { |k|
|
|
if k.starts_with?('openid.')
|
|
signed_list << k[7..-1]
|
|
end
|
|
}
|
|
|
|
signed_list << 'signed'
|
|
signed_list.sort!
|
|
|
|
signed_message.set_arg(OPENID_NS, 'signed', signed_list.join(','))
|
|
sig = get_message_signature(signed_message)
|
|
signed_message.set_arg(OPENID_NS, 'sig', sig)
|
|
return signed_message
|
|
end
|
|
end
|
|
|
|
class AssociationNegotiator
|
|
attr_reader :allowed_types
|
|
|
|
def self.get_session_types(assoc_type)
|
|
case assoc_type
|
|
when 'HMAC-SHA1'
|
|
['DH-SHA1', 'no-encryption']
|
|
when 'HMAC-SHA256'
|
|
['DH-SHA256', 'no-encryption']
|
|
else
|
|
raise ProtocolError, "Unknown association type #{assoc_type.inspect}"
|
|
end
|
|
end
|
|
|
|
def self.check_session_type(assoc_type, session_type)
|
|
if !get_session_types(assoc_type).include?(session_type)
|
|
raise ProtocolError, "Session type #{session_type.inspect} not "\
|
|
"valid for association type #{assoc_type.inspect}"
|
|
end
|
|
end
|
|
|
|
def initialize(allowed_types)
|
|
self.allowed_types=(allowed_types)
|
|
end
|
|
|
|
def copy
|
|
Marshal.load(Marshal.dump(self))
|
|
end
|
|
|
|
def allowed_types=(allowed_types)
|
|
allowed_types.each do |assoc_type, session_type|
|
|
self.class.check_session_type(assoc_type, session_type)
|
|
end
|
|
@allowed_types = allowed_types
|
|
end
|
|
|
|
def add_allowed_type(assoc_type, session_type=nil)
|
|
if session_type.nil?
|
|
session_types = self.class.get_session_types(assoc_type)
|
|
else
|
|
self.class.check_session_type(assoc_type, session_type)
|
|
session_types = [session_type]
|
|
end
|
|
for session_type in session_types do
|
|
@allowed_types << [assoc_type, session_type]
|
|
end
|
|
end
|
|
|
|
def allowed?(assoc_type, session_type)
|
|
@allowed_types.include?([assoc_type, session_type])
|
|
end
|
|
|
|
def get_allowed_type
|
|
@allowed_types.empty? ? nil : @allowed_types[0]
|
|
end
|
|
end
|
|
|
|
DefaultNegotiator =
|
|
AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'],
|
|
['HMAC-SHA1', 'no-encryption'],
|
|
['HMAC-SHA256', 'DH-SHA256'],
|
|
['HMAC-SHA256', 'no-encryption']])
|
|
|
|
EncryptedNegotiator =
|
|
AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'],
|
|
['HMAC-SHA256', 'DH-SHA256']])
|
|
end
|