/src/strongswan/src/libstrongswan/plugins/pem/pem_builder.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2013 Tobias Brunner |
3 | | * Copyright (C) 2009 Martin Willi |
4 | | * Copyright (C) 2001-2008 Andreas Steffen |
5 | | * |
6 | | * Copyright (C) secunet Security Networks AG |
7 | | * |
8 | | * This program is free software; you can redistribute it and/or modify it |
9 | | * under the terms of the GNU General Public License as published by the |
10 | | * Free Software Foundation; either version 2 of the License, or (at your |
11 | | * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. |
12 | | * |
13 | | * This program is distributed in the hope that it will be useful, but |
14 | | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
15 | | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
16 | | * for more details. |
17 | | */ |
18 | | |
19 | | #include "pem_builder.h" |
20 | | |
21 | | #include <stdio.h> |
22 | | #include <stdlib.h> |
23 | | #include <unistd.h> |
24 | | #include <errno.h> |
25 | | #include <string.h> |
26 | | #include <stddef.h> |
27 | | #include <fcntl.h> |
28 | | #include <sys/types.h> |
29 | | #include <sys/stat.h> |
30 | | |
31 | | #include <utils/debug.h> |
32 | | #include <library.h> |
33 | | #include <utils/lexparser.h> |
34 | | #include <asn1/asn1.h> |
35 | | #include <crypto/hashers/hasher.h> |
36 | | #include <crypto/crypters/crypter.h> |
37 | | #include <credentials/certificates/x509.h> |
38 | | |
39 | 0 | #define PKCS5_SALT_LEN 8 /* bytes */ |
40 | | |
41 | | /** |
42 | | * check the presence of a pattern in a character string, skip if found |
43 | | */ |
44 | | static bool present(char* pattern, chunk_t* ch) |
45 | 12.2M | { |
46 | 12.2M | u_int len = strlen(pattern); |
47 | | |
48 | 12.2M | if (ch->len >= len && strneq(ch->ptr, pattern, len)) |
49 | 125k | { |
50 | 125k | *ch = chunk_skip(*ch, len); |
51 | 125k | return TRUE; |
52 | 125k | } |
53 | 12.1M | return FALSE; |
54 | 12.2M | } |
55 | | |
56 | | /** |
57 | | * find a boundary of the form -----tag name----- |
58 | | */ |
59 | | static bool find_boundary(char* tag, chunk_t *line) |
60 | 1.59M | { |
61 | 1.59M | chunk_t name = chunk_empty; |
62 | | |
63 | 1.59M | if (!present("-----", line) || |
64 | 94.7k | !present(tag, line) || |
65 | 22.6k | !line->len || *line->ptr != ' ') |
66 | 1.57M | { |
67 | 1.57M | return FALSE; |
68 | 1.57M | } |
69 | 17.6k | *line = chunk_skip(*line, 1); |
70 | | |
71 | | /* extract name */ |
72 | 17.6k | name.ptr = line->ptr; |
73 | 10.6M | while (line->len > 0) |
74 | 10.6M | { |
75 | 10.6M | if (present("-----", line)) |
76 | 7.99k | { |
77 | 7.99k | DBG2(DBG_ASN, " -----%s %.*s-----", tag, (int)name.len, name.ptr); |
78 | 7.99k | return TRUE; |
79 | 7.99k | } |
80 | 10.5M | line->ptr++; line->len--; name.len++; |
81 | 10.5M | } |
82 | 9.64k | return FALSE; |
83 | 17.6k | } |
84 | | |
85 | | /* |
86 | | * decrypts a passphrase protected encrypted data block |
87 | | */ |
88 | | static status_t pem_decrypt(chunk_t *blob, encryption_algorithm_t alg, |
89 | | size_t key_size, chunk_t iv, chunk_t passphrase) |
90 | 0 | { |
91 | 0 | hasher_t *hasher; |
92 | 0 | crypter_t *crypter; |
93 | 0 | chunk_t salt = { iv.ptr, PKCS5_SALT_LEN }; |
94 | 0 | chunk_t hash; |
95 | 0 | chunk_t decrypted; |
96 | 0 | chunk_t key = {alloca(key_size), key_size}; |
97 | 0 | uint8_t padding, *last_padding_pos, *first_padding_pos; |
98 | 0 | size_t block_size; |
99 | |
|
100 | 0 | if (!key_size) |
101 | 0 | { |
102 | 0 | return FAILED; |
103 | 0 | } |
104 | | |
105 | | /* build key from passphrase and IV */ |
106 | 0 | hasher = lib->crypto->create_hasher(lib->crypto, HASH_MD5); |
107 | 0 | if (hasher == NULL) |
108 | 0 | { |
109 | 0 | DBG1(DBG_ASN, " MD5 hash algorithm not available"); |
110 | 0 | return NOT_SUPPORTED; |
111 | 0 | } |
112 | 0 | hash.len = hasher->get_hash_size(hasher); |
113 | 0 | hash.ptr = alloca(hash.len); |
114 | 0 | if (!hasher->get_hash(hasher, passphrase, NULL) || |
115 | 0 | !hasher->get_hash(hasher, salt, hash.ptr)) |
116 | 0 | { |
117 | 0 | hasher->destroy(hasher); |
118 | 0 | return FAILED; |
119 | 0 | } |
120 | 0 | memcpy(key.ptr, hash.ptr, hash.len); |
121 | |
|
122 | 0 | if (key.len > hash.len) |
123 | 0 | { |
124 | 0 | if (!hasher->get_hash(hasher, hash, NULL) || |
125 | 0 | !hasher->get_hash(hasher, passphrase, NULL) || |
126 | 0 | !hasher->get_hash(hasher, salt, hash.ptr)) |
127 | 0 | { |
128 | 0 | hasher->destroy(hasher); |
129 | 0 | return FAILED; |
130 | 0 | } |
131 | 0 | memcpy(key.ptr + hash.len, hash.ptr, key.len - hash.len); |
132 | 0 | } |
133 | 0 | hasher->destroy(hasher); |
134 | | |
135 | | /* decrypt blob */ |
136 | 0 | crypter = lib->crypto->create_crypter(lib->crypto, alg, key_size); |
137 | 0 | if (crypter == NULL) |
138 | 0 | { |
139 | 0 | DBG1(DBG_ASN, " %N encryption algorithm not available", |
140 | 0 | encryption_algorithm_names, alg); |
141 | 0 | memwipe(key.ptr, key.len); |
142 | 0 | return NOT_SUPPORTED; |
143 | 0 | } |
144 | | |
145 | 0 | block_size = crypter->get_block_size(crypter); |
146 | 0 | if (iv.len != crypter->get_iv_size(crypter) || blob->len % block_size) |
147 | 0 | { |
148 | 0 | DBG1(DBG_ASN, " data size is not multiple of block size"); |
149 | 0 | crypter->destroy(crypter); |
150 | 0 | memwipe(key.ptr, key.len); |
151 | 0 | return PARSE_ERROR; |
152 | 0 | } |
153 | 0 | if (!crypter->set_key(crypter, key) || |
154 | 0 | !crypter->decrypt(crypter, *blob, iv, &decrypted)) |
155 | 0 | { |
156 | 0 | crypter->destroy(crypter); |
157 | 0 | memwipe(key.ptr, key.len); |
158 | 0 | return FAILED; |
159 | 0 | } |
160 | 0 | crypter->destroy(crypter); |
161 | 0 | memcpy(blob->ptr, decrypted.ptr, blob->len); |
162 | 0 | chunk_clear(&decrypted); |
163 | 0 | memwipe(key.ptr, key.len); |
164 | | |
165 | | /* determine amount of padding */ |
166 | 0 | last_padding_pos = blob->ptr + blob->len - 1; |
167 | 0 | padding = *last_padding_pos; |
168 | 0 | if (!padding || padding > block_size) |
169 | 0 | { |
170 | 0 | DBG1(DBG_ASN, " invalid passphrase"); |
171 | 0 | return INVALID_ARG; |
172 | 0 | } |
173 | 0 | first_padding_pos = last_padding_pos - padding; |
174 | | /* check the padding pattern */ |
175 | 0 | while (--last_padding_pos > first_padding_pos) |
176 | 0 | { |
177 | 0 | if (*last_padding_pos != padding) |
178 | 0 | { |
179 | 0 | DBG1(DBG_ASN, " invalid passphrase"); |
180 | 0 | return INVALID_ARG; |
181 | 0 | } |
182 | 0 | } |
183 | | /* remove padding */ |
184 | 0 | blob->len -= padding; |
185 | 0 | return SUCCESS; |
186 | 0 | } |
187 | | |
188 | | /** |
189 | | * Converts a PEM encoded file into its binary form (RFC 1421, RFC 934) |
190 | | */ |
191 | | static status_t pem_to_bin(chunk_t *blob, bool *pgp) |
192 | 11.5k | { |
193 | 11.5k | typedef enum { |
194 | 11.5k | PEM_PRE = 0, |
195 | 11.5k | PEM_MSG = 1, |
196 | 11.5k | PEM_HEADER = 2, |
197 | 11.5k | PEM_BODY = 3, |
198 | 11.5k | PEM_POST = 4, |
199 | 11.5k | PEM_ABORT = 5 |
200 | 11.5k | } state_t; |
201 | | |
202 | 11.5k | encryption_algorithm_t alg = ENCR_UNDEFINED; |
203 | 11.5k | size_t key_size = 0; |
204 | 11.5k | bool encrypted = FALSE; |
205 | 11.5k | state_t state = PEM_PRE; |
206 | 11.5k | chunk_t src = *blob; |
207 | 11.5k | chunk_t dst = *blob; |
208 | 11.5k | chunk_t line = chunk_empty; |
209 | 11.5k | chunk_t iv = chunk_empty; |
210 | 11.5k | u_char iv_buf[HASH_SIZE_MD5]; |
211 | 11.5k | status_t status = NOT_FOUND; |
212 | 11.5k | enumerator_t *enumerator; |
213 | 11.5k | shared_key_t *shared; |
214 | | |
215 | 11.5k | dst.len = 0; |
216 | 11.5k | iv.ptr = iv_buf; |
217 | 11.5k | iv.len = 0; |
218 | | |
219 | 1.60M | while (fetchline(&src, &line)) |
220 | 1.59M | { |
221 | 1.59M | if (state == PEM_PRE) |
222 | 727k | { |
223 | 727k | if (find_boundary("BEGIN", &line)) |
224 | 7.28k | { |
225 | 7.28k | state = PEM_MSG; |
226 | 7.28k | } |
227 | 727k | continue; |
228 | 727k | } |
229 | 865k | else |
230 | 865k | { |
231 | 865k | if (find_boundary("END", &line)) |
232 | 714 | { |
233 | 714 | state = PEM_POST; |
234 | 714 | break; |
235 | 714 | } |
236 | 864k | if (state == PEM_MSG) |
237 | 6.66k | { |
238 | 6.66k | state = PEM_HEADER; |
239 | 6.66k | if (memchr(line.ptr, ':', line.len) == NULL) |
240 | 2.06k | { |
241 | 2.06k | state = PEM_BODY; |
242 | 2.06k | } |
243 | 6.66k | } |
244 | 864k | if (state == PEM_HEADER) |
245 | 225k | { |
246 | 225k | err_t ugh = NULL; |
247 | 225k | chunk_t name = chunk_empty; |
248 | 225k | chunk_t value = chunk_empty; |
249 | | |
250 | | /* an empty line separates HEADER and BODY */ |
251 | 225k | if (line.len == 0) |
252 | 291 | { |
253 | 291 | state = PEM_BODY; |
254 | 291 | continue; |
255 | 291 | } |
256 | | |
257 | | /* we are looking for a parameter: value pair */ |
258 | 225k | DBG2(DBG_ASN, " %.*s", (int)line.len, line.ptr); |
259 | 225k | ugh = extract_parameter_value(&name, &value, &line); |
260 | 225k | if (ugh != NULL) |
261 | 72.5k | { |
262 | 72.5k | continue; |
263 | 72.5k | } |
264 | 152k | if (match("Proc-Type", &name) && value.len && *value.ptr == '4') |
265 | 10.2k | { |
266 | 10.2k | encrypted = TRUE; |
267 | 10.2k | } |
268 | 142k | else if (match("DEK-Info", &name)) |
269 | 25.3k | { |
270 | 25.3k | chunk_t dek; |
271 | | |
272 | 25.3k | if (!extract_token(&dek, ',', &value)) |
273 | 7.44k | { |
274 | 7.44k | dek = value; |
275 | 7.44k | } |
276 | 25.3k | if (match("DES-EDE3-CBC", &dek)) |
277 | 6.67k | { |
278 | 6.67k | alg = ENCR_3DES; |
279 | 6.67k | key_size = 24; |
280 | 6.67k | } |
281 | 18.7k | else if (match("AES-128-CBC", &dek)) |
282 | 3.92k | { |
283 | 3.92k | alg = ENCR_AES_CBC; |
284 | 3.92k | key_size = 16; |
285 | 3.92k | } |
286 | 14.8k | else if (match("AES-192-CBC", &dek)) |
287 | 3.38k | { |
288 | 3.38k | alg = ENCR_AES_CBC; |
289 | 3.38k | key_size = 24; |
290 | 3.38k | } |
291 | 11.4k | else if (match("AES-256-CBC", &dek)) |
292 | 10.9k | { |
293 | 10.9k | alg = ENCR_AES_CBC; |
294 | 10.9k | key_size = 32; |
295 | 10.9k | } |
296 | 495 | else |
297 | 495 | { |
298 | 495 | DBG1(DBG_ASN, " encryption algorithm '%.*s'" |
299 | 495 | " not supported", (int)dek.len, dek.ptr); |
300 | 495 | return NOT_SUPPORTED; |
301 | 495 | } |
302 | 24.9k | if (!eat_whitespace(&value) || value.len > 2*sizeof(iv_buf)) |
303 | 361 | { |
304 | 361 | return PARSE_ERROR; |
305 | 361 | } |
306 | 24.5k | iv = chunk_from_hex(value, iv_buf); |
307 | 24.5k | } |
308 | 152k | } |
309 | 639k | else /* state is PEM_BODY */ |
310 | 639k | { |
311 | 639k | chunk_t data; |
312 | | |
313 | | /* remove any trailing whitespace */ |
314 | 639k | if (!extract_token(&data ,' ', &line)) |
315 | 498k | { |
316 | 498k | data = line; |
317 | 498k | } |
318 | | |
319 | | /* check for PGP armor checksum */ |
320 | 639k | if (data.len && *data.ptr == '=') |
321 | 4.18k | { |
322 | 4.18k | *pgp = TRUE; |
323 | 4.18k | data.ptr++; |
324 | 4.18k | data.len--; |
325 | 4.18k | DBG2(DBG_ASN, " armor checksum: %.*s", (int)data.len, |
326 | 4.18k | data.ptr); |
327 | 4.18k | continue; |
328 | 4.18k | } |
329 | | |
330 | 635k | if (blob->len - dst.len < data.len / 4 * 3) |
331 | 0 | { |
332 | 0 | state = PEM_ABORT; |
333 | 0 | } |
334 | 635k | data = chunk_from_base64(data, dst.ptr); |
335 | | |
336 | 635k | dst.ptr += data.len; |
337 | 635k | dst.len += data.len; |
338 | 635k | } |
339 | 864k | } |
340 | 1.59M | } |
341 | | /* set length to size of binary blob */ |
342 | 10.6k | blob->len = dst.len; |
343 | | |
344 | 10.6k | if (state != PEM_POST || !blob->len) |
345 | 9.98k | { |
346 | 9.98k | DBG1(DBG_LIB, " file coded in unknown format, discarded"); |
347 | 9.98k | return PARSE_ERROR; |
348 | 9.98k | } |
349 | 691 | if (!encrypted) |
350 | 660 | { |
351 | 660 | return SUCCESS; |
352 | 660 | } |
353 | 31 | if (alg == ENCR_UNDEFINED || !key_size) |
354 | 13 | { |
355 | 13 | DBG1(DBG_ASN, " encrypted PEM file has no valid DEK-Info"); |
356 | 13 | return PARSE_ERROR; |
357 | 13 | } |
358 | | |
359 | 18 | enumerator = lib->credmgr->create_shared_enumerator(lib->credmgr, |
360 | 18 | SHARED_PRIVATE_KEY_PASS, NULL, NULL); |
361 | 18 | while (enumerator->enumerate(enumerator, &shared, NULL, NULL)) |
362 | 0 | { |
363 | 0 | chunk_t passphrase, chunk; |
364 | |
|
365 | 0 | passphrase = shared->get_key(shared); |
366 | 0 | chunk = chunk_clone(*blob); |
367 | 0 | status = pem_decrypt(&chunk, alg, key_size, iv, passphrase); |
368 | 0 | if (status == SUCCESS) |
369 | 0 | { |
370 | 0 | memcpy(blob->ptr, chunk.ptr, chunk.len); |
371 | 0 | blob->len = chunk.len; |
372 | 0 | } |
373 | 0 | chunk_clear(&chunk); |
374 | 0 | if (status != INVALID_ARG) |
375 | 0 | { /* try again only if passphrase invalid */ |
376 | 0 | break; |
377 | 0 | } |
378 | 0 | } |
379 | 18 | enumerator->destroy(enumerator); |
380 | 18 | return status; |
381 | 31 | } |
382 | | |
383 | | /** |
384 | | * Check if a blob looks like an ASN1 SEQUENCE or SET with BER indefinite length |
385 | | */ |
386 | | static bool is_ber_indefinite_length(chunk_t blob) |
387 | 24.6k | { |
388 | 24.6k | if (blob.len >= 4) |
389 | 24.0k | { |
390 | 24.0k | switch (blob.ptr[0]) |
391 | 24.0k | { |
392 | 14.5k | case ASN1_SEQUENCE: |
393 | 14.9k | case ASN1_SET: |
394 | | /* BER indefinite length uses 0x80, and is terminated with |
395 | | * end-of-content using 0x00,0x00 */ |
396 | 14.9k | return blob.ptr[1] == 0x80 && |
397 | 665 | blob.ptr[blob.len - 2] == 0 && |
398 | 586 | blob.ptr[blob.len - 1] == 0; |
399 | 9.06k | default: |
400 | 9.06k | break; |
401 | 24.0k | } |
402 | 24.0k | } |
403 | 9.64k | return FALSE; |
404 | 24.6k | } |
405 | | |
406 | | /** |
407 | | * load the credential from a blob |
408 | | */ |
409 | | static void *load_from_blob(chunk_t blob, credential_type_t type, int subtype, |
410 | | identification_t *subject, x509_flag_t flags) |
411 | 24.6k | { |
412 | 24.6k | void *cred = NULL; |
413 | 24.6k | bool pgp = FALSE; |
414 | | |
415 | 24.6k | blob = chunk_clone(blob); |
416 | 24.6k | if (!is_ber_indefinite_length(blob) && !is_asn1(blob)) |
417 | 11.5k | { |
418 | 11.5k | if (pem_to_bin(&blob, &pgp) != SUCCESS) |
419 | 10.8k | { |
420 | 10.8k | chunk_clear(&blob); |
421 | 10.8k | return NULL; |
422 | 10.8k | } |
423 | 660 | if (pgp && type == CRED_PRIVATE_KEY) |
424 | 0 | { |
425 | | /* PGP encoded keys are parsed with a KEY_ANY key type, as it |
426 | | * can contain any type of key. However, ipsec.secrets uses |
427 | | * RSA for PGP keys, which is actually wrong. */ |
428 | 0 | subtype = KEY_ANY; |
429 | 0 | } |
430 | 660 | } |
431 | | /* if CERT_ANY is given, ASN1 encoded blob is handled as X509 */ |
432 | 13.7k | if (type == CRED_CERTIFICATE && subtype == CERT_ANY) |
433 | 0 | { |
434 | 0 | subtype = pgp ? CERT_GPG : CERT_X509; |
435 | 0 | } |
436 | 13.7k | if (type == CRED_CERTIFICATE && subtype == CERT_TRUSTED_PUBKEY && subject) |
437 | 0 | { |
438 | 0 | cred = lib->creds->create(lib->creds, type, subtype, |
439 | 0 | BUILD_BLOB_ASN1_DER, blob, BUILD_SUBJECT, subject, |
440 | 0 | BUILD_END); |
441 | 0 | } |
442 | 13.7k | else |
443 | 13.7k | { |
444 | 13.7k | cred = lib->creds->create(lib->creds, type, subtype, |
445 | 13.7k | pgp ? BUILD_BLOB_PGP : BUILD_BLOB_ASN1_DER, blob, |
446 | 13.7k | flags ? BUILD_X509_FLAG : BUILD_END, |
447 | 13.7k | flags, BUILD_END); |
448 | 13.7k | } |
449 | 13.7k | chunk_clear(&blob); |
450 | 13.7k | return cred; |
451 | 24.6k | } |
452 | | |
453 | | /** |
454 | | * load the credential from a file |
455 | | */ |
456 | | static void *load_from_file(char *file, credential_type_t type, int subtype, |
457 | | identification_t *subject, x509_flag_t flags) |
458 | 0 | { |
459 | 0 | void *cred; |
460 | 0 | chunk_t *chunk; |
461 | |
|
462 | 0 | chunk = chunk_map(file, FALSE); |
463 | 0 | if (!chunk) |
464 | 0 | { |
465 | 0 | DBG1(DBG_LIB, " opening '%s' failed: %s", file, strerror(errno)); |
466 | 0 | return NULL; |
467 | 0 | } |
468 | 0 | cred = load_from_blob(*chunk, type, subtype, subject, flags); |
469 | 0 | chunk_unmap_clear(chunk); |
470 | 0 | return cred; |
471 | 0 | } |
472 | | |
473 | | /** |
474 | | * Load all kind of PEM encoded credentials. |
475 | | */ |
476 | | static void *pem_load(credential_type_t type, int subtype, va_list args) |
477 | 87.1k | { |
478 | 87.1k | char *file = NULL; |
479 | 87.1k | chunk_t pem = chunk_empty; |
480 | 87.1k | identification_t *subject = NULL; |
481 | 87.1k | int flags = 0; |
482 | | |
483 | 87.1k | while (TRUE) |
484 | 111k | { |
485 | 111k | switch (va_arg(args, builder_part_t)) |
486 | 111k | { |
487 | 0 | case BUILD_FROM_FILE: |
488 | 0 | file = va_arg(args, char*); |
489 | 0 | continue; |
490 | 24.6k | case BUILD_BLOB: |
491 | 24.6k | case BUILD_BLOB_PEM: |
492 | 24.6k | pem = va_arg(args, chunk_t); |
493 | 24.6k | continue; |
494 | 0 | case BUILD_SUBJECT: |
495 | 0 | subject = va_arg(args, identification_t*); |
496 | 0 | continue; |
497 | 0 | case BUILD_X509_FLAG: |
498 | 0 | flags = va_arg(args, int); |
499 | 0 | continue; |
500 | 24.6k | case BUILD_END: |
501 | 24.6k | break; |
502 | 62.4k | default: |
503 | 62.4k | return NULL; |
504 | 111k | } |
505 | 24.6k | break; |
506 | 111k | } |
507 | | |
508 | 24.6k | if (pem.len) |
509 | 24.6k | { |
510 | 24.6k | return load_from_blob(pem, type, subtype, subject, flags); |
511 | 24.6k | } |
512 | 0 | if (file) |
513 | 0 | { |
514 | 0 | return load_from_file(file, type, subtype, subject, flags); |
515 | 0 | } |
516 | 0 | return NULL; |
517 | 0 | } |
518 | | |
519 | | /** |
520 | | * Private key PEM loader. |
521 | | */ |
522 | | private_key_t *pem_private_key_load(key_type_t type, va_list args) |
523 | 2 | { |
524 | 2 | return pem_load(CRED_PRIVATE_KEY, type, args); |
525 | 2 | } |
526 | | |
527 | | /** |
528 | | * Public key PEM loader. |
529 | | */ |
530 | | public_key_t *pem_public_key_load(key_type_t type, va_list args) |
531 | 1.02k | { |
532 | 1.02k | return pem_load(CRED_PUBLIC_KEY, type, args); |
533 | 1.02k | } |
534 | | |
535 | | /** |
536 | | * Certificate PEM loader. |
537 | | */ |
538 | | certificate_t *pem_certificate_load(certificate_type_t type, va_list args) |
539 | 86.0k | { |
540 | 86.0k | return pem_load(CRED_CERTIFICATE, type, args); |
541 | 86.0k | } |
542 | | |
543 | | /** |
544 | | * Container PEM loader. |
545 | | */ |
546 | | container_t *pem_container_load(container_type_t type, va_list args) |
547 | 0 | { |
548 | 0 | return pem_load(CRED_CONTAINER, type, args); |
549 | 0 | } |