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