Skip to content

Commit

Permalink
Add secret transfer for cluster environments
Browse files Browse the repository at this point in the history
  • Loading branch information
oliwel committed Jun 14, 2020
1 parent 4d1a6d9 commit 328823b
Showing 1 changed file with 297 additions and 10 deletions.
307 changes: 297 additions & 10 deletions core/server/OpenXPKI/Crypto/TokenManager.pm
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ use OpenXPKI::Exception;
use OpenXPKI::Server::Context qw( CTX );
use Data::Dumper;
use English;
use Crypt::PK::ECC;
use Crypt::Argon2;
use Digest::SHA qw(sha256_hex sha1_base64);
use OpenXPKI::Crypto::Backend::API;
use OpenXPKI::Crypto::Tool::SCEP::API;
use OpenXPKI::Crypto::Tool::LibSCEP::API;
use OpenXPKI::Crypto::Tool::CreateJavaKeystore::API;
use OpenXPKI::Crypto::VolatileVault;
use OpenXPKI::Crypto::Secret;
use OpenXPKI::Serialization::Simple;

=head1 Name
Expand Down Expand Up @@ -151,7 +155,7 @@ sub __load_secret
$secret->{realm} = $realm;
}

##! 32: $secret_config
##! 32: $secret
$self->__init_secret( $secret );

OpenXPKI::Exception->throw (
Expand Down Expand Up @@ -223,7 +227,8 @@ sub __init_secret {

=head2 __set_secret_from_cache ()
Load all secrets in the current realm from the cache
Try to find the secret groups value in the cache, returns true if the secret
could be restored.
=cut

Expand All @@ -250,13 +255,17 @@ sub __set_secret_from_cache {

} elsif ($cache_config eq "daemon") {
## daemon mode
##! 4: "let's get the serialized secret from the database"
# in cluster mode or after unclean shutdown we might have items with
# the same group name that we can not read so we add the VV key id
my $group_id = sprintf("%s:%s", CTX('volatile_vault')->get_key_id(), $group);

##! 4: "let's get the serialized secret from the database ($group_id)"
my $row = CTX('dbi')->select_one(
from => "secret",
columns => [ 'data' ],
where => {
pki_realm => $self->{SECRET}->{$realm}->{$group}->{realm},
group_id => $group,
group_id => $group_id,
}
);
$secret = $row->{data};
Expand All @@ -271,13 +280,13 @@ sub __set_secret_from_cache {
});
}

##! 16: 'secret: ' . $secret
if (defined $secret and length $secret)
{
##! 16: 'setting serialized secret'
$self->{SECRET}->{$realm}->{$group}->{ref}->set_serialized ($secret);
##! 16: 'blob is: ' . $secret
return $self->{SECRET}->{$realm}->{$group}->{ref}->set_serialized ($secret);
}
return 1;
return;
}

=head2 get_secret_groups
Expand Down Expand Up @@ -414,13 +423,14 @@ sub set_secret_group_part
CTX('session')->data->secret(group => $group, value => $secret);
CTX('session')->persist;
} else {
my $group_id = sprintf("%s:%s", CTX('volatile_vault')->get_key_id(), $group);
##! 4: "merge secret into database"
CTX('dbi')->merge(
into => "secret",
set => { data => $secret },
where => {
pki_realm => $obj->{realm},
group_id => $group,
group_id => $group_id,
},
);
}
Expand Down Expand Up @@ -452,11 +462,12 @@ sub clear_secret_group
}
else {
##! 4: "delete secret in database"
my $group_id = sprintf("%s:%s", CTX('volatile_vault')->get_key_id(), $group);
my $result = CTX('dbi')->delete(
from => "secret",
where => {
pki_realm => $self->{SECRET}->{$realm}->{$group}->{realm},
group_id => $group,
group_id => $group_id,
}
);
OpenXPKI::Control::reload();
Expand All @@ -470,6 +481,283 @@ sub clear_secret_group
return 1;
}

=head2 request_secret_transfer
Initialize a secret transfer to the current node. Creates a keypair for
negotiation of the transfer secret and writes placeholder items for this key
into the secret table
=cut

sub request_secret_transfer {

my $self = shift;
my $realm = CTX('session')->data->pki_realm;

my @groups;
foreach my $group (keys %{$self->get_secret_groups()}) {
##! 32: "Check if group $group is complete"
if (!$self->is_secret_group_complete($group)) {
push @groups, $group;
}
}

# no secret groups to load
if (scalar @groups == 0) {
##! 16: 'No groups to load'
return;
}
##! 8: "Requesting secret transfer for " . join(", ", @groups)

# generate our half of the key
my $priv = Crypt::PK::ECC->new();
$priv->generate_key('secp256r1');

my $key_id = substr(sha1_base64($priv->export_key_der('public')), 0, 8);
##! 16: "key_id for transfer $key_id"

# we store our transfer key as secret
my $group_id = $key_id;
$self->{SECRET}->{$realm}->{$group_id} = {
label => 'Cluster Transfer',
type => 'Plain',
cache => 'daemon',
realm => $realm,
ref => OpenXPKI::Crypto::Secret->new ({
TYPE => "Plain", PARTS => 1
}),
};

# this persists the private key in the secret cache so we can load it later
$self->set_secret_group_part( { GROUP => $group_id,
VALUE => $priv->export_key_pem('private') });

# create an item in the secret cache table for each secret we expect
# group name is key_id (transfer) + secert group name, value is empty
foreach my $group (@groups) {
##! 4: "insert placeholder into database"
CTX('dbi')->insert(
into => "secret",
values => {
pki_realm => $realm,
group_id => join(":", ($key_id, $group)),
data => undef,
},
);
}

return {
transfer_id => $key_id,
pubkey => $priv->export_key_pem('public'),
groups => \@groups,
}
}

=head2 transfer_secret_groups ( PUBKEY )
Needs to be executed on the node that already has established its secret.
Expects the public key created by I<request_secret_transfer> as parameter and
tries to fill the rows in the secret table assigned to this transfer key.
=cut

sub transfer_secret_groups {

##! 1: "start"
my $self = shift;
my $pubkey = shift;

# create ecc pubkey object for secret sharing
my $pub = Crypt::PK::ECC->new( \$pubkey );
my $key_id = substr(sha1_base64($pub->export_key_der('public')), 0, 8);

my $realm = CTX('session')->data->pki_realm;

my $db_groups = CTX('dbi')->select(
from => "secret",
columns => [ 'group_id' ],
where => {
pki_realm => $realm,
group_id => { -like => "$key_id:%" },
data => undef,
}
)->fetchall_arrayref([0]);

##! 64: $db_groups

my @groups = map { my ($k,$g) = split /:/, $_->[0]; $g; } @{$db_groups};

# nothing to do
if (@groups == 0) {
return;
}

# generate our half of the key
my $priv = Crypt::PK::ECC->new();
$priv->generate_key('secp256r1');

my $vv = OpenXPKI::Crypto::VolatileVault->new({
TOKEN => CTX('api2')->get_default_token(),
KEY => uc(sha256_hex($priv->shared_secret( $pub ))),
IV => undef
});
##! 32: 'Transfer vault key id id ' . $vv->get_key_id()

my @transfered;
my @incomplete;
# loop though the group list
foreach my $group_id (@groups) {
# if the secret was not unlocked we can not export it
if (!$self->is_secret_group_complete($group_id)) {
push @incomplete, $group_id;
next;
}

my $secret = $self->{SECRET}->{$realm}->{$group_id}->{ref}->get_secret();
CTX('dbi')->update(
table => "secret",
set => { data => $vv->encrypt( $secret ) },
where => {
pki_realm => $realm,
group_id => "$key_id:$group_id",
}
);
push @transfered, $group_id;
}

return {
transfer_id => $key_id,
pubkey => $priv->export_key_pem('public'),
transfered => \@transfered,
incomplete => \@incomplete,
}
}


=head2 accept_secret_transfer ( ID, PUBKEY )
Needs to be executed on the receiving node, expects the transfer_id and the
public key generated by the donating node by I<transfer_secret_groups>.
Transfers the exported secrets from the transfer pool into the secret cache
so it can be used by all childs of this node.
=cut

sub accept_secret_transfer {

##! 1: "start"
my $self = shift;
my $transfer_id = shift;
my $pubkey = shift;

my $realm = CTX('session')->data->pki_realm;

# pubkey of the sender
my $pub = Crypt::PK::ECC->new( \$pubkey );

# in case this is a different child process we need to restore the secret
# the group id is the transfer_id (=key_id)
if (!exists $self->{SECRET}->{$realm}->{$transfer_id}) {
##! 8: "Restore private key from secret cache"
$self->{SECRET}->{$realm}->{$transfer_id} = {
label => 'Cluster Transfer',
type => 'Plain',
cache => 'daemon',
realm => $realm,
ref => OpenXPKI::Crypto::Secret->new ({
TYPE => "Plain", PARTS => 1,
}),
};
$self->__set_secret_from_cache({
GROUP => $transfer_id,
});
}
# load the private key from the secret vault
my $privkey = $self->{SECRET}->{$realm}->{$transfer_id}->{ref}->get_secret();
if (!$privkey) {
OpenXPKI::Exception->throw(
message => "Unable to restore transfer key"
);
}

my $priv = Crypt::PK::ECC->new( \$privkey );
# validate if this is the right key
if (substr(sha1_base64($priv->export_key_der('public')), 0, 8)
ne $transfer_id) {
OpenXPKI::Exception->throw(
message => "Transfer id does not match key"
);
}

# create volatile vault using shared secret
my $vv = OpenXPKI::Crypto::VolatileVault->new({
TOKEN => CTX('api2')->get_default_token(),
KEY => uc(sha256_hex($priv->shared_secret( $pub ))),
IV => undef
});

##! 32: 'Transfer vault key id id ' . $vv->get_key_id()
my $db_groups = CTX('dbi')->select(
from => "secret",
columns => [ 'group_id', 'data' ],
where => {
pki_realm => $realm,
group_id => { -like => "$transfer_id:%" },
data => {"!=", undef },
}
)->fetchall_arrayref([]);

##! 64: $db_groups

# nothing to do
if (@${db_groups} == 0) {
return;
}

my @complete;
# loop though the group list
foreach my $row (@{$db_groups}) {

my $db_groups = CTX('dbi')->delete(
from => "secret",
where => {
pki_realm => $realm,
group_id => $row->[0],
}
);

##! 32: $row
my ($k, $group_id) = split /:/, $row->[0];

# already set - dont touch
if ($self->is_secret_group_complete($group_id)) {
##! 16: "Group $group_id is already unlocked - skipping"
next;
}

if (!$vv->can_decrypt( $row->[1] )) {
##! 16: "Group $group_id was not encrypted with this vault"
next;
}

my $secret = $vv->decrypt( $row->[1] );
if (!$secret) {
OpenXPKI::Exception->throw (
message => "Unable to decrypt secret from transport encryption",
);
}
##! 8: "Unlock group $group_id from transfer"
$self->set_secret_group_part({ GROUP => $group_id, VALUE => $secret });
push @complete, $group_id;

}

return {
transfer_id => $transfer_id,
complete => \@complete,
};
}

######################################################################
## slot management ##
######################################################################
Expand Down Expand Up @@ -711,4 +999,3 @@ sub __use_token {

1;
__END__

0 comments on commit 328823b

Please sign in to comment.