our
$VERSION
=
'0.067'
;
use
Carp
qw( carp croak )
;
our
@ISA
=
qw(Exporter)
;
our
@EXPORT
=
qw( read_ECDSA_signature_file write_ECDSA_signature_file )
;
my
$DEBUG
= 0;
our
$parameters_label
=
"EC PARAMETERS"
;
our
$private_pem_label
=
"EC PRIVATE KEY"
;
our
$public_pem_label
=
"EC PUBLIC KEY"
;
our
$ecdsa_signature_label
=
"ECDSA SIGNATURE"
;
sub
new {
my
(
$class
,
%args
) =
@_
;
my
$self
= {};
bless
$self
,
$class
;
$self
->{password} =
$args
{password}
if
$args
{password};
if
(
$args
{filename} ) {
$self
->{filename} =
$args
{filename};
$self
->read_PEM(
%args
);
}
return
$self
;
}
sub
read_PEM {
my
(
$self
,
%args
) =
@_
;
open
(
my
$fh
,
'<'
,
$args
{filename} )
or croak
"Cannot open file $args{filename} for reading: $!"
;
$args
{fh} =
$fh
;
my
$retval
=
$self
->read_PEM_fh(
%args
);
close
$fh
;
return
$retval
;
}
sub
read_PEM_fh {
my
(
$self
,
%args
) =
@_
;
my
$infh
=
$args
{fh} or
return
;
my
@pem_lines
= <
$infh
>;
@pem_lines
= decrypt_pem( \
@pem_lines
,
$args
{password} )
if
$args
{password};
my
$buf
=
''
;
my
%names
;
my
$working_key
;
foreach
my
$line
(
@pem_lines
) {
if
(
$line
=~ /^--.+--$/ ) {
if
(
$line
=~ /^[\s\-]+BEGIN\s+([\w\s\d]+)[\s\-]+/ ) {
$working_key
= $1;
$names
{$1} = ();
}
elsif
(
$line
=~ /^[\s\-]+END\s+([\w\s\d]+)[\s\-]+/ ) {
$working_key
=
''
;
}
}
else
{
$names
{
$working_key
} .=
$line
if
$working_key
;
}
}
$self
->{DER_entries} = \
%names
;
$self
->read_PEM_entries(
%args
);
}
sub
read_PEM_entries {
my
(
$self
,
%args
) =
@_
;
my
%names
= %{
$self
->{DER_entries} };
while
(
my
(
$name
,
$content
) =
each
%names
) {
if
(
$name
eq
$private_pem_label
) {
$self
->{private_pem_tree}->{name} =
$content
;
my
$der_content
= decode_base64(
$content
);
$args
{der_content} =
$der_content
;
$args
{ptree} =
$self
->{private_pem_tree};
$self
->private_pem_DER_to_tree(
$args
{der_content},
$args
{ptree} );
}
elsif
(
$name
eq
$parameters_label
) {
$self
->{ec_parameters_tree}->{name} =
$content
;
my
$der_content
= decode_base64(
$content
);
my
@ber_bytes
=
unpack
(
'w*'
,
substr
$der_content
, 2 );
if
(
$ber_bytes
[0] < 0x28 ) {
unshift
@ber_bytes
, 0x00 }
elsif
(
$ber_bytes
[0] < 0x50 ) {
$ber_bytes
[0] -= 0x28;
unshift
@ber_bytes
, 0x01;
}
else
{
$ber_bytes
[0] -= 0x50;
unshift
@ber_bytes
, 0x02 }
my
$oid
=
join
'.'
,
@ber_bytes
;
$self
->{ec_parameters_tree}->{namedCurve} =
$oid
;
(
$self
->{ec_parameters_tree}->{standard},
undef
) =
ANS1_oid_to_standard_curve(
$oid
);
}
elsif
(
$name
eq
$public_pem_label
) {
}
elsif
(
$name
eq
$ecdsa_signature_label
) {
}
}
}
sub
private_pem_DER_to_tree {
my
(
$self
,
$content
,
$ptree
) =
@_
;
my
$coding
= Encoding::BER::DER->new();
$coding
->add_implicit_tag(
'context'
,
'constructed'
,
'namedCurve'
, 0x00,
'oid'
);
$coding
->add_implicit_tag(
'context'
,
'constructed'
,
'subjectPublicKey'
,
0x01,
'bit_string'
);
my
$tree
=
$coding
->decode(
$content
);
$ptree
->{d} = DER_octet_string_to_bint(
$tree
->{value}->[1]->{value} );
my
$curve_oid
=
$tree
->{value}->[2]->{value}->[0]->{value};
my
(
$standard
,
$alg
) = ANS1_oid_to_standard_curve(
$curve_oid
);
my
$curve
=
$alg
eq
'Koblitz'
? Crypt::ECDSA::Curve::Koblitz->new(
standard
=>
$standard
)
: Crypt::ECDSA::Curve::Prime->new(
standard
=>
$standard
);
my
$pkey_bstring
=
$tree
->{value}->[3]->{value};
my
$pub_point
= DER_public_key_to_point(
$pkey_bstring
,
$curve
);
$ptree
->{standard} =
$standard
;
$ptree
->{curve} =
$curve
;
$ptree
->{Q} =
$pub_point
;
$ptree
->{order} =
$curve
->{point_order};
$ptree
->{tree} =
$tree
;
return
$ptree
;
}
sub
write_PEM {
my
(
$self
,
%args
) =
@_
;
my
$key
=
$args
{key};
my
$filename
=
$args
{filename};
my
$password
=
$args
{password};
my
$cipher
=
$args
{cipher};
my
$txt
;
if
(
$args
{private} ) {
$txt
=
$self
->key_to_private_PEM(
$key
,
$password
,
$cipher
);
}
else
{
warn
"making public key PEM"
if
$DEBUG
;
$txt
=
$self
->key_to_public_PEM(
$key
);
}
open
my
$outfh
,
'>'
,
$filename
or croak
"Cannot write to $filename: $!"
;
binmode
$outfh
;
my
$written
=
print
$outfh
$txt
;
close
$outfh
;
return
$written
;
}
sub
key_to_private_PEM {
my
(
$self
,
$key
,
$password
,
$cipher
) =
@_
;
$cipher
=
'Rijndael'
unless
$cipher
;
my
$version
= 1;
my
$d_octet
=
pack
"H*"
,
substr
(
$key
->secret->as_hex, 2 );
my
$ans1_numbers
= standard_curve_to_ANS1(
$key
->curve->standard );
my
$public_octet
=
$key
->curve->to_octet(
$key
->Qx,
$key
->Qy );
my
$coding
= Encoding::BER::DER->new();
$coding
->add_implicit_tag(
'context'
,
'constructed'
,
'namedCurve'
, 0x00,
'oid'
);
$coding
->add_implicit_tag(
'context'
,
'constructed'
,
'subjectPublicKey'
,
0x01,
'bit_string'
);
my
$tree
= {
'type'
=> [
'universal'
,
'constructed'
,
'sequence'
],
'value'
=> [
{
'value'
=> 1,
'type'
=> [
'universal'
,
'primitive'
,
'integer'
, ],
},
{
'type'
=> [
'universal'
,
'primitive'
,
'octet_string'
, ],
'value'
=>
$d_octet
,
},
{
'type'
=> [
'context'
,
'constructed'
,
'namedCurve'
, ],
'value'
=> [
{
'value'
=>
$ans1_numbers
,
'type'
=> [
'universal'
,
'primitive'
,
'oid'
, ],
},
],
},
{
'type'
=> [
'context'
,
'constructed'
,
'subjectPublicKey'
, ],
'value'
=> [
{
'value'
=>
$public_octet
,
'type'
=> [
'universal'
,
'primitive'
,
'bit_string'
, ],
},
],
},
],
};
my
$b64str
= encode_base64(
$coding
->encode(
$tree
) );
$b64str
=~ s/\s//g;
$Text::Wrap::columns
= 65;
my
$txt
= Text::Wrap::wrap(
''
,
''
,
$b64str
);
my
$PEM
;
if
(
$password
) {
my
@lines
=
map
{
"$_\n"
}
split
/\n/,
$txt
;
$PEM
= encrypt_pem( \
@lines
,
$cipher
,
$password
);
}
else
{
my
$dashes
=
'-----'
;
my
$begin
=
'BEGIN '
;
my
$end
=
'END '
;
$PEM
=
"$dashes$begin$private_pem_label$dashes\n"
.
$txt
.
"\n$dashes$end$private_pem_label$dashes"
;
}
$self
->{private_pem_tree}->{output_PEM} =
$PEM
;
return
$PEM
;
}
sub
key_to_public_PEM {
my
(
$self
,
$key
,
$password
) =
@_
;
my
$version
= 1;
my
$ans1_numbers
= standard_curve_to_ANS1(
$key
->curve->standard );
my
$public_octet
=
$key
->curve->to_octet(
$key
->Qx,
$key
->Qy );
my
$coding
= Encoding::BER::DER->new();
$coding
->add_implicit_tag(
'context'
,
'constructed'
,
'namedCurve'
, 0x00,
'oid'
);
$coding
->add_implicit_tag(
'context'
,
'constructed'
,
'subjectPublicKey'
,
0x01,
'bit_string'
);
my
$tree
= {
'type'
=> [
'universal'
,
'constructed'
,
'sequence'
],
'value'
=> [
{
'value'
=> 1,
'type'
=> [
'universal'
,
'primitive'
,
'integer'
, ],
},
{
'type'
=> [
'context'
,
'constructed'
,
'namedCurve'
, ],
'value'
=> [
{
'value'
=>
$ans1_numbers
,
'type'
=> [
'universal'
,
'primitive'
,
'oid'
, ],
},
],
},
{
'type'
=> [
'context'
,
'constructed'
,
'subjectPublicKey'
, ],
'value'
=> [
{
'value'
=>
$public_octet
,
'type'
=> [
'universal'
,
'primitive'
,
'bit_string'
, ],
},
],
},
],
};
my
$b64str
= encode_base64(
$coding
->encode(
$tree
) );
$b64str
=~ s/\s//g;
$Text::Wrap::columns
= 65;
my
$txt
= Text::Wrap::wrap(
''
,
''
,
$b64str
);
my
$dashes
=
'-----'
;
my
$begin
=
'BEGIN '
;
my
$end
=
'END '
;
my
$PEM
=
"$dashes$begin$public_pem_label$dashes\n"
.
$txt
.
"\n$dashes$end$public_pem_label$dashes"
;
$self
->{public_pem_tree}->{output_PEM} =
$PEM
;
return
$PEM
;
}
sub
read_ECDSA_signature_file {
my
(
$filename
) =
@_
;
open
(
my
$fh
,
'<'
,
$filename
)
or croak(
"Cannot read signature file $filename: $!"
);
binmode
$fh
;
read
(
$fh
,
my
$content
, -s
$fh
);
close
$fh
;
my
$coding
= Encoding::BER::DER->new();
my
$tree
=
$coding
->decode(
$content
);
my
$r
= bint(
$tree
->{value}->[0]->{value}->as_hex );
my
$s
= bint(
$tree
->{value}->[1]->{value}->as_hex );
warn
"r is $r and s is $s"
if
$DEBUG
;
return
(
$r
,
$s
);
}
sub
write_ECDSA_signature_file {
my
(
$filename
,
$r
,
$s
) =
@_
;
open
(
my
$outfh
,
'>'
,
$filename
)
or croak(
"Cannot open file $filename for writing: $!"
);
binmode
$outfh
;
my
$coding
= Encoding::BER::DER->new();
warn
"r is $r and s is $s"
if
$DEBUG
;
my
$tree
= {
'type'
=> [
'universal'
,
'constructed'
,
'sequence'
],
'value'
=> [
{
'value'
=>
$r
->as_hex,
'type'
=> [
'universal'
,
'primitive'
,
'integer'
, ],
},
{
'value'
=>
$s
->as_hex,
'type'
=> [
'universal'
,
'primitive'
,
'integer'
, ],
},
],
};
my
$retval
=
print
$outfh
$coding
->encode(
$tree
);
close
$outfh
;
return
$retval
;
}
sub
DER_octet_string_to_bint {
my
(
$str
) =
@_
;
return
hex_bint(
unpack
(
'H*'
,
$str
) );
}
sub
bint_to_DER_octet_string {
my
(
$n
) =
@_
;
$n
= bint(
$n
)
unless
ref
$n
;
return
uc
substr
(
$n
->as_hex, 2 );
}
sub
DER_public_key_to_point {
my
(
$str
,
$curve
) =
@_
;
my
$point
= Crypt::ECDSA::Point->new(
octet
=>
$str
,
curve
=>
$curve
,
order
=>
$curve
->order
);
return
$point
;
}
our
$curve_type
= {
'1.2.840.10045.3.1.1'
=>
'secp192r1'
,
'1.3.132.0.1'
=>
'sect163k1'
,
'1.3.132.0.15'
=>
'sect163r2'
,
'1.3.132.0.33'
=>
'sect224r1'
,
'1.3.132.0.26'
=>
'sect233k1'
,
'1.3.132.0.27'
=>
'sect233r1'
,
'1.2.840.10045.3.1.7'
=>
'secp256r1'
,
'1.3.132.0.16'
=>
'sect283k1'
,
'1.3.132.0.17'
=>
'sect283r1'
,
'1.3.132.0.34'
=>
'sect384r1'
,
'1.3.132.0.36'
=>
'sect409k1'
,
'1.3.132.0.37'
=>
'sect409r1'
,
'1.3.132.0.35'
=>
'secp521r1'
,
'1.3.132.0.38'
=>
'sect571k1'
,
'1.3.132.0.39'
=>
'sect571r1'
,
};
sub
ANS1_oid_to_standard_curve {
my
(
$oid
) =
@_
;
return
unless
$oid
and
$curve_type
->{
$oid
};
no
warnings;
my
$curve_type_to_curve
=
$Crypt::ECDSA::Curve::ANS1_lookup
;
my
$named_curve
=
$Crypt::ECDSA::Curve::named_curve
;
my
$standard
=
$curve_type_to_curve
->{
$curve_type
->{
$oid
} };
return
unless
$standard
;
my
$alg
=
$named_curve
->{
$standard
}->{algorithm};
return
(
$standard
,
$alg
);
}
sub
standard_curve_to_ANS1 {
my
(
$standard
) =
@_
;
my
$curve_type_to_curve
=
$Crypt::ECDSA::Curve::ANS1_lookup
;
my
%standard_to_names
=
reverse
%$curve_type_to_curve
;
my
%names_to_ans1
=
reverse
%$curve_type
;
return
$names_to_ans1
{
$standard_to_names
{
$standard
} };
}
our
$PEM_cipher_type
= {
'DES-CBC'
=>
'DES'
,
'DES-EDE3-CBC'
=>
'DES_EDE3'
,
'AES-128-CBC'
=>
'Rijndael'
,
'BF-CBC'
=>
'Blowfish'
,
};
our
$cipher_to_DEK
= {
Blowfish
=>
'BF-CBC'
,
"DES_EDE3"
=>
'DES-EDE3-CBC'
,
Rijndael
=>
'AES-128-CBC'
,
DES
=>
'DES-CBC'
,
};
our
$cipher_iv_bitsize
= {
"DES_EDE3"
=> 64,
Rijndael
=> 128,
Blowfish
=> 64,
DES
=> 64,
};
our
$cipher_key_bytesize
= {
"DES_EDE3"
=> 24,
Rijndael
=> 16,
Blowfish
=> 16,
DES
=> 8,
};
sub
encrypt_pem {
my
(
$pem_lines
,
$cipher
,
$password
) =
@_
;
my
$DEK_type
=
$cipher_to_DEK
->{
$cipher
};
croak
"Need password and cipher type"
unless
$password
and
$DEK_type
;
warn
"encrypting PEM with password $password and type $DEK_type"
if
$DEBUG
;
my
$bytes_needed
=
$cipher_iv_bitsize
->{
$cipher
} / 8;
my
$iv_str
=
''
;
do
{
$iv_str
=
uc
random_hex_bytes(
$cipher_iv_bitsize
->{
$cipher
} / 8 );
warn
"desired bit length is "
,
$cipher_iv_bitsize
->{
$cipher
},
" and generated length of $iv_str is "
,
length
(
$iv_str
) * 8
if
$DEBUG
;
}
while
length
(
$iv_str
) * 4 !=
$cipher_iv_bitsize
->{
$cipher
};
my
$iv
=
pack
"H*"
,
$iv_str
;
my
$keystring
= evp_key(
$password
,
$iv
,
$cipher_key_bytesize
->{
$cipher
} );
my
$work
=
join
''
, @{
$pem_lines
};
warn
"decoding base64"
if
$DEBUG
;
$work
= decode_base64(
$work
);
my
$alg
= Crypt::CBC->new(
-literal_key
=> 1,
-key
=>
$keystring
,
-cipher
=>
$cipher
,
-iv
=>
$iv
,
-header
=>
'none'
,
-keysize
=>
$cipher_key_bytesize
->{
$cipher
},
);
warn
"encrypting binary"
if
$DEBUG
;
$work
=
$alg
->encrypt(
$work
);
warn
"beginning base64 encode"
if
$DEBUG
;
my
$b64str
= encode_base64(
$work
);
$b64str
=~ s/\s//g;
$Text::Wrap::columns
= 65;
my
$txt
= Text::Wrap::wrap(
''
,
''
,
$b64str
);
my
$begin
=
"-----BEGIN EC PRIVATE KEY-----"
;
my
$second
=
"Proc-Type: 4,ENCRYPTED"
;
my
$third
=
"DEK-Info: $cipher_to_DEK->{$cipher},$iv_str"
;
my
$end
=
"-----END EC PRIVATE KEY-----"
;
my
$PEM
=
"$begin\n$second\n$third\n\n$txt\n$end\n"
;
return
$PEM
;
}
sub
decrypt_pem {
my
(
$pem_lines
,
$password
) =
@_
;
my
(
$begin
,
$end
,
$cipher
,
$iv
,
$keystring
);
my
$work
=
''
;
my
$found_encryption
;
for
my
$line
(
@$pem_lines
) {
if
(
$line
=~ /^-----BEGIN/ ) {
$begin
=
$line
;
}
elsif
(
$line
=~ /^-----END/ ) {
$end
=
$line
;
last
;
}
elsif
(
$line
=~ /^Proc-Type/i ) {
next
;
}
elsif
(
$line
=~ /^DEK-Info:\s*([^\,]+),([\dabcdef]+)/i ) {
$found_encryption
= 1;
$cipher
=
$PEM_cipher_type
->{$1};
my
$key_bytesize
=
$cipher_key_bytesize
->{
$cipher
};
$iv
=
pack
"H*"
, $2;
$keystring
= evp_key(
$password
,
$iv
,
$key_bytesize
);
}
else
{
$work
.=
$line
;
}
}
return
(
@$pem_lines
)
unless
$found_encryption
;
croak
"Missing data: password($password), iv($iv)"
unless
$password
and
$iv
;
$work
=~ s/\s//g;
$work
= decode_base64(
$work
);
my
$alg
= Crypt::CBC->new(
-keysize
=>
$cipher_key_bytesize
->{
$cipher
},
-literal_key
=> 1,
-key
=>
$keystring
,
-cipher
=>
$cipher
,
-iv
=>
$iv
,
-header
=>
'none'
,
);
$work
=
$alg
->decrypt(
$work
);
$work
= encode_base64(
$work
);
return
(
$begin
,
$work
,
$end
);
}
sub
evp_key {
my
(
$data
,
$salt
,
$key_byte_size
) =
@_
;
$salt
=
substr
(
$salt
, 0, 8 );
my
$key
= md5(
$data
,
$salt
);
while
(
length
(
$key
) <
$key_byte_size
) {
$key
.= md5(
$key
,
$data
,
$salt
);
}
return
substr
$key
, 0,
$key_byte_size
;
}
1;