1package OpenXPKI::Server::API2::Plugin::Cert::private_key; 2use OpenXPKI::Server::API2::EasyPlugin; 3 4=head1 NAME 5 6OpenXPKI::Server::API2::Plugin::Cert::private_key 7 8=cut 9 10# Project modules 11use OpenXPKI::Server::Context qw( CTX ); 12use OpenXPKI::Server::API2::Types; 13 14 15 16=head1 COMMANDS 17 18=head2 get_private_key_for_cert 19 20returns an ecrypted private key for a certificate if the private 21key was generated on the CA during the certificate request process. 22 23Parameters are the same as for I<convert_private_key> except that 24I<private_key> must not be passed but is read from the datapool and 25I<cert_identifier> is mandatory. 26 27=cut 28 29command "get_private_key_for_cert" => { 30 identifier => { isa => 'Base64', required => 1, }, 31 format => { isa => 'Str', matching => qr{ \A ( PKCS8_(PEM|DER) | OPENSSL_(PRIVKEY|RSA) | PKCS12 | JAVA_KEYSTORE ) \z }xms, required => 1, }, 32 password => { isa => 'Str', required => 1, }, 33 passout => { isa => 'Str', }, 34 nopassword => { isa => 'Bool', default => 0, }, 35 keeproot => { isa => 'Bool', default => 0, }, 36 alias => { isa => 'AlphaPunct', }, 37 csp => { isa => 'AlphaPunct', }, 38} => sub { 39 my ($self, $params) = @_; 40 41 my $identifier = $params->identifier; 42 my $nopassword = $params->nopassword; 43 44 if ($nopassword) { 45 CTX('log')->audit('key')->warn("private key export without password", { certid => $identifier }); 46 } else { 47 CTX('log')->audit('key')->info("private key export", { certid => $identifier }); 48 } 49 50 my $private_key = $self->get_private_key_from_db($identifier) 51 or OpenXPKI::Exception->throw( 52 message => 'I18N_OPENXPKI_SERVER_API_OBJECT_PRIVATE_KEY_NOT_FOUND_IN_DB', 53 params => { 'IDENTIFIER' => $identifier, }, 54 ); 55 56 $params->{private_key} = $private_key; 57 58 return $self->api->convert_private_key(%$params); 59 60}; 61 62 63=head2 convert_private_key 64 65expects a private key and converts it into another format. If a bundle 66with certificates is requested (PKCS12, JKS), the certificate to use as the 67end entity certificate must be given via I<identifier> or as first element of 68I<chain>. 69 70=over 71 72=item * format - the output format 73 74=over 75 76=item PKCS8_PEM (PKCS#8 in PEM format) 77 78=item PKCS8_DER (PKCS#8 in DER format) 79 80=item PKCS12 (PKCS#12 in DER format) 81 82=item OPENSSL_PRIVKEY (OpenSSL native key format in PEM) 83 84=item OPENSSL_RSA (OpenSSL RSA with DEK-Info Header) 85 86=item JAVA_KEYSTORE (JKS including chain). 87 88=back 89 90=item * password - the private key password 91 92Password that was used when the key was generated. 93 94=item * passout - the password for the exported key, default is PASSWORD 95 96The password to encrypt the exported key with, if empty the input password 97is used. 98 99This option is only supported with format OPENSSL_PRIVKEY, PKCS12 and JKS! 100 101=item * nopasswd 102 103If set to a true value, the B<key is exported without a password!>. 104You must also set passout to the empty string. 105 106=item * identifier 107 108the identifier of the certificate to merge into the export file. 109The output file will contain also certificates of the chain, with or 110without root weather I<keeproot> is set. 111Only used with JKS or PKCS12 export format. 112 113=item * keeproot 114 115Boolean, when set the root certifcate is included in the keystore. 116Only used when identifier is set to export PKCS12 or Java Keystore. 117 118=item * chain 119 120A PEM encoded list of certificates to be merged into the output file. 121Only used with JKS or PKCS12 export format, content is used "as is" and 122concatenated to the chain retrieved from I<identifier>/I<keeproot>. 123 124If I<identifier> is not set, the first certificate of the chain must match 125the private key. 126 127=item * alias 128 129String to set as alias for the key/certificate for JKS or PKCS12. 130 131=item * csp 132 133String, write name as a Microsoft CSP name (PKCS12 only) 134 135 136=back 137 138If the input password does not decrypt the private key, an exception is thrown. 139 140=cut 141 142command "convert_private_key" => { 143 144 private_key => { isa => 'PEMPKey', required => 1 }, 145 format => { isa => 'Str', matching => qr{ \A ( PKCS8_(PEM|DER) | OPENSSL_(PRIVKEY|RSA) | PKCS12 | JAVA_KEYSTORE ) \z }xms, required => 1, }, 146 password => { isa => 'Str', required => 1, }, 147 passout => { isa => 'Str', }, 148 nopassword => { isa => 'Bool', default => 0, }, 149 identifier => { isa => 'Base64' }, 150 chain => { isa => 'ArrayRefOrPEMCertChain', coerce => 1, }, 151 keeproot => { isa => 'Bool', default => 0, }, 152 alias => { isa => 'AlphaPunct', }, 153 csp => { isa => 'AlphaPunct', }, 154} => sub { 155 my ($self, $params) = @_; 156 157 my $identifier = $params->identifier || ''; 158 my $format = $params->format; 159 my $password = $params->password; 160 my $pass_out = $params->passout; 161 my $nopassword = $params->nopassword; 162 my $private_key = $params->private_key; 163 164 if ($nopassword and (!defined $pass_out or $pass_out ne '')) { 165 OpenXPKI::Exception->throw( 166 message => "Parameter 'passout' must be set to empty string if 'nopassword' is given" 167 ); 168 } 169 170 if ($nopassword) { 171 CTX('log')->audit('key')->warn("private key export without password"); 172 } 173 174 ##! 4: 'identifier: ' . $identifier 175 ##! 4: 'format: ' . $format 176 ##! 16: 'pkey ' . $private_key 177 178 # NB: The key in the database is in native openssl format 179 my $command_hashref; 180 181 if ( $format =~ /PKCS8_(PEM|DER)/ ) { 182 $format = $1; 183 $command_hashref = { 184 PASSWD => $password, 185 DATA => $private_key, 186 COMMAND => 'convert_pkcs8', 187 OUT => $format, 188 REVERSE => 1 189 }; 190 191 if ($nopassword) { 192 $command_hashref->{NOPASSWD} = 1; 193 } 194 elsif ($pass_out) { 195 $command_hashref->{OUT_PASSWD} = $pass_out; 196 } 197 } 198 elsif ( $format =~ /OPENSSL_(PRIVKEY|RSA)/ ) { 199 200 # we just need to spit out the blob from the database but we need to check 201 # if the password matches, so we do a 1:1 conversion 202 $command_hashref = { 203 PASSWD => $password, 204 DATA => $private_key, 205 COMMAND => 'convert_pkey', 206 }; 207 208 if ($format eq 'OPENSSL_RSA') { 209 $command_hashref->{KEYTYPE} = 'rsa'; 210 } 211 212 if ($nopassword) { 213 $command_hashref->{NOPASSWD} = 1; 214 } 215 elsif ($pass_out) { 216 $command_hashref->{OUT_PASSWD} = $pass_out; 217 } 218 219 } 220 elsif ( $format eq 'PKCS12' or $format eq 'JAVA_KEYSTORE' ) { 221 222 my @chain; 223 224 if ($identifier) { 225 ##! 16: 'identifier: ' . $identifier 226 @chain = $self->get_chain_certificates({ 227 'KEEPROOT' => $params->keeproot, 228 'IDENTIFIER' => $identifier, 229 'FORMAT' => 'PEM', 230 }); 231 } 232 233 if ($params->has_chain) { 234 push @chain, @{$params->chain}; 235 } 236 237 ##! 16: 'chain: ' . Dumper \@chain 238 OpenXPKI::Exception->throw( 239 message => 'private key export missing certificates', 240 params => { 'FORMAT' => $format }, 241 ) unless (scalar @chain); 242 243 # the first one is the entity certificate 244 my $certificate = shift @chain; 245 246 $command_hashref = { 247 COMMAND => 'create_pkcs12', 248 PASSWD => $password, 249 KEY => $private_key, 250 CERT => $certificate, 251 CHAIN => \@chain, 252 }; 253 254 if ($nopassword) { 255 $command_hashref->{NOPASSWD} = 1; 256 } 257 elsif ($pass_out) { 258 $command_hashref->{PKCS12_PASSWD} = $pass_out; 259 # set password for JKS export 260 $password = $pass_out; 261 } 262 263 if ($params->has_csp) { 264 $command_hashref->{CSP} = $params->csp; 265 } 266 267 if ($params->has_alias) { 268 $command_hashref->{ALIAS} = $params->alias; 269 } 270 elsif ( $format eq 'JAVA_KEYSTORE' ) { 271 # Java Keystore only: if no alias is specified, set to 'key' 272 $command_hashref->{ALIAS} = 'key'; 273 } 274 275 } 276 277 my $result; 278 eval { 279 $result = $self->api->get_default_token()->command($command_hashref); 280 }; 281 if (!$result) { 282 OpenXPKI::Exception->throw( 283 message => 'I18N_OPENXPKI_SERVER_API_OBJECT_UNABLE_EXPORT_KEY', 284 params => { 'IDENTIFIER' => $identifier, ERROR => $? }, 285 ); 286 } 287 288 if ( $format eq 'JAVA_KEYSTORE' ) { 289 my $token = CTX('crypto_layer')->get_system_token({ TYPE => 'javaks' }); 290 291 my $pkcs12 = $result; 292 $result = $token->command({ 293 COMMAND => 'create_keystore', 294 PKCS12 => $pkcs12, 295 PASSWD => $password, 296 OUT_PASSWD => $password, 297 }); 298 } 299 300 return $result; 301}; 302 303=head2 private_key_exists_for_cert 304 305Checks whether a corresponding CA-generated private key exists for 306the given certificate identifier (named parameter IDENTIFIER). 307Returns true if there is a private key, false otherwise. 308 309=cut 310command "private_key_exists_for_cert" => { 311 identifier => { isa => 'Base64', required => 1, }, 312} => sub { 313 my ($self, $params) = @_; 314 315 my $privkey = $self->get_private_key_from_db($params->identifier, 1); 316 return ( defined $privkey ); 317}; 318 319 320 321=head1 METHODS 322 323=head2 get_private_key_from_db 324 325Gets a private key from the database for a given certificate 326identifier by looking up the CSR serial of the certificate and 327extracting the private_key from the datapool. Returns undef if 328no key is available. 329 330=cut 331 332sub get_private_key_from_db { 333 334 my ($self, $cert_identifier, $check_only) = @_; 335 336 my $datapool = $self->api->get_data_pool_entry( 337 namespace => 'certificate.privatekey', 338 key => $cert_identifier, 339 decrypt => not $check_only, 340 ); 341 342 # we also use the option to store the private key using the key identifier 343 # instead of the certificate identifier. We leave it to the workflows to 344 # take care that the mapping is unique, as the datapool has a unique index 345 # on the relevant colums there is no risk that it breaks at this stage 346 347 if (!$datapool) { 348 349 ##! 2: "Fetching certificate from database" 350 my $cert = CTX('dbi')->select_one( 351 columns => [ 'subject_key_identifier' ], 352 from => 'certificate', 353 where => { 'identifier' => $cert_identifier }, 354 ); 355 356 $datapool = $self->api->get_data_pool_entry( 357 namespace => 'certificate.privatekey', 358 key => $cert->{subject_key_identifier}, 359 decrypt => not $check_only, 360 ); 361 362 } 363 364 if ($datapool) { 365 return $datapool->{value}; 366 } 367 else { 368 return; 369 } 370} 371 372sub get_chain_certificates { 373 my ($self, $args) = @_; 374 ##! 4: Dumper $args 375 my $id = $args->{IDENTIFIER}; 376 my $format = $args->{FORMAT}; 377 378 my $chain_ref = $self->api->get_chain(start_with => $id, format => $format); 379 380 my @chain = @{ $chain_ref->{certificates} }; 381 ##! 16: 'Chain ' . Dumper $chain_ref 382 383 # pop off root certificates 384 if ( $chain_ref->{complete} and scalar @chain > 1 and !$args->{KEEPROOT} ) { 385 pop @chain; # we don't need the first element 386 } 387 ##! 1: 'end' 388 return @chain; 389} 390 391__PACKAGE__->meta->make_immutable; 392