You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rtpengine/perl/NGCP/Rtpclient/DTLS.pm

326 lines
7.2 KiB

package NGCP::Rtpclient::DTLS;
use strict;
use warnings;
use NGCP::Rtpclient::SRTP;
use File::Temp;
use Crypt::OpenSSL::RSA;
use IO::Socket::INET;
use IPC::Open3;
use IO::Multiplex;
use Time::HiRes qw(sleep time);
sub new {
my ($class, $mux, $local_sockets, $output_func, $tag, $cert) = @_;
my $self = {};
bless $self, $class;
$self->{_output_func} = $output_func;
$self->{_mux} = $mux;
$self->{_tag} = $tag;
$self->{_local_sockets} = $local_sockets;
if ($cert) {
$self->set_cert($cert);
}
else {
$self->new_cert();
}
return $self;
}
sub new_cert {
my ($self) = @_;
my $rsa_key = Crypt::OpenSSL::RSA->generate_key(1024);
my $priv_key = $rsa_key->get_private_key_string();
my $key_file = File::Temp->new();
print $key_file $priv_key;
close($key_file);
my $cert_file = File::Temp->new();
system(qw(openssl req -key), $key_file->filename(), '-out', $cert_file->filename(),
qw(-new -x509 -days 30 -subj /CN=tester -batch));
my $cert;
read($cert_file, $cert, 10000);
close($cert_file);
my $cert_key_file = File::Temp->new();
print $cert_key_file $cert;
print $cert_key_file $priv_key;
close($cert_key_file);
$self->set_cert($cert_key_file);
return $cert_key_file;
}
sub get_cert {
my ($self) = @_;
return $self->{_cert_key_file};
}
sub set_cert {
my ($self, $file) = @_;
$self->{_cert_key_file} = $file;
}
# XXX unify these two
sub connect { ## no critic: Subroutines::ProhibitBuiltinHomonyms
my ($self) = @_;
$self->{_connected} and return;
$self->_kill_openssl_child();
my $near = $self->{_near};
$near or ($near = $self->{_near} = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost',
Proto => 'udp'));
my $near_port = $near->sockport();
my ($openssl_in, $openssl_out);
$self->{_openssl_pid} = open3($openssl_in, $openssl_out, undef,
qw(openssl s_client -connect),
"localhost:$near_port",
'-cert', $self->{_cert_key_file}->filename(), qw(-dtls1 -use_srtp
SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32 -keymatexport EXTRACTOR-dtls_srtp
-keymatexportlen 60));
$self->{_openssl_out} = $openssl_out;
$self->{_openssl_in} = $openssl_in;
$self->{_openssl_buf} = '';
$self->{_mux}->add($near);
$self->{_mux}->add($openssl_out);
}
sub accept { ## no critic: Subroutines::ProhibitBuiltinHomonyms
my ($self) = @_;
$self->{_connected} and return;
$self->_kill_openssl_child();
my ($near_port, $near_peer);
my $near = $self->{_near};
if ($near) {
$near_port = $near->peerport();
$near_peer = $near->peeraddr();
}
else {
my $tmp = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost', Proto => 'udp');
$near_port = $tmp->sockport();
undef($tmp);
$near = $self->{_near} = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost',
Proto => 'udp');
$near_peer = pack_sockaddr_in($near_port, inet_aton("localhost"));
# $near gets connected below
}
my ($openssl_in, $openssl_out);
$self->{_openssl_pid} = open3($openssl_in, $openssl_out, undef,
qw(openssl s_server -accept),
$near_port,
'-cert', $self->{_cert_key_file}->filename(), qw(-dtls1 -use_srtp
SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32 -keymatexport EXTRACTOR-dtls_srtp
-keymatexportlen 60));
# XXX dtls 1.2 ?
sleep(0.2); # given openssl a short while to start up
$self->_near_peer($near_peer);
$self->{_openssl_out} = $openssl_out;
$self->{_openssl_in} = $openssl_in;
$self->{_openssl_buf} = '';
$self->{_mux}->add($near);
$self->{_mux}->add($openssl_out);
}
sub _kill_openssl_child {
my ($self) = @_;
if ($self->{_openssl_pid}) {
kill(9, $self->{_openssl_pid});
waitpid($self->{_openssl_pid}, 0);
}
delete($self->{_openssl_pid});
delete($self->{_openssl_in});
delete($self->{_openssl_out});
}
sub DESTROY {
my ($self) = @_;
$self->_kill_openssl_child();
}
sub _openssl_input {
my ($self, $fh, $s_r, $peer) = @_;
if ($self->{_openssl_done}) {
$$s_r = '';
return;
}
$self->{_openssl_buf} .= $$s_r;
$$s_r = '';
if ($self->{_openssl_buf} =~ /Server certificate\n(-----BEGIN CERTIFICATE-----\n.*?\n-----END CERTIFICATE-----\n).*SRTP Extension negotiated, profile=(\S+).*Keying material: ([0-9a-fA-F]{120})/s) {
$self->{_peer_cert} = $1;
$self->{_profile} = $2;
$self->{_keys} = pack('H*', $3);
$self->{_connected} = 1;
$self->{_openssl_done} = 1;
}
if ($self->{_openssl_buf} =~ /\nDONE\n/s) {
$self->{_openssl_done} = 1;
}
}
sub _near_peer {
my ($self, $peer) = @_;
$self->{_near_peer} and return;
$self->{_near_peer} = $peer;
CORE::connect($self->{_near}, $self->{_near_peer});
}
sub _near_input {
my ($self, $fh, $s_r, $peer) = @_;
my $func = $self->{_output_func};
if (ref($func) eq 'CODE') {
$func->($self->{_tag}, $$s_r);
}
else {
# object
$func->dtls_send($self->{_tag}, $$s_r);
}
$self->_near_peer($peer);
$$s_r = '';
}
sub input {
my ($self, $fh, $s_r, $peer) = @_;
$$s_r eq '' and return;
if ($fh == $self->{_openssl_out}) { # openssl's stdout
return $self->_openssl_input($fh, $s_r, $peer);
}
elsif ($fh == $self->{_near}) { # UDP input from openssl - forward to peer
return $self->_near_input($fh, $s_r, $peer);
}
# UDP input from peer - demux and forward to openssl
is_dtls($$s_r) or return;
$self->{_near_peer} or return; # nowhere to forward it to
grep {$fh == $_} @{$self->{_local_sockets}} or return; # not one of ours
send($self->{_near}, $$s_r, 0);
$$s_r = '';
}
sub peer_cert {
my ($self) = @_;
$self->{_peer_cert_file} and return $self->{_peer_cert_file};
$self->{_peer_cert} or return;
my $cert_file = File::Temp->new();
print $cert_file $self->{_peer_cert};
close($cert_file);
return ($self->{_peer_cert_file} = $cert_file);
}
sub cert_fingerprint {
my ($cert_file) = @_;
my $fd;
open($fd, '-|', qw(openssl x509 -in), $cert_file->filename(), qw(-fingerprint -noout));
my $fp = <$fd>;
close($fd);
$fp =~ /SHA1 Fingerprint=([0-9a-f:]+)/i or return;
return $1;
}
sub fingerprint {
my ($self) = @_;
return cert_fingerprint($self->{_cert_key_file});
}
sub get_keys {
my ($self) = @_;
$self->{_keys} =~ /^(.{16})(.{16})(.{14})(.{14})$/s or return;
return ($self->{_profile}, $1, $2, $3, $4);
}
sub is_dtls {
my ($s) = @_;
length($s) < 1 and return 0;
my $c = ord(substr($s, 0, 1));
$c < 20 and return 0;
$c > 63 and return 0;
return 1;
}
sub encode {
my ($self) = @_;
my @ret;
push(@ret, 'a=setup:actpass');
push(@ret, 'a=fingerprint:sha-1 ' . $self->fingerprint());
return @ret;
}
package NGCP::Rtpclient::DTLS::Group;
sub new {
my ($class, $mux, $output_func, $socket_components, $cert) = @_;
my $self = [];
bless $self, $class;
my $max_component = $#{$socket_components};
for my $idx (0 .. $max_component) {
my $local_sockets = $socket_components->[$idx];
my $cl = NGCP::Rtpclient::DTLS->new($mux, $local_sockets, $output_func, $idx, $cert);
push(@$self, $cl);
$cert = $cl->get_cert();
}
return $self;
}
sub encode {
my ($self, @rest) = @_;
return $self->[0]->encode(@rest);
}
sub connect { ## no critic: Subroutines::ProhibitBuiltinHomonyms
my ($self, @rest) = @_;
for my $cl (@$self) {
$cl->accept(@rest);
}
}
sub accept { ## no critic: Subroutines::ProhibitBuiltinHomonyms
my ($self, @rest) = @_;
for my $cl (@$self) {
$cl->accept(@rest);
}
}
sub input {
my ($self, @rest) = @_;
for my $cl (@$self) {
$cl->input(@rest);
}
}
1;