shell bypass 403
use strict;
use Carp ();
############################################################################
package Net::OpenID::Association;
$Net::OpenID::Association::VERSION = '1.18';
use fields (
'server', # author-identity identity provider endpoint
'secret', # the secret for this association
'handle', # the 255-character-max ASCII printable handle (33-126)
'expiry', # unixtime, adjusted, of when this association expires
'type', # association type
);
use Storable ();
use Digest::SHA ();
use Net::OpenID::Common;
use URI::Escape qw(uri_escape);
################################################################
# Association and Session Types
# session type hash
# name - by which session type appears in URI parameters (required)
# len - number of bytes in digest (undef => accommodates any length)
# fn - DH hash function (undef => secret passed in the clear)
# https - must use encrypted connection (boolean)
#
my %_session_types = ();
# {versionkey}{name} -> session type
# {NO}{versionkey} -> no-encryption stype for this version
# {MAX}{versionkey} -> strongest encryption stype for this version
# association type hash
# name - by which assoc. type appears in URI parameters (required)
# len - number of bytes in digest (required)
# macfn - MAC hash function (required)
#
my %_assoc_types = ();
# {versionkey}{name} -> association type
# {MAX}{versionkey} -> strongest encryption atype for this version
my %_assoc_macfn = ();
# {name} -> hmac function
# ... since association types in the cache are only listed by name
# and don't say what version they're from. Which should not matter
# as long as the macfn associated with a given association type
# name does not change in future versions.
# (floating point version numbers scare me)
# (also version key can stay the same if the
# set of hash functions available does not change)
# ('NO' and 'MAX' should never be used as version keys)
sub _version_key_from_numeric {
my ($numeric_protocol_version) = @_;
return $numeric_protocol_version < 2 ? 'v1' : 'v2';
}
# can SESSION_TYPE be used with ASSOC_TYPE?
sub _compatible_stype_atype {
my ($s_type, $a_type) = @_;
return !$s_type->{len} || $s_type->{len} == $a_type->{len};
}
{
# Define the no-encryption session types.
# In version 1.1/1.0, the no-encryption session type
# is the default and never explicitly specified
$_session_types{$_->[0]}{$_->[1]}
= $_session_types{NO}{$_->[0]}
= {
name => $_->[1],
https => 1,
}
foreach ([v1 => ''], [v2 => 'no-encryption']);
# Define SHA-based session and association types
my %_sha_fns =
(
SHA1 => { minv => 'v1', # first version group in which this appears
v1max => 1, # best encryption for v1
len => 20, # number of bytes in digest
fn => \&Digest::SHA::sha1,
macfn => \&Digest::SHA::hmac_sha1, },
SHA256 => { minv => 'v2',
v2max => 1, # best encryption for v2
len => 32,
fn => \&Digest::SHA::sha256,
macfn => \&Digest::SHA::hmac_sha256, },
# doubtless there will be more...
);
foreach my $SHAX (keys %_sha_fns) {
my $s = $_sha_fns{$SHAX};
my $a_type = { name => "HMAC-${SHAX}", map {$_,$s->{$_}} qw(len macfn) };
my $s_type = { name => "DH-${SHAX}", map {$_,$s->{$_}} qw(len fn) };
my $seen_minv = 0;
foreach my $v (qw(v1 v2)) {
$seen_minv = 1 if $v eq $s->{minv};
next unless $seen_minv;
$_assoc_types{$v}{$a_type->{name}} = $a_type;
$_session_types{$v}{$s_type->{name}} = $s_type;
if ($s->{"${v}max"}) {
$_assoc_types{MAX}{$v} = $a_type;
$_session_types{MAX}{$v} = $s_type;
}
}
$_assoc_macfn{$a_type->{name}} = $a_type->{macfn};
}
}
################################################################
sub new {
my Net::OpenID::Association $self = shift;
$self = fields::new( $self ) unless ref $self;
my %opts = @_;
for my $f (qw( server secret handle expiry type )) {
$self->{$f} = delete $opts{$f};
}
Carp::croak("unknown options: " . join(", ", keys %opts)) if %opts;
return $self;
}
sub handle {
my $self = shift;
die if @_;
$self->{'handle'};
}
sub secret {
my $self = shift;
die if @_;
$self->{'secret'};
}
sub type {
my $self = shift;
die if @_;
$self->{'type'};
}
sub generate_signature {
my Net::OpenID::Association $self = shift;
my $string = shift;
return OpenID::util::b64($_assoc_macfn{$self->type}->($string, $self->secret));
}
sub server {
my Net::OpenID::Association $self = shift;
Carp::croak("Too many parameters") if @_;
return $self->{server};
}
sub expired {
my Net::OpenID::Association $self = shift;
return time() > $self->{'expiry'};
}
sub usable {
my Net::OpenID::Association $self = shift;
return 0 unless $self->{'handle'} =~ /^[\x21-\x7e]{1,255}$/;
return 0 unless $self->{'expiry'} =~ /^\d+$/;
return 0 unless $self->{'secret'};
return 0 if $self->expired;
return 1;
}
# server_assoc(CSR, SERVER, FORCE_REASSOCIATE, OPTIONS...)
#
# Return an association for SERVER (provider), whether already
# cached and not yet expired, or freshly negotiated.
# Return undef if no local storage/cache is available
# or negotiation fails for whatever reason,
# in which case the caller goes into dumb consumer mode.
# FORCE_REASSOCIATE true => ignore the cache
# OPTIONS... are passed to new_server_assoc()
#
sub server_assoc {
my ($csr, $server, $force_reassociate, @opts) = @_;
# closure to return undef (dumb consumer mode) and log why
my $dumb = sub {
$csr->_debug("server_assoc: dumb mode: $_[0]");
return undef;
};
my $cache = $csr->cache;
return $dumb->("no_cache") unless $cache;
unless ($force_reassociate) {
# try first from cached association handle
if (my $handle = $cache->get("shandle:$server")) {
my $assoc = handle_assoc($csr, $server, $handle);
if ($assoc && $assoc->usable) {
$csr->_debug("Found association from cache (handle=$handle)");
return $assoc;
}
}
}
# make a new association
my ($assoc, $err, $retry) = new_server_assoc($csr, $server, @opts);
return $dumb->($err)
if $err;
($assoc, $err) = new_server_assoc($csr, $server, @opts, %$retry)
if $retry;
return $dumb->($err || 'second_retry')
unless $assoc;
my $ahandle = $assoc->handle;
$cache->set("hassoc:$server:$ahandle", Storable::freeze({%$assoc}));
$cache->set("shandle:$server", $ahandle);
# now we test that the cache object given to us actually works. if it
# doesn't, it'll also fail later, making the verify fail, so let's
# go into stateless (dumb mode) earlier if we can detect this.
$cache->get("shandle:$server")
or return $dumb->("cache_broken");
return $assoc;
}
# new_server_assoc(CSR, SERVER, OPTIONS...)
#
# Attempts to negotiate a fresh association from C<$server> (provider)
# with session and association types determined by OPTIONS...
# (accepts protocol_version and all assoc_options from Consumer,
# however max_encrypt and session_no_encrypt_https are ignored
# if assoc_type and session_type are passed directly as hashes)
# Returns
# ($association) on success
# (undef, $error_message) on unrecoverable failure
# (undef, undef, {retry...}) if identity provider suggested
# alternate session/assoc types in an error response
#
sub new_server_assoc {
my ($csr, $server, %opts) = @_;
my $server_is_https = lc($server) =~ m/^https:/;
my $protocol_version = delete $opts{protocol_version} || 1;
my $version_key = _version_key_from_numeric($protocol_version);
my $allow_eavesdropping = (delete $opts{allow_eavesdropping} || 0) && $protocol_version < 2;
my $a_maxencrypt = delete $opts{max_encrypt} || 0;
my $s_noencrypt = delete $opts{session_no_encrypt_https} && $server_is_https;
my $s_type = delete $opts{session_type} || "DH-SHA1";
unless (ref $s_type) {
if ($s_noencrypt) {
$s_type = $_session_types{NO}{$version_key};
}
elsif ($a_maxencrypt) {
$s_type = $_session_types{MAX}{$version_key};
}
}
my $a_type = delete $opts{assoc_type} || "HMAC-SHA1";
unless (ref $a_type) {
$a_type = $_assoc_types{MAX}{$version_key}
if $a_maxencrypt;
}
Carp::croak("unknown options: " . join(", ", keys %opts)) if %opts;
$a_type = $_assoc_types{$version_key}{$a_type} unless ref $a_type;
Carp::croak("unknown association type") unless $a_type;
$s_type = $_session_types{$version_key}{$s_type} unless ref $s_type;
Carp::croak("unknown session type") unless $s_type;
my $error = sub { return (undef, $_[0].($_[1]?" ($_[1])":'')); };
return $error->("incompatible_session_type")
unless _compatible_stype_atype($s_type, $a_type);
return $error->("https_required")
if $s_type->{https} && !$server_is_https && !$allow_eavesdropping;
my %post = ( "openid.mode" => "associate" );
$post{'openid.ns'} = OpenID::util::version_2_namespace()
if $protocol_version == 2;
$post{'openid.assoc_type'} = $a_type->{name};
$post{'openid.session_type'} = $s_type->{name} if $s_type->{name};
my $dh;
if ($s_type->{fn}) {
$dh = OpenID::util::get_dh();
$post{'openid.dh_consumer_public'} = OpenID::util::int2arg($dh->pub_key);
}
my $req = HTTP::Request->new(POST => $server);
$req->header("Content-Type" => "application/x-www-form-urlencoded");
$req->content(join("&", map { "$_=" . uri_escape($post{$_}) } keys %post));
$csr->_debug("Associate mode request: " . $req->content);
my $ua = $csr->ua;
my $res = $ua->request($req);
return $error->("http_no_response") unless $res;
my $recv_time = time();
my $content = $res->content;
my %args = OpenID::util::parse_keyvalue($content);
$csr->_debug("Response to associate mode: [$content] parsed = " . join(",", %args));
my $r_a_type = $_assoc_types{$version_key}{$args{'assoc_type'}};
my $r_s_type = $_session_types{$version_key}{$args{'session_type'}||''};
unless ($res->is_success) {
# direct error
return $error->("http_failure_no_associate")
if ($protocol_version < 2);
return $error->("http_direct_error")
unless $args{'error_code'} eq 'unsupported_type';
return (undef,undef,{assoc_type => $r_a_type, session_type => $r_s_type})
if $r_a_type && $r_s_type && ($r_a_type != $a_type || $r_s_type != $s_type);
return $error->("unsupported_type");
}
return $error->("unknown_assoc_type",$args{'assoc_type'})
unless $r_a_type;
return $error->("unknown_session_type",$args{'session_type'})
unless $r_s_type;
return $error->("wrong_assoc_type",$r_a_type->{name})
unless $a_type == $r_a_type;
return $error->("wrong_session_type",$r_s_type->{name})
unless $s_type == $r_s_type || ($protocol_version < 2);
# protocol version 1.1
my $expires_in = $args{'expires_in'};
# protocol version 1.0 (DEPRECATED)
if (! $expires_in) {
if (my $issued = OpenID::util::w3c_to_time($args{'issued'})) {
my $expiry = OpenID::util::w3c_to_time($args{'expiry'});
my $replace_after = OpenID::util::w3c_to_time($args{'replace_after'});
# seconds ahead (positive) or behind (negative) the provider is
$expires_in = ($replace_after || $expiry) - $issued;
}
}
# between 1 second and 2 years
return $error->("bogus_expires_in")
unless $expires_in > 0 && $expires_in < 63072000;
my $ahandle = $args{'assoc_handle'};
my $secret;
unless ($r_s_type->{fn}) {
$secret = OpenID::util::d64($args{'mac_key'});
}
else {
my $server_pub = OpenID::util::arg2int($args{'dh_server_public'});
my $dh_sec = $dh->compute_secret($server_pub);
$secret = OpenID::util::d64($args{'enc_mac_key'})
^ $r_s_type->{fn}->(OpenID::util::int2bytes($dh_sec));
}
return $error->("bad_secret_length")
if $r_s_type->{len} && length($secret) != $r_s_type->{len};
my %assoc = (
handle => $ahandle,
server => $server,
secret => $secret,
type => $r_a_type->{name},
expiry => $recv_time + $expires_in,
);
return Net::OpenID::Association->new( %assoc );
}
# returns association, or undef if it can't be found
sub handle_assoc {
my ($csr, $server, $handle) = @_;
# closure to return undef (dumb consumer mode) and log why
my $dumb = sub {
$csr->_debug("handle_assoc: dumb mode: $_[0]");
return undef;
};
return $dumb->("no_handle") unless $handle;
my $cache = $csr->cache;
return $dumb->("no_cache") unless $cache;
my $frozen = $cache->get("hassoc:$server:$handle");
return $dumb->("not_in_cache") unless $frozen;
my $param = eval { Storable::thaw($frozen) };
return $dumb->("not_a_hashref") unless ref $param eq "HASH";
return Net::OpenID::Association->new( %$param );
}
sub invalidate_handle {
my ($csr, $server, $handle) = @_;
my $cache = $csr->cache
or return;
$cache->set("hassoc:$server:$handle", "");
}
1;
__END__
=head1 NAME
Net::OpenID::Association - A relationship with an identity provider
=head1 VERSION
version 1.18
=head1 DESCRIPTION
Internal class.
=head1 COPYRIGHT, WARRANTY, AUTHOR
See L<Net::OpenID::Consumer> for author, copyright and licensing information.
=head1 SEE ALSO
L<Net::OpenID::Consumer>
L<Net::OpenID::VerifiedIdentity>
L<Net::OpenID::Server>
Website: L<http://openid.net/>