Crypt-JWS-OpenSSL

 view release on metacpan or  search on metacpan

lib/Crypt/JWS/OpenSSL/Algorithm/ECC.pm  view on Meta::CPAN

package Crypt::JWS::OpenSSL::Algorithm::ECC;
$Crypt::JWS::OpenSSL::Algorithm::ECC::VERSION = '0.005';
use Moo;
with qw(
  Crypt::JWS::OpenSSL::Role::Algorithm
  Crypt::JWS::OpenSSL::Role::Encoder
);
use Crypt::OpenSSL::Random 0.15;
use Crypt::OpenSSL::EC 1.32;
use Crypt::OpenSSL::ECDSA 0.10;
use Crypt::OpenSSL::Bignum 0.09;
use Crypt::OpenSSL::Bignum::CTX;
use Digest::SHA;
use Carp qw( croak );
use namespace::clean;

my $ECC_ALGO_MAP = {
    'ES256' => [ \&Digest::SHA::sha256, 'prime256v1', 32, 415, qr!\x04\x20(.{32})!s, \&Digest::SHA::hmac_sha256 ],
    'ES384' => [ \&Digest::SHA::sha384, 'secp384r1',  48, 715, qr!\x04\x30(.{48})!s, \&Digest::SHA::hmac_sha384 ],
    'ES512' => [ \&Digest::SHA::sha512, 'secp521r1',  66, 716, qr!\x04\x42(.{66})!s, \&Digest::SHA::hmac_sha512 ],
};

sub can_do_non_deterministic { 1 }
sub can_do_deterministic { 1 }

sub sign {
    my( $self, $request) = parse_params(@_);
    my $algorithm = $request->{algorithm};    
    my ($hash_method, $curve, $coord_len, $nid, $priv_key_qr, $hmac_method, $qlen ) = @{ $ECC_ALGO_MAP->{$algorithm} };
    
    my $key_ref = ref($request->{key});
    
    my $hex_private_key;
    
    if ( $key_ref )  {
        if ( $key_ref eq 'HASH' && exists($request->{key}->{d}) ) {
            my $keybin = $self->decode_jwk_element($request->{key}->{d});
            $hex_private_key = unpack("H*", $keybin);
        } else {
            croak 'failed to parse private key';
        }
    } else {
        $hex_private_key = $self->_load_private_key_as_hex($request->{key}, $priv_key_qr );
    }
        
    unless( $hex_private_key ) {
        croak "Could not extract a ${coord_len}-byte private key scalar from the private key";
    }
            
    my $ecc_key = Crypt::OpenSSL::EC::EC_KEY::new_by_curve_name($nid);
    die "Failed to initialize EC_KEY for curve NID $curve" unless $ecc_key;
    
    unless(Crypt::OpenSSL::EC::EC_KEY::set_private_key(
            $ecc_key, Crypt::OpenSSL::Bignum->new_from_hex($hex_private_key)
        )
    ) {
        croak "Failed to set private key on the EC_KEY object.";
    }
            
    my $digest = $hash_method->($request->{message});
    
    my $sig_obj;
        
    if ( $request->{non_deterministic} ) {
        $sig_obj = Crypt::OpenSSL::ECDSA::ECDSA_do_sign($digest, $ecc_key)
            or croak "Signing failed";
    } else {
        my $group = Crypt::OpenSSL::EC::EC_GROUP::new_by_curve_name($nid);
        my $ctx   = Crypt::OpenSSL::Bignum::CTX->new();
        my $q     = Crypt::OpenSSL::Bignum->new;
        Crypt::OpenSSL::EC::EC_GROUP::get_order($group, $q, $ctx)
            or croak 'failed to initialize curve order';
        
        my $k = _generate_k(
            $q->to_hex,
            $hex_private_key,
            $digest,
            $hmac_method,
            $coord_len,
            $q->num_bits
        );
                
        # Calculate kinv
        my $q_minus_two = $q->sub(Crypt::OpenSSL::Bignum->new_from_word(2));
        my $kinv        = $k->mod_exp($q_minus_two, $q, $ctx);
        
        # Calculate rp
        my $R = Crypt::OpenSSL::EC::EC_POINT::new($group);
        Crypt::OpenSSL::EC::EC_POINT::mul($group, $R, $k, \0, \0, $ctx);
        
        my $rx = Crypt::OpenSSL::Bignum->new();
        my $ry = Crypt::OpenSSL::Bignum->new();
        
        Crypt::OpenSSL::EC::EC_POINT::get_affine_coordinates_GFp($group, $R, $rx, $ry, $ctx);
        
        my (undef, $rp) = $rx->div($q, $ctx);
        
        $sig_obj = Crypt::OpenSSL::ECDSA::ECDSA_do_sign_ex($digest, $kinv, $rp, $ecc_key)
            or croak "Signing failed";
    }
        
    my $r_oct = $sig_obj->get_r();
    my $s_oct = $sig_obj->get_s();
    
    # pack r and s to correct length with leading bytes
    
    $r_oct = pack("a$coord_len", ("\x00" x ($coord_len - length($r_oct))) . $r_oct);
    $s_oct = pack("a$coord_len", ("\x00" x ($coord_len - length($s_oct))) . $s_oct);
    
    my $signature = $r_oct . $s_oct;
    
    return $signature;
}

sub verify {
    my( $self, $request) = parse_params(@_);
        
    my $algorithm = $request->{algorithm};
        
    my ($hash_method, $curve, $coord_len, $nid, $priv_key_qr, $hmac_method ) = @{  $ECC_ALGO_MAP->{$algorithm} };
    
    my $key_ref = ref($request->{key});
    
    my ( $pub_x_bin, $pub_y_bin );
    
    if ($key_ref) {
        if ($key_ref eq 'HASH' && exists($request->{key}->{x}) && exists($request->{key}->{y})) {
            $pub_x_bin = $self->decode_jwk_element($request->{key}->{x});
            $pub_y_bin = $self->decode_jwk_element($request->{key}->{y});
            $pub_x_bin = undef unless( length($pub_x_bin) == $coord_len);
            $pub_y_bin = undef unless( length($pub_y_bin) == $coord_len);
        }
    } else {
        my $p_key_bytes = $self->decode_pem($request->{key});
        my $raw_pub_points = substr($p_key_bytes, - ( $coord_len * 2 ));
        $pub_x_bin = substr($raw_pub_points, 0, $coord_len);
        $pub_y_bin = substr($raw_pub_points, $coord_len, $coord_len);
    }
    
    unless( $pub_x_bin && $pub_y_bin ) {
        croak 'failed to parse public key';
    }
    
    my $digest = $hash_method->($request->{message});
    
    my $group = Crypt::OpenSSL::EC::EC_GROUP::new_by_curve_name($nid);
    my $ecc_key = Crypt::OpenSSL::EC::EC_KEY::new();
    Crypt::OpenSSL::EC::EC_KEY::set_group($ecc_key, $group);
    
    # Populate Public Key Point
    my $x_bn  = Crypt::OpenSSL::Bignum->new_from_bin($pub_x_bin);
    my $y_bn  = Crypt::OpenSSL::Bignum->new_from_bin($pub_y_bin);
    my $point = Crypt::OpenSSL::EC::EC_POINT::new($group);
    
    my $ctx = Crypt::OpenSSL::Bignum::CTX->new();
    
    Crypt::OpenSSL::EC::EC_POINT::set_affine_coordinates_GFp($group, $point, $x_bn, $y_bn, $ctx)
        || croak "Failed to map point coordinates";
    Crypt::OpenSSL::EC::EC_KEY::set_public_key($ecc_key, $point)
        || croak "Failed to assign public key";
    
    my $r_bin = substr($request->{signature}, 0, $coord_len);
    my $s_bin = substr($request->{signature}, $coord_len, $coord_len);
    
    my $ecdsa_sig = Crypt::OpenSSL::ECDSA::ECDSA_SIG->new();
    
    $ecdsa_sig->set_r($r_bin);
    $ecdsa_sig->set_s($s_bin);
    
    my $result = Crypt::OpenSSL::ECDSA::ECDSA_do_verify( $digest, $ecdsa_sig, $ecc_key );
    
    if ( $result && $result eq '1') {
        return 1;
    } else {
        return 0;
    }
}

sub _load_private_key_as_hex {
    my($self, $pem, $rx) = @_;
    my $raw_key = $self->decode_pem($pem);
    my $hex_private_key;
    if( $raw_key =~ $rx)  {
        $hex_private_key = unpack("H*", $1);
    }
    return $hex_private_key;
}

sub _generate_k {
    my ($curve_order_hex, $private_key_hex, $digest, $hmac_method, $bytelen, $bitlen) = @_;
    my $order = Crypt::OpenSSL::Bignum->new_from_hex($curve_order_hex);
    my $key   = Crypt::OpenSSL::Bignum->new_from_hex($private_key_hex);
    
    my $curve = {
        q       => $order,
        bitlen  => $bitlen,
        bytelen => $bytelen,
    };
    
    my $privkey_bytes = $key->to_bin();
    
    substr( $privkey_bytes, 0, 0, "\x00" x ($curve->{'bytelen'} - length $privkey_bytes) );
    
    my $hash_len = length $digest;
    
    # Initialize cryptographic components V and K
    my $V = "\x01" x $hash_len;
    my $K = "\x00" x $hash_len;
    
    my $digest_octets = _bits2octets($digest, $curve );
        
    # Spin the HMAC state using the raw key string components
    $K = $hmac_method->( $V . "\x00" . $privkey_bytes . $digest_octets, $K );
    $V = $hmac_method->( $V, $K );
    $K = $hmac_method->( $V . "\x01" . $privkey_bytes . $digest_octets, $K );
    $V = $hmac_method->( $V, $K );
    
    # a Bignum with a value of 1
    my $bn_one = Crypt::OpenSSL::Bignum->one; 
    
    # Generation loop
    
    while (1) {
        my $T = '';
        while (1) {
            $V = $hmac_method->( $V, $K );
            $T .= $V;
            last if length($T) * 8 >= $curve->{'bitlen'};
        }
        
        my $k_candidate = _bits2int($T, $curve);
        
        ## rfc6979 allowed range between 1 and q -1
        ## 
        ## $bn_a->cmp($bn_b);
        ## returns:
        ## -1 if $bn_a <  bn_b
        ##  0 if $bn_a == bn_b
        ##  1 if $bn_a >  bn_b
        
        if (   $k_candidate->cmp($bn_one) > -1          # $k_candidate is equal to or greater than 1
            && $k_candidate->cmp($curve->{'q'}) == -1 ) # $k_candidate is less than q
        {
            return $k_candidate;
        }
        
        # If outside boundaries, stir the HMAC key ring state and repeat
        $K = $hmac_method->( $V . "\x00", $K );
        $V = $hmac_method->( $V, $K );
    }
}

lib/Crypt/JWS/OpenSSL/Algorithm/ECC.pm  view on Meta::CPAN

    my ( $bytes, $curve ) = @_;
    my $bignum = Crypt::OpenSSL::Bignum->new_from_bin( $bytes );
    my $blen = 8 * length($bytes);
    if ($curve->{'bitlen'} < $blen) {
        my $rshift = $blen - $curve->{'bitlen'};
        return $bignum->rshift($rshift);
    }
    return $bignum;
}

## Integer to Octet String
## int2octets
## rfc6979 section-2.3.3

sub _int2octets {
    my ( $octets, $curve ) = @_;
    if (length($octets) > $curve->{'bytelen'}) {
        substr( $octets, 0, -$curve->{'bytelen'} ) = '';
    }
    elsif (length($octets) < $curve->{'bytelen'}) {
        substr( $octets, 0, 0, "\x00" x ($curve->{'bytelen'} - length $octets) );
    }
    return $octets;
}

## Bit String to Octet String
## bits2octets
## rfc6979 section-2.3.4

sub _bits2octets {
    my ( $bits, $curve ) = @_;
    my $z1 = _bits2int($bits, $curve);
    my $ctx = Crypt::OpenSSL::Bignum::CTX->new;
    my $z2 = $z1->copy->mod($curve->{'q'}, $ctx );
    return _int2octets($z2->to_bin, $curve);
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Crypt::JWS::OpenSSL::Algorithm::ECC - Sign and verify tokens using ECDSA algorithms

=head1 VERSION

version 0.005

=head1 SYNOPSIS

  use Crypt::JWS::OpenSSL::Algorithm::ECC;
  my $jws = Crypt::JWS::OpenSSL::Algorithm::ECC->new;
  
  my $token_1 = $jws->sign(
    algorithm => 'ES384',
    key       => $secp384r1_private_key_pem_content,
    message   => join('.', $base64urlEncodedHeader, $base64urlEncodedClaims),
  );
  
  my $token2 = $jws->sign(
    algorithm     => 'ES256',
    key           => $prime256v1_private_key_pem_content,
    message       => join('.', $base64urlEncodedHeader, $base64urlEncodedClaims),
    deterministic => 1
  );
  
  my $is_verified_1 = $jws->verify(
    algorithm => 'ES384',
    key       => $secp384r1_public_key_pem_content,
    message   => join('.', $base64urlEncodedHeader, $base64urlEncodedClaims),
    signature => $base64urlDecodedSignature,
  );
  
  my $is_verified_2 = $jws->verify(
    algorithm => 'ES256',
    key       => $prime256v1_public_key_pem_content,
    message   => join('.', $base64urlEncodedHeader, $base64urlEncodedClaims),
    signature => $base64urlDecodedSignature,
  );

=head1 DESCRIPTION

This module uses L<Crypt::OpenSSL::ECDSA> in combination with L<Crypt::OpenSSL::EC>
to sign and verify JWTs using elliptic curve digital signatures.

It is used within L<Crypt::JWS::OpenSSL> but it can be used directly
if you handle the Base64 url encoding and decoding elswhere.

=head1 METHODS

=head2 sign

  my $token = $jwt->sign(
    algorithm => $algo,
    key       => $key,
    message   => $message
  );
  
  my $token_2 = $jwt->sign(
    algorithm         => $algo,
    key               => $key,
    message           => $message,
    non_deterministic => 1,
  );

The method accepts an even numbered list or a hash reference. It returns a signature.

By default, the deterministic signing method is used.

=over

=item C<algorithm>

The name of the algorithm supported by the C<key> parameter.

=item C<key>

The pem encoded ECDSA private key or a hash reference containing a JWK.

It is more efficient to convert a JWK to a pem encoded key and use that
for signing.



( run in 1.074 second using v1.01-cache-2.11-cpan-8dfa8b56332 )