openssl engine rsa引擎tls认证 (9)

openssl engine rsa引擎进行tls认证 (9)

openssl引擎在tls通信的应用,实现tls的客户端和服务端,进行tls双休认证

我们通过rsa引擎可以替换openssl中得rsa算法,使用我们自己得算法,那么在tls(c/s)通信过程中,在handshark握手阶段有证书认证,使用rsa证书得话会涉及rsa签名,验签得过程,同时还会调用rsa公钥加密接口。

rangine.h
#ifndef __RANG_ENG_H__
#define __RANG_ENG_H__
#include <openssl/engine.h>
#include <stdio.h>
#include <openssl/rsa.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/ecdh.h>
#include <openssl/crypto.h>
#include <string.h>

#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include <openssl/ssl.h>




const char *eng_id="randeng";
const char *eng_name="rand engines for OpenSSL";

/*需要声明一下结构体*/
struct evp_md_st {
    int type;
    int pkey_type;
    int md_size;
    unsigned long flags;
    int (*init) (EVP_MD_CTX *ctx);
    int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count);
    int (*final) (EVP_MD_CTX *ctx, unsigned char *md);
    int (*copy) (EVP_MD_CTX *to, const EVP_MD_CTX *from);
    int (*cleanup) (EVP_MD_CTX *ctx);
    int block_size;
    int ctx_size;               /* how big does the ctx->md_data need to be */
    /* control function */
    int (*md_ctrl) (EVP_MD_CTX *ctx, int cmd, int p1, void *p2);
} /* EVP_MD */ ;

struct evp_md_ctx_st {
    const EVP_MD *digest;
    ENGINE *engine;             /* functional reference if 'digest' is
                                 * ENGINE-provided */
    unsigned long flags;
    void *md_data;
    /* Public key context for sign/verify */
    EVP_PKEY_CTX *pctx;
    /* Update function: usually copied from EVP_MD */
    int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count);
} /* EVP_MD_CTX */ ;


typedef volatile int CRYPTO_REF_COUNT;
struct rsa_st {
    /*
     * The first parameter is used to pickup errors where this is passed
     * instead of an EVP_PKEY, it is set to 0
     */
    int pad;
    int32_t version;
    const RSA_METHOD *meth;
    /* functional reference if 'meth' is ENGINE-provided */
    ENGINE *engine;
    BIGNUM *n;
    BIGNUM *e;
    BIGNUM *d;
    BIGNUM *p;
    BIGNUM *q;
    BIGNUM *dmp1;
    BIGNUM *dmq1;
    BIGNUM *iqmp;
    /* for multi-prime RSA, defined in RFC 8017 */
    STACK_OF(RSA_PRIME_INFO) *prime_infos;
    /* If a PSS only key this contains the parameter restrictions */
    RSA_PSS_PARAMS *pss;
    /* be careful using this if the RSA structure is shared */
    CRYPTO_EX_DATA ex_data;
    CRYPTO_REF_COUNT references;
    int flags;
    /* Used to cache montgomery values */
    BN_MONT_CTX *_method_mod_n;
    BN_MONT_CTX *_method_mod_p;
    BN_MONT_CTX *_method_mod_q;
    /*
     * all BIGNUM values are actually in the following data, if it is not
     * NULL
     */
    char *bignum_data;
    BN_BLINDING *blinding;
    BN_BLINDING *mt_blinding;
    CRYPTO_RWLOCK *lock;
};


struct rsa_meth_st {
    char *name;
    int (*rsa_pub_enc) (int flen, const unsigned char *from,
                        unsigned char *to, RSA *rsa, int padding);
    int (*rsa_pub_dec) (int flen, const unsigned char *from,
                        unsigned char *to, RSA *rsa, int padding);
    int (*rsa_priv_enc) (int flen, const unsigned char *from,
                         unsigned char *to, RSA *rsa, int padding);
    int (*rsa_priv_dec) (int flen, const unsigned char *from,
                         unsigned char *to, RSA *rsa, int padding);
    /* Can be null */
    int (*rsa_mod_exp) (BIGNUM *r0, const BIGNUM *I, RSA *rsa, BN_CTX *ctx);
    /* Can be null */
    int (*bn_mod_exp) (BIGNUM *r, const BIGNUM *a, const BIGNUM *p,
                       const BIGNUM *m, BN_CTX *ctx, BN_MONT_CTX *m_ctx);
    /* called at new */
    int (*init) (RSA *rsa);
    /* called at free */
    int (*finish) (RSA *rsa);
    /* RSA_METHOD_FLAG_* things */
    int flags;
    /* may be needed! */
    char *app_data;
    /*
     * New sign and verify functions: some libraries don't allow arbitrary
     * data to be signed/verified: this allows them to be used. Note: for
     * this to work the RSA_public_decrypt() and RSA_private_encrypt() should
     * *NOT* be used RSA_sign(), RSA_verify() should be used instead.
     */
    int (*rsa_sign) (int type,
                     const unsigned char *m, unsigned int m_length,
                     unsigned char *sigret, unsigned int *siglen,
                     const RSA *rsa);
    int (*rsa_verify) (int dtype, const unsigned char *m,
                       unsigned int m_length, const unsigned char *sigbuf,
                       unsigned int siglen, const RSA *rsa);
    /*
     * If this callback is NULL, the builtin software RSA key-gen will be
     * used. This is for behavioural compatibility whilst the code gets
     * rewired, but one day it would be nice to assume there are no such
     * things as "builtin software" implementations.
     */
    int (*rsa_keygen) (RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);
    int (*rsa_multi_prime_keygen) (RSA *rsa, int bits, int primes,
                                   BIGNUM *e, BN_GENCB *cb);
};


#endif // DEBUG
rangine.c

实现tls客户端

#include "rangeng.h"

static int myrand_init(ENGINE *e)
{
    printf("myrand_init\n");
    return 1;
}
static int myrand_finish(ENGINE *e)
{
    printf("myrand_finish\n");
    return 1;
}
static int myrand_destroy(ENGINE *e)
{
    printf("myrand_destroy\n");
    return 1;
}

static int myrand_status(void)
{
    printf("myrand_status\n");
    return 1;
}

static int myrand_bytes(unsigned char *buf, size_t num)
{
    printf("myrand_bytes 产生随机数%d个\n",num);
    memset(buf,0,num);
    return 1;
}

const RAND_METHOD rand_method=
{
    NULL,
    myrand_bytes,
    NULL,
    NULL,
    NULL,
    myrand_status
};

static int  my_engine_sha256_update(EVP_MD_CTX *ctx, const void *data, size_t count);
static  int my_engine_sha256_init (EVP_MD_CTX *ctx)
{
    ctx->update = &my_engine_sha256_update;

    printf("initialized! SHA256\n");
    return 1;
}

static int  my_engine_sha256_update(EVP_MD_CTX *ctx, const void *data, size_t count)
{
    printf("SHA256 update \n");
    unsigned char * digest256 = (unsigned char*) malloc(sizeof(unsigned char)*32);

    memset(digest256,1,32);
    count = 32;
    ctx->md_data = digest256;
    return 1;
}

static int my_engine_sha256_final (EVP_MD_CTX *ctx, unsigned char *md)
{
    printf("SHA256 final size of EVP_MD: %d\n", sizeof(EVP_MD));
    memcpy(md, ctx->md_data, ctx->digest->md_size);
    return 1;
}


int my_engine_sha256_cope(EVP_MD_CTX *to, const EVP_MD_CTX *from)
{
    printf("Copy SHA256\n");
    if (to->md_data && from->md_data) {
        memcpy(to->md_data, from->md_data,sizeof(from->md_data));
    }
    return 1;
}

static int my_engine_sha256_clean(EVP_MD_CTX *ctx) {
    printf("SHA256 cleanup\n");
    if (ctx->md_data)
        memset(ctx->md_data, 0, 32);
    return 1;
}

// 摘要算法 digest
static EVP_MD my_sha256_engine={
    NID_sha256,
    0,
    32,
    EVP_MD_FLAG_DIGALGID_ABSENT,
    my_engine_sha256_init,
    my_engine_sha256_update,
    my_engine_sha256_final,
    my_engine_sha256_cope,
    my_engine_sha256_clean,
    64,
    32
};

static int my_rsa_pub_enc(int flen, const unsigned char *from,
                        unsigned char *to, RSA *rsa, int padding)
{
    printf("my_rsa_pub_enc\n");
    for (int i = 0; i < flen; i++)
    {
        to[i]=from[i]+10;
    }
    return flen;
}
static int my_rsa_pub_dec(int flen, const unsigned char *from,
                        unsigned char *to, RSA *rsa, int padding)
{
    printf("my_rsa_pub_dec\n");
    for (int i = 0; i < flen; i++)
    {
        to[i]=from[i]-10;
    }
    return flen;
}
static int my_rsa_priv_enc(int flen, const unsigned char *from,
                        unsigned char *to, RSA *rsa, int padding)
{
    printf("my_rsa_priv_enc\n");
    return 1;
}
static int my_rsa_priv_dec(int flen, const unsigned char *from,
                        unsigned char *to, RSA *rsa, int padding)
{
    printf("my_rsa_priv_dec\n");
    return 1;
}

static int my_rsa_sign (int type,const unsigned char *m, unsigned int m_length,
unsigned char *sigret, unsigned int *siglen,const RSA *rsa)
{
    printf("my_rsa_sign\n");
    return 1;
}

static int my_rsa_verify (int type,const unsigned char *m, unsigned int m_length,
unsigned char *sigret, unsigned int *siglen,const RSA *rsa)
{
    printf("my_rsa_verify\n");
    return 1;
}

static int my_rsa_init(RSA *rsa)
{
    printf("my_rsa_init\n");
    return 1;
}

static int my_rsa_finish(RSA *rsa)
{
    printf("my_rsa_finish\n");
    return 1;
}

const RSA_METHOD rsa_method = {
    "rsa_engine_test",
    my_rsa_pub_enc,
    my_rsa_pub_dec,
    NULL,
    NULL,
    NULL,
    NULL,
    my_rsa_init,
    my_rsa_finish,
    ENGINE_METHOD_RSA,
    NULL,
    my_rsa_sign,
    my_rsa_verify,
    NULL,
    NULL
};



static int my_digest_ids[] = {NID_sha256};
static int my_engine_digest_selector(ENGINE *e, const EVP_MD **digest,
        const int **nids, int nid) {
    int ok = 1;
    if (!digest) {
        *nids = my_digest_ids;
        printf("\n Digest is empty! Nid:%d\n", nid);
        return 2;
    }
    printf("Digest no %d requested\n",nid);
    if (nid == NID_sha256) {
        *digest = &my_sha256_engine;
    }
    else {
        ok = 0;
        *digest = NULL;
    }
    return ok;
}


/*绑定函数*/
static int bind_pp(ENGINE *e)
{
     if(!ENGINE_set_id(e,eng_id)||
    !ENGINE_set_name(e,eng_name)||   
    !ENGINE_set_RAND(e,&rand_method)||
    !ENGINE_set_destroy_function(e,myrand_destroy)||
    !ENGINE_set_init_function(e,myrand_init)||
    !ENGINE_set_finish_function(e,myrand_finish)||
    !ENGINE_set_RSA(e,&rsa_method)||
    !ENGINE_set_digests(e, &my_engine_digest_selector)) //设置摘要选择器函数
        return 0;
    
    //RAND_set_rand_method(&rand_method);
    printf("ERR_load_rsa success\n");
    return 1;
}


static ENGINE *enging_range(void)
{
    ENGINE*e =ENGINE_new();
    if(!e)
        return NULL;
    if(!bind_pp(e))
    {
        ENGINE_free(e);
		return NULL;
    }
    return e;
}
void ENGINE_load_rand()
{
    ENGINE* e=enging_range();
    if(!e)
        return ;
    ENGINE_add(e);
    ENGINE_free(e);
    //ERR_add_error(e);
}
static void display_engine_list()
{
	ENGINE *h;
	int loop;
	
	h = ENGINE_get_first();
	loop = 0;
	printf("start:\n");
	while(h)
	{
		printf("engine %i, id = \"%s\", name = \"%s\"\n",
			loop++, ENGINE_get_id(h), ENGINE_get_name(h));
		h = ENGINE_get_next(h);
	}
	printf("end of list\n");
	ENGINE_free(h);
}








#define CACERT "../cert/ca.crt"
#define CLIENT_CRT "../cert/client.crt"
#define CLIENT_KEY "../cert/client.key"
#define CHK_ERR(err,s) if ((err)==-1) { perror(s); exit(-2); }
#define SERVER_ADDR "172.16.39.197" //输入自己的IP地址
#define PORT 20001

#define SERVER_mode 1
#define CLIENT_mode 0

SSL_CTX* InitSSL(char *ca_path,char *client_crt_path,char *client_key_path,int mothflag)
{
    SSL_CTX* ctx=NULL;
    SSL_METHOD *meth;
    int status;
 
    /* * 算法初始化 * */   
    SSL_library_init();
    // 加载SSL错误信息
    SSL_load_error_strings();
 
    // 添加SSL的加密/HASH算法
    SSLeay_add_ssl_algorithms();
    
    /*采用什么协议(SSLv2/SSLv3/TLSv1)在此指定*/
    if(mothflag)
        meth = (SSL_METHOD *)TLSv1_2_server_method();
    else
        meth = (SSL_METHOD *)TLSv1_2_client_method();
    /* 创建SSL会话环境 */
    ctx = SSL_CTX_new (meth);                    
    if(ctx == NULL)
    {
        printf ("SSL_CTX_new error\n");
        return NULL;
    }
  
    // /*验证与否,是否要验证对方*/
   SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,NULL);   
    // /*若验证对方,则放置CA证书*/
   SSL_CTX_load_verify_locations(ctx,ca_path,NULL); 

    /*加载自己的证书*/
    if (SSL_CTX_use_certificate_file(ctx, client_crt_path, SSL_FILETYPE_PEM) <= 0) 
    {
        printf("SSL_CTX_use_certificate_file error\n");
        goto exit;
    }
    /*加载自己的私钥,以用于签名*/
    if (SSL_CTX_use_PrivateKey_file(ctx, client_key_path, SSL_FILETYPE_PEM) <= 0) 
    {
        printf("SSL_CTX_use_PrivateKey_file error\n");
        goto exit;
    }

       // 设置证书私钥文件的密码
    //SSL_CTX_set_default_passwd_cb_userdata(ctx, pw);

    /*调用了以上两个函数后,检验一下自己的证书与私钥是否配对*/
    if (!SSL_CTX_check_private_key(ctx)) 
    {
        printf("SSL_CTX_check_private_key error\n");
        goto exit;
    } 
    return ctx;

exit:
    if(ctx) SSL_CTX_free (ctx);
    return NULL;
}






int main()
{
    
    // 初始化 OpenSSL 引擎系统
    ENGINE_load_builtin_engines();
    ENGINE_register_all_complete();
    //加载引擎
    ENGINE_load_rand();
    display_engine_list();	
    ENGINE *e=NULL;
    e=ENGINE_by_id(eng_id);
    if(!e)
    {
        printf("err load engine failed\n");
        return 0;
    }    

    ENGINE_init(e);
    printf("RAND-----BEGIN---END\n");
    // 调用引擎中的随机数生成方法
    //ENGINE_set_default_RAND(e);
    unsigned char rand_buf[6]={0};
    int err = RAND_bytes(rand_buf,5);
    for(int i= 0; i < 5; i++) {
        printf("%x",rand_buf[i]);
    }
    printf("\n");
 
    char * str1 = "Fraunhofer FKIE Wachtberg!";
    int str_len =  26;
    int er;
    er = ENGINE_set_default_digests(e);
    printf("ENGINE SETTING DEFAULT DIGESTS %d\n",er);

    unsigned char  digest[32] = {0};
    unsigned int digestSize = -1;
    EVP_MD_CTX *evp_ctx=NULL;
    printf("SHA256-----BEGIN---END\n");
    evp_ctx = EVP_MD_CTX_create();
    er = EVP_DigestInit_ex(evp_ctx, EVP_sha256(),e);
    printf("Digest INIT %d\n",er);

    er = EVP_DigestUpdate(evp_ctx, (unsigned char*)str1, str_len);
    printf("Digest Update %d\n",er);

    er = EVP_DigestFinal(evp_ctx, digest, &digestSize);
    printf("Digest Final %d Digest size:%d\n",er,digestSize);

    for(int i= 0; i< digestSize; i++) {
        printf("%x", digest[i]);
    }
    printf("\n");
    EVP_MD_CTX_destroy(evp_ctx);

    printf("RSA-----BEGIN---END\n");
    ENGINE_register_RSA(e); //
    RSA *rsa = RSA_new();
    BIGNUM *bn = BN_new();
    BN_set_word(bn, RSA_F4);
    RSA_generate_key_ex(rsa, 2048, bn, NULL);
    printf("RSA Key Pair:\n");
    RSA_print_fp(stdout, rsa, 0);

    // 使用引擎进行 RSA 加密
    unsigned char plaintext[] = "Hello, World!";
    unsigned char ciphertext[256]={0}; // 注意,缓冲区大小可能需要根据您的密钥长度进行调整
    unsigned char plaintext1[256]={0};
    int len = RSA_public_encrypt(sizeof(plaintext), plaintext, ciphertext, rsa, RSA_PKCS1_PADDING);

    len=RSA_public_decrypt(len, ciphertext, plaintext1, rsa, RSA_PKCS1_PADDING);


    // 打印加密后的数据
    printf("Encrypted data: ");
    for (int i = 0; i < len; i++) {
        printf("%02x ", ciphertext[i]);
    }
    printf("\n");
    printf("ciphertext=%s\n",ciphertext);

    // 打印加密后的数据
    printf("decrypted data: ");
    for (int i = 0; i < len; i++) {
        printf("%02x ", plaintext1[i]);
    }
    printf("\n");
    printf("ciphertext=%s\n",plaintext1);


    int sd=0;
    int confd=0;
    SSL* ssl=NULL;
    SSL_CTX* ctx=NULL;
    struct sockaddr_in sa={0};
    ctx=InitSSL(CACERT,CLIENT_CRT,CLIENT_KEY,CLIENT_mode);
    if(ctx==NULL) return -1;

    /* 指定加密器类型 */
    //SSL_CTX_set_cipher_list (ctx, "ECDHE-RSA-AES256-SHA");
    SSL_CTX_set_mode (ctx, SSL_MODE_AUTO_RETRY);

    /*以下是正常的TCP socket建立过程 .............................. */
    printf("Begin tcp socket...\n");

    sd= socket (AF_INET, SOCK_STREAM, 0);       
    if(sd <= 0)
    {
        perror("socket");
        goto exit;
    }
    
    sa.sin_family      = AF_INET;
    sa.sin_addr.s_addr = inet_addr(SERVER_ADDR);   /* Server IP */
    sa.sin_port        = htons(PORT);          /* Server Port number */
    confd = connect(sd, (struct sockaddr*)&sa, sizeof(sa)); 
    if(confd < 0)
    {
        printf("connect error=%d\n",confd);
        goto exit;
    }


    /* TCP 链接已建立.开始 SSL 握手过程.......................... */
    printf("Begin SSL negotiation \n");

    /*申请一个SSL套接字*/
    ssl = SSL_new (ctx);                        
    if(ssl <= 0)
    {
        printf("Error creating SSL new \n");
        goto exit;
    }

    /*绑定读写套接字*/
    SSL_set_fd (ssl, sd);
    SSL_connect (ssl);               
    printf("链接已建立.开始 SSL 握手过程 \n");


    /*打印所有加密算法的信息(可选)*/
    printf ("SSL connection using %s\n", SSL_get_cipher (ssl));

    /*得到服务端的证书并打印些信息(可选) */
    X509* server_cert = SSL_get_peer_certificate (ssl);      
    printf ("Server certificate:\n");

    char *str = X509_NAME_oneline (X509_get_subject_name (server_cert),0,0);
    printf ("/t subject: %s\n", str);
    free (str);

    str = X509_NAME_oneline (X509_get_issuer_name  (server_cert),0,0);
    printf ("/t issuer: %s\n", str);
    free (str);

    X509_free (server_cert);  /*如不再需要,需将证书释放 */

    /* 数据交换开始,用SSL_write,SSL_read代替write,read */
    printf("Begin SSL data exchange\n");

    unsigned char buf[300]={0};
    while(1)
    {
        gets(buf);
        int ret = SSL_write (ssl, buf, sizeof(buf)); 
        memset (buf, 0, sizeof(buf));
    }
    SSL_shutdown (ssl);  /* send SSL/TLS close_notify */
    /* 收尾工作 */
    shutdown (sd,2);
exit:
    if(sd > 0) close (sd);
    if(confd > 0) close (confd); 
    if(ctx) SSL_CTX_free(ctx);
    if(ssl) SSL_free (ssl);
    return 0;

    // 清理引擎资源
    ENGINE_finish(e);
    ENGINE_free(e);

}

tls服务端
OPENSSL TLS认证

研究openssl 加密套件协商过程

在一台 uBuntu 上执行以下命令建立 TLS/SSL Server

openssl s_server -CAfile ca.crt -cert server.crt -key server.key -port 20001 -verify 1 -tls1_2

切记证书相同保证一致

结果查看

我自己写的简单rsa算法并不满足openssl的tls验证,但是通过客户端打印可以发现,openssl 在进行tls认证的时候使用的是我们rsa引擎的算法!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
OpenSSL是一个开源的加密库,它提供了一系列的密码学功能,包括RSA签名。在C++中使用OpenSSL进行RSA签名,可以按照以下步骤进行: 1. 导入OpenSSL库:在C++代码中,需要包含OpenSSL的头文件,并链接OpenSSL的库文件。 2. 生成RSA密钥对:使用OpenSSL的函数生成RSA密钥对,包括公钥和私钥。 3. 加载私钥:将生成的私钥加载到内存中,以便后续进行签名操作。 4. 准备待签名数据:将待签名的数据准备好,可以是任意长度的字节流。 5. 进行签名:使用OpenSSL提供的函数,将待签名数据和私钥作为输入,生成签名结果。 下面是一个简单的示例代码,演示了如何使用OpenSSL进行RSA签名: ```cpp #include <openssl/rsa.h> #include <openssl/pem.h> // 生成RSA密钥对 RSA* generateRSAKey() { RSA* rsa = RSA_new(); BIGNUM* bne = BN_new(); unsigned long e = RSA_F4; BN_set_word(bne, e); RSA_generate_key_ex(rsa, 2048, bne, NULL); return rsa; } // 加载私钥 EVP_PKEY* loadPrivateKey(RSA* rsa) { EVP_PKEY* pkey = EVP_PKEY_new(); EVP_PKEY_assign_RSA(pkey, rsa); return pkey; } // 进行RSA签名 std::string rsaSign(const std::string& data, EVP_PKEY* privateKey) { EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); EVP_SignInit(mdctx, EVP_sha256()); EVP_SignUpdate(mdctx, data.c_str(), data.length()); size_t siglen = EVP_PKEY_size(privateKey); std::vector<unsigned char> signature(siglen); EVP_SignFinal(mdctx, signature.data(), &siglen, privateKey); EVP_MD_CTX_free(mdctx); return std::string(signature.begin(), signature.end()); } int main() { // 生成RSA密钥对 RSA* rsa = generateRSAKey(); // 加载私钥 EVP_PKEY* privateKey = loadPrivateKey(rsa); // 准备待签名数据 std::string data = "Hello, World!"; // 进行RSA签名 std::string signature = rsaSign(data, privateKey); // 打印签名结果 for (unsigned char c : signature) { printf("%02x", c); } printf("\n"); // 释放资源 RSA_free(rsa); EVP_PKEY_free(privateKey); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值