2012-09-24 20:11:48 +04:00
|
|
|
/* Instantiate a public key crypto key from an X.509 Certificate
|
|
|
|
*
|
|
|
|
* Copyright (C) 2012 Red Hat, Inc. All Rights Reserved.
|
|
|
|
* Written by David Howells (dhowells@redhat.com)
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public Licence
|
|
|
|
* as published by the Free Software Foundation; either version
|
|
|
|
* 2 of the Licence, or (at your option) any later version.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#define pr_fmt(fmt) "X.509: "fmt
|
|
|
|
#include <linux/module.h>
|
|
|
|
#include <linux/kernel.h>
|
|
|
|
#include <linux/slab.h>
|
|
|
|
#include <linux/err.h>
|
|
|
|
#include <linux/mpi.h>
|
|
|
|
#include <linux/asn1_decoder.h>
|
|
|
|
#include <keys/asymmetric-subtype.h>
|
|
|
|
#include <keys/asymmetric-parser.h>
|
2013-08-20 22:36:27 +04:00
|
|
|
#include <keys/system_keyring.h>
|
2012-09-24 20:11:48 +04:00
|
|
|
#include <crypto/hash.h>
|
|
|
|
#include "asymmetric_keys.h"
|
|
|
|
#include "public_key.h"
|
|
|
|
#include "x509_parser.h"
|
|
|
|
|
2014-06-17 12:56:59 +04:00
|
|
|
static bool use_builtin_keys;
|
2014-09-16 20:36:13 +04:00
|
|
|
static struct asymmetric_key_id *ca_keyid;
|
2014-06-17 12:56:58 +04:00
|
|
|
|
|
|
|
#ifndef MODULE
|
2015-02-11 15:33:34 +03:00
|
|
|
static struct {
|
|
|
|
struct asymmetric_key_id id;
|
|
|
|
unsigned char data[10];
|
|
|
|
} cakey;
|
|
|
|
|
2014-06-17 12:56:58 +04:00
|
|
|
static int __init ca_keys_setup(char *str)
|
|
|
|
{
|
|
|
|
if (!str) /* default system keyring */
|
|
|
|
return 1;
|
|
|
|
|
2014-09-16 20:36:13 +04:00
|
|
|
if (strncmp(str, "id:", 3) == 0) {
|
2015-02-11 15:33:34 +03:00
|
|
|
struct asymmetric_key_id *p = &cakey.id;
|
|
|
|
size_t hexlen = (strlen(str) - 3) / 2;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
if (hexlen == 0 || hexlen > sizeof(cakey.data)) {
|
|
|
|
pr_err("Missing or invalid ca_keys id\n");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = __asymmetric_key_hex_to_key_id(str + 3, p, hexlen);
|
|
|
|
if (ret < 0)
|
|
|
|
pr_err("Unparsable ca_keys id hex string\n");
|
|
|
|
else
|
2014-09-16 20:36:13 +04:00
|
|
|
ca_keyid = p; /* owner key 'id:xxxxxx' */
|
|
|
|
} else if (strcmp(str, "builtin") == 0) {
|
2014-06-17 12:56:59 +04:00
|
|
|
use_builtin_keys = true;
|
2014-09-16 20:36:13 +04:00
|
|
|
}
|
2014-06-17 12:56:58 +04:00
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
__setup("ca_keys=", ca_keys_setup);
|
|
|
|
#endif
|
|
|
|
|
2014-07-28 17:11:32 +04:00
|
|
|
/**
|
|
|
|
* x509_request_asymmetric_key - Request a key by X.509 certificate params.
|
|
|
|
* @keyring: The keys to search.
|
2015-07-20 23:16:26 +03:00
|
|
|
* @id: The issuer & serialNumber to look for or NULL.
|
|
|
|
* @skid: The subjectKeyIdentifier to look for or NULL.
|
2014-10-06 18:21:05 +04:00
|
|
|
* @partial: Use partial match if true, exact if false.
|
2014-07-28 17:11:32 +04:00
|
|
|
*
|
2015-07-20 23:16:26 +03:00
|
|
|
* Find a key in the given keyring by identifier. The preferred identifier is
|
|
|
|
* the issuer + serialNumber and the fallback identifier is the
|
|
|
|
* subjectKeyIdentifier. If both are given, the lookup is by the former, but
|
|
|
|
* the latter must also match.
|
2013-08-20 22:36:27 +04:00
|
|
|
*/
|
2014-07-28 17:11:32 +04:00
|
|
|
struct key *x509_request_asymmetric_key(struct key *keyring,
|
2015-07-20 23:16:26 +03:00
|
|
|
const struct asymmetric_key_id *id,
|
|
|
|
const struct asymmetric_key_id *skid,
|
2014-10-06 18:21:05 +04:00
|
|
|
bool partial)
|
2013-08-20 22:36:27 +04:00
|
|
|
{
|
2015-07-20 23:16:26 +03:00
|
|
|
struct key *key;
|
|
|
|
key_ref_t ref;
|
|
|
|
const char *lookup;
|
|
|
|
char *req, *p;
|
|
|
|
int len;
|
|
|
|
|
|
|
|
if (id) {
|
|
|
|
lookup = id->data;
|
|
|
|
len = id->len;
|
|
|
|
} else {
|
|
|
|
lookup = skid->data;
|
|
|
|
len = skid->len;
|
|
|
|
}
|
|
|
|
|
2014-09-16 20:36:13 +04:00
|
|
|
/* Construct an identifier "id:<keyid>". */
|
2015-07-20 23:16:26 +03:00
|
|
|
p = req = kmalloc(2 + 1 + len * 2 + 1, GFP_KERNEL);
|
|
|
|
if (!req)
|
2013-08-20 22:36:27 +04:00
|
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
|
2014-10-06 18:21:05 +04:00
|
|
|
if (partial) {
|
|
|
|
*p++ = 'i';
|
|
|
|
*p++ = 'd';
|
|
|
|
} else {
|
|
|
|
*p++ = 'e';
|
|
|
|
*p++ = 'x';
|
|
|
|
}
|
2014-09-16 20:36:13 +04:00
|
|
|
*p++ = ':';
|
2015-07-20 23:16:26 +03:00
|
|
|
p = bin2hex(p, lookup, len);
|
2014-09-16 20:36:13 +04:00
|
|
|
*p = 0;
|
2013-08-20 22:36:27 +04:00
|
|
|
|
2015-07-20 23:16:26 +03:00
|
|
|
pr_debug("Look up: \"%s\"\n", req);
|
2013-08-20 22:36:27 +04:00
|
|
|
|
2015-07-20 23:16:26 +03:00
|
|
|
ref = keyring_search(make_key_ref(keyring, 1),
|
|
|
|
&key_type_asymmetric, req);
|
|
|
|
if (IS_ERR(ref))
|
|
|
|
pr_debug("Request for key '%s' err %ld\n", req, PTR_ERR(ref));
|
|
|
|
kfree(req);
|
2013-08-20 22:36:27 +04:00
|
|
|
|
2015-07-20 23:16:26 +03:00
|
|
|
if (IS_ERR(ref)) {
|
|
|
|
switch (PTR_ERR(ref)) {
|
2013-08-20 22:36:27 +04:00
|
|
|
/* Hide some search errors */
|
|
|
|
case -EACCES:
|
|
|
|
case -ENOTDIR:
|
|
|
|
case -EAGAIN:
|
|
|
|
return ERR_PTR(-ENOKEY);
|
|
|
|
default:
|
2015-07-20 23:16:26 +03:00
|
|
|
return ERR_CAST(ref);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
key = key_ref_to_ptr(ref);
|
|
|
|
if (id && skid) {
|
|
|
|
const struct asymmetric_key_ids *kids = asymmetric_key_ids(key);
|
|
|
|
if (!kids->id[1]) {
|
|
|
|
pr_debug("issuer+serial match, but expected SKID missing\n");
|
|
|
|
goto reject;
|
|
|
|
}
|
|
|
|
if (!asymmetric_key_id_same(skid, kids->id[1])) {
|
|
|
|
pr_debug("issuer+serial match, but SKID does not\n");
|
|
|
|
goto reject;
|
2013-08-20 22:36:27 +04:00
|
|
|
}
|
|
|
|
}
|
2015-07-20 23:16:26 +03:00
|
|
|
|
|
|
|
pr_devel("<==%s() = 0 [%x]\n", __func__, key_serial(key));
|
|
|
|
return key;
|
2013-08-20 22:36:27 +04:00
|
|
|
|
2015-07-20 23:16:26 +03:00
|
|
|
reject:
|
|
|
|
key_put(key);
|
|
|
|
return ERR_PTR(-EKEYREJECTED);
|
2013-08-20 22:36:27 +04:00
|
|
|
}
|
2014-08-03 15:54:48 +04:00
|
|
|
EXPORT_SYMBOL_GPL(x509_request_asymmetric_key);
|
2013-08-20 22:36:27 +04:00
|
|
|
|
2012-09-24 20:11:48 +04:00
|
|
|
/*
|
2013-08-30 19:18:02 +04:00
|
|
|
* Set up the signature parameters in an X.509 certificate. This involves
|
|
|
|
* digesting the signed data and extracting the signature.
|
2012-09-24 20:11:48 +04:00
|
|
|
*/
|
2013-08-30 19:18:02 +04:00
|
|
|
int x509_get_sig_params(struct x509_certificate *cert)
|
2012-09-24 20:11:48 +04:00
|
|
|
{
|
|
|
|
struct crypto_shash *tfm;
|
|
|
|
struct shash_desc *desc;
|
|
|
|
size_t digest_size, desc_size;
|
2013-08-30 19:18:02 +04:00
|
|
|
void *digest;
|
2012-09-24 20:11:48 +04:00
|
|
|
int ret;
|
|
|
|
|
|
|
|
pr_devel("==>%s()\n", __func__);
|
2013-08-30 19:18:02 +04:00
|
|
|
|
2014-09-16 20:36:15 +04:00
|
|
|
if (cert->unsupported_crypto)
|
|
|
|
return -ENOPKG;
|
2013-08-30 19:18:02 +04:00
|
|
|
if (cert->sig.rsa.s)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
cert->sig.rsa.s = mpi_read_raw_data(cert->raw_sig, cert->raw_sig_size);
|
|
|
|
if (!cert->sig.rsa.s)
|
|
|
|
return -ENOMEM;
|
|
|
|
cert->sig.nr_mpi = 1;
|
|
|
|
|
2012-09-24 20:11:48 +04:00
|
|
|
/* Allocate the hashing algorithm we're going to need and find out how
|
|
|
|
* big the hash operational data will be.
|
|
|
|
*/
|
2013-05-06 16:58:15 +04:00
|
|
|
tfm = crypto_alloc_shash(hash_algo_name[cert->sig.pkey_hash_algo], 0, 0);
|
2014-09-16 20:36:15 +04:00
|
|
|
if (IS_ERR(tfm)) {
|
|
|
|
if (PTR_ERR(tfm) == -ENOENT) {
|
|
|
|
cert->unsupported_crypto = true;
|
|
|
|
return -ENOPKG;
|
|
|
|
}
|
|
|
|
return PTR_ERR(tfm);
|
|
|
|
}
|
2012-09-24 20:11:48 +04:00
|
|
|
|
|
|
|
desc_size = crypto_shash_descsize(tfm) + sizeof(*desc);
|
|
|
|
digest_size = crypto_shash_digestsize(tfm);
|
|
|
|
|
2013-08-30 19:18:02 +04:00
|
|
|
/* We allocate the hash operational data storage on the end of the
|
|
|
|
* digest storage space.
|
2012-09-24 20:11:48 +04:00
|
|
|
*/
|
|
|
|
ret = -ENOMEM;
|
2013-08-30 19:18:02 +04:00
|
|
|
digest = kzalloc(digest_size + desc_size, GFP_KERNEL);
|
|
|
|
if (!digest)
|
|
|
|
goto error;
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-30 19:18:02 +04:00
|
|
|
cert->sig.digest = digest;
|
|
|
|
cert->sig.digest_size = digest_size;
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-30 19:18:02 +04:00
|
|
|
desc = digest + digest_size;
|
|
|
|
desc->tfm = tfm;
|
|
|
|
desc->flags = CRYPTO_TFM_REQ_MAY_SLEEP;
|
2012-09-24 20:11:48 +04:00
|
|
|
|
|
|
|
ret = crypto_shash_init(desc);
|
|
|
|
if (ret < 0)
|
|
|
|
goto error;
|
2013-08-30 19:18:02 +04:00
|
|
|
might_sleep();
|
|
|
|
ret = crypto_shash_finup(desc, cert->tbs, cert->tbs_size, digest);
|
|
|
|
error:
|
|
|
|
crypto_free_shash(tfm);
|
|
|
|
pr_devel("<==%s() = %d\n", __func__, ret);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
EXPORT_SYMBOL_GPL(x509_get_sig_params);
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-30 19:18:02 +04:00
|
|
|
/*
|
|
|
|
* Check the signature on a certificate using the provided public key
|
|
|
|
*/
|
|
|
|
int x509_check_signature(const struct public_key *pub,
|
|
|
|
struct x509_certificate *cert)
|
|
|
|
{
|
|
|
|
int ret;
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-30 19:18:02 +04:00
|
|
|
pr_devel("==>%s()\n", __func__);
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-30 19:18:02 +04:00
|
|
|
ret = x509_get_sig_params(cert);
|
|
|
|
if (ret < 0)
|
|
|
|
return ret;
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-30 19:18:02 +04:00
|
|
|
ret = public_key_verify_signature(pub, &cert->sig);
|
2014-09-16 20:36:15 +04:00
|
|
|
if (ret == -ENOPKG)
|
|
|
|
cert->unsupported_crypto = true;
|
2012-09-24 20:11:48 +04:00
|
|
|
pr_debug("Cert Verification: %d\n", ret);
|
|
|
|
return ret;
|
|
|
|
}
|
2013-08-30 19:18:02 +04:00
|
|
|
EXPORT_SYMBOL_GPL(x509_check_signature);
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-20 22:36:27 +04:00
|
|
|
/*
|
|
|
|
* Check the new certificate against the ones in the trust keyring. If one of
|
|
|
|
* those is the signing key and validates the new certificate, then mark the
|
|
|
|
* new certificate as being trusted.
|
|
|
|
*
|
|
|
|
* Return 0 if the new certificate was successfully validated, 1 if we couldn't
|
|
|
|
* find a matching parent certificate in the trusted list and an error if there
|
|
|
|
* is a matching certificate but the signature check fails.
|
|
|
|
*/
|
|
|
|
static int x509_validate_trust(struct x509_certificate *cert,
|
|
|
|
struct key *trust_keyring)
|
|
|
|
{
|
|
|
|
struct key *key;
|
|
|
|
int ret = 1;
|
|
|
|
|
|
|
|
if (!trust_keyring)
|
|
|
|
return -EOPNOTSUPP;
|
|
|
|
|
2015-07-20 23:16:26 +03:00
|
|
|
if (ca_keyid && !asymmetric_key_id_partial(cert->akid_skid, ca_keyid))
|
2014-06-17 12:56:58 +04:00
|
|
|
return -EPERM;
|
|
|
|
|
2015-07-20 23:16:26 +03:00
|
|
|
key = x509_request_asymmetric_key(trust_keyring,
|
|
|
|
cert->akid_id, cert->akid_skid,
|
2014-10-06 18:21:05 +04:00
|
|
|
false);
|
2013-08-20 22:36:27 +04:00
|
|
|
if (!IS_ERR(key)) {
|
2014-06-17 12:56:59 +04:00
|
|
|
if (!use_builtin_keys
|
|
|
|
|| test_bit(KEY_FLAG_BUILTIN, &key->flags))
|
|
|
|
ret = x509_check_signature(key->payload.data, cert);
|
2013-08-20 22:36:27 +04:00
|
|
|
key_put(key);
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2012-09-24 20:11:48 +04:00
|
|
|
/*
|
|
|
|
* Attempt to parse a data blob for a key as an X509 certificate.
|
|
|
|
*/
|
|
|
|
static int x509_key_preparse(struct key_preparsed_payload *prep)
|
|
|
|
{
|
2014-09-16 20:36:13 +04:00
|
|
|
struct asymmetric_key_ids *kids;
|
2012-09-24 20:11:48 +04:00
|
|
|
struct x509_certificate *cert;
|
2014-09-16 20:36:13 +04:00
|
|
|
const char *q;
|
2012-09-24 20:11:48 +04:00
|
|
|
size_t srlen, sulen;
|
2014-09-16 20:36:13 +04:00
|
|
|
char *desc = NULL, *p;
|
2012-09-24 20:11:48 +04:00
|
|
|
int ret;
|
|
|
|
|
|
|
|
cert = x509_cert_parse(prep->data, prep->datalen);
|
|
|
|
if (IS_ERR(cert))
|
|
|
|
return PTR_ERR(cert);
|
|
|
|
|
|
|
|
pr_devel("Cert Issuer: %s\n", cert->issuer);
|
|
|
|
pr_devel("Cert Subject: %s\n", cert->subject);
|
2013-08-30 19:18:15 +04:00
|
|
|
|
|
|
|
if (cert->pub->pkey_algo >= PKEY_ALGO__LAST ||
|
|
|
|
cert->sig.pkey_algo >= PKEY_ALGO__LAST ||
|
|
|
|
cert->sig.pkey_hash_algo >= PKEY_HASH__LAST ||
|
|
|
|
!pkey_algo[cert->pub->pkey_algo] ||
|
|
|
|
!pkey_algo[cert->sig.pkey_algo] ||
|
2013-05-06 16:58:15 +04:00
|
|
|
!hash_algo_name[cert->sig.pkey_hash_algo]) {
|
2013-08-30 19:18:15 +04:00
|
|
|
ret = -ENOPKG;
|
|
|
|
goto error_free_cert;
|
|
|
|
}
|
|
|
|
|
2013-08-30 19:15:24 +04:00
|
|
|
pr_devel("Cert Key Algo: %s\n", pkey_algo_name[cert->pub->pkey_algo]);
|
2015-07-29 18:58:32 +03:00
|
|
|
pr_devel("Cert Valid period: %lld-%lld\n", cert->valid_from, cert->valid_to);
|
2013-04-25 11:43:56 +04:00
|
|
|
pr_devel("Cert Signature: %s + %s\n",
|
|
|
|
pkey_algo_name[cert->sig.pkey_algo],
|
2013-05-06 16:58:15 +04:00
|
|
|
hash_algo_name[cert->sig.pkey_hash_algo]);
|
2012-09-24 20:11:48 +04:00
|
|
|
|
2013-08-30 19:15:24 +04:00
|
|
|
cert->pub->algo = pkey_algo[cert->pub->pkey_algo];
|
2012-09-24 20:11:48 +04:00
|
|
|
cert->pub->id_type = PKEY_ID_X509;
|
|
|
|
|
2013-08-30 19:18:31 +04:00
|
|
|
/* Check the signature on the key if it appears to be self-signed */
|
2015-07-20 23:16:26 +03:00
|
|
|
if ((!cert->akid_skid && !cert->akid_id) ||
|
|
|
|
asymmetric_key_id_same(cert->skid, cert->akid_skid) ||
|
|
|
|
asymmetric_key_id_same(cert->id, cert->akid_id)) {
|
2013-08-20 22:36:27 +04:00
|
|
|
ret = x509_check_signature(cert->pub, cert); /* self-signed */
|
2012-09-24 20:11:48 +04:00
|
|
|
if (ret < 0)
|
|
|
|
goto error_free_cert;
|
2013-08-20 22:36:27 +04:00
|
|
|
} else if (!prep->trusted) {
|
|
|
|
ret = x509_validate_trust(cert, get_system_trusted_keyring());
|
|
|
|
if (!ret)
|
|
|
|
prep->trusted = 1;
|
2012-09-24 20:11:48 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Propose a description */
|
|
|
|
sulen = strlen(cert->subject);
|
2014-10-03 19:17:02 +04:00
|
|
|
if (cert->raw_skid) {
|
|
|
|
srlen = cert->raw_skid_size;
|
|
|
|
q = cert->raw_skid;
|
|
|
|
} else {
|
|
|
|
srlen = cert->raw_serial_size;
|
|
|
|
q = cert->raw_serial;
|
|
|
|
}
|
2014-09-16 20:36:13 +04:00
|
|
|
if (srlen > 1 && *q == 0) {
|
|
|
|
srlen--;
|
|
|
|
q++;
|
|
|
|
}
|
|
|
|
|
2012-09-24 20:11:48 +04:00
|
|
|
ret = -ENOMEM;
|
2014-09-16 20:36:13 +04:00
|
|
|
desc = kmalloc(sulen + 2 + srlen * 2 + 1, GFP_KERNEL);
|
2012-09-24 20:11:48 +04:00
|
|
|
if (!desc)
|
|
|
|
goto error_free_cert;
|
2014-09-16 20:36:13 +04:00
|
|
|
p = memcpy(desc, cert->subject, sulen);
|
|
|
|
p += sulen;
|
|
|
|
*p++ = ':';
|
|
|
|
*p++ = ' ';
|
|
|
|
p = bin2hex(p, q, srlen);
|
|
|
|
*p = 0;
|
|
|
|
|
|
|
|
kids = kmalloc(sizeof(struct asymmetric_key_ids), GFP_KERNEL);
|
|
|
|
if (!kids)
|
|
|
|
goto error_free_desc;
|
|
|
|
kids->id[0] = cert->id;
|
|
|
|
kids->id[1] = cert->skid;
|
2012-09-24 20:11:48 +04:00
|
|
|
|
|
|
|
/* We're pinning the module by being linked against it */
|
|
|
|
__module_get(public_key_subtype.owner);
|
|
|
|
prep->type_data[0] = &public_key_subtype;
|
2014-09-16 20:36:13 +04:00
|
|
|
prep->type_data[1] = kids;
|
2014-07-18 21:56:34 +04:00
|
|
|
prep->payload[0] = cert->pub;
|
2012-09-24 20:11:48 +04:00
|
|
|
prep->description = desc;
|
|
|
|
prep->quotalen = 100;
|
|
|
|
|
|
|
|
/* We've finished with the certificate */
|
|
|
|
cert->pub = NULL;
|
2014-09-16 20:36:13 +04:00
|
|
|
cert->id = NULL;
|
|
|
|
cert->skid = NULL;
|
2012-09-24 20:11:48 +04:00
|
|
|
desc = NULL;
|
|
|
|
ret = 0;
|
|
|
|
|
2014-09-16 20:36:13 +04:00
|
|
|
error_free_desc:
|
|
|
|
kfree(desc);
|
2012-09-24 20:11:48 +04:00
|
|
|
error_free_cert:
|
|
|
|
x509_free_certificate(cert);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static struct asymmetric_key_parser x509_key_parser = {
|
|
|
|
.owner = THIS_MODULE,
|
|
|
|
.name = "x509",
|
|
|
|
.parse = x509_key_preparse,
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Module stuff
|
|
|
|
*/
|
|
|
|
static int __init x509_key_init(void)
|
|
|
|
{
|
|
|
|
return register_asymmetric_key_parser(&x509_key_parser);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void __exit x509_key_exit(void)
|
|
|
|
{
|
|
|
|
unregister_asymmetric_key_parser(&x509_key_parser);
|
|
|
|
}
|
|
|
|
|
|
|
|
module_init(x509_key_init);
|
|
|
|
module_exit(x509_key_exit);
|
2013-09-17 15:14:55 +04:00
|
|
|
|
|
|
|
MODULE_DESCRIPTION("X.509 certificate parser");
|
|
|
|
MODULE_LICENSE("GPL");
|