基于BearSSL实现自签名证书双向认证测试代码

客户端、服务器端双向认证大致过程:可以参考:https://blog.csdn.net/fengbingchun/article/details/106856332

(1). 客户端发起连接请求;

(2). 服务器端返回消息,包含服务器端证书server.crt;

(3). 客户端验证服务器端证书server.crt的合法性;

(4). 客户端向服务器端发送客户端证书client.crt;

(5). 服务器端验证客户端证书client.crt,并将选定的加密方案发给客户端;

(6). 客户端发送随机生成的对称公钥给服务器端;

(7). 服务器端获取客户端发送的对称公钥;

接下来双方即可进行数据传输。

        使用OpenSSL生成自签名证书:可以参考:https://blog.csdn.net/fengbingchun/article/details/107386847

        1. 生成自签名服务器端证书:

(1). 产生长度为3072的rsa私钥server.key;

(2). 创建服务器端证书签名请求(Certificate Signing Request)文件server.csr: CN填写本地测试机的IP,或域名,这里填写的IP或域名需要与代码中保持一致,填写域名可能需要修改本地/etc/hosts文件;

(3). 创建服务器端证书server.crt;

执行命令如下:

LD_LIBRARY_PATH=../lib ./openssl genrsa -out server.key 3072
LD_LIBRARY_PATH=../lib ./openssl req -new -out server.csr -key server.key -subj  "/C=cn/ST=beijing/L=haidian/O=FBC/OU=test/CN=10.4.96.33/emailAddress=fengbingchun@163.com"
LD_LIBRARY_PATH=../lib ./openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650

2. 生成自签名客户端证书:

(1). 产生长度为3072的rsa私钥client.key;

(2). 创建客户端端证书签名请求client.csr;

(3). 创建客户端证书client.crt;

执行命令如下:

LD_LIBRARY_PATH=../lib ./openssl genrsa -out client.key 3072
LD_LIBRARY_PATH=../lib ./openssl req -new -out client.csr -key client.key -subj "/C=cn/ST=beijing/L=haidian/O=Spring/OU=server_test/CN=XXXX/emailAddress=Spring@client_test.com"
LD_LIBRARY_PATH=../lib ./openssl x509 -req -in client.csr -out client.crt -signkey client.key -days 3650

通过bearssl的工具brssl将相关证书文件生成bearssl支持的C代码

(1). 将服务端证书server.crt生成trust anchors,将其存放在trust_anchors.inc文件中;

(2). 将服务器端证书server.crt生成server chain,将其存放在chain.inc文件中;

(3). 将服务器端私钥server.key生成server rsa,将其存放在key.inc文件中;

(4). 将客户端证书client.crt生成trust anchors,将其存放在trust_anchors.inc文件中;

(5). 将客户端证书client.crt生成client chain,将其存放在chain.inc文件中;

(6). 将客户端私钥client.key生成client rsa,将其存放在key.inc文件中;

执行命令如下:

./brssl ta server.crt
./brssl chain server.crt
./brssl skey -C server.key
./brssl ta client.crt
./brssl chain client.crt
./brssl skey -C client.key

测试代码段如下:

namespace {

// reference: bearssl/samples: client_basic.c/server_basic.c
// 客户端连接服务器:host为服务器端ipv4或域名,port为端口号
SOCKET client_connect(const char *host, const char *port)
{
	struct addrinfo hints, *si;
	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	// getaddrinfo: 获取主机信息,既支持ipv4也支持ipv6
	auto err = getaddrinfo(host, port, &hints, &si);
	if (err != 0) {
		fprintf(stderr, "fail to getaddrinfo: %s\n", gai_strerror(err));
		return INVALID_SOCKET;
	}

	SOCKET fd = INVALID_SOCKET;
	struct addrinfo* p = nullptr;
	for (p = si; p != nullptr; p = p->ai_next) {
		struct sockaddr* sa = (struct sockaddr *)p->ai_addr;
		if (sa->sa_family != AF_INET) // 仅处理AF_INET
			continue;

		struct in_addr addr;
		addr.s_addr = ((struct sockaddr_in *)(p->ai_addr))->sin_addr.s_addr;
		// inet_ntoa: 将二进制类型的IP地址转换为字符串类型
		fprintf(stdout, "server ip: %s, family: %d, socktype: %d, protocol: %d\n",
			inet_ntoa(addr), p->ai_family, p->ai_socktype, p->ai_protocol);

		// 创建流式套接字
		fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
		if (fd < 0 || fd == INVALID_SOCKET) {
			auto err_code = get_error_code();
			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to socket: %d, error code: %d, message: %s\n", fd, err_code, ec.message().c_str());
			continue;
		}

		// 连接,connect函数的第二参数是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址,以及协议类型
#ifdef __linux__
		auto ret = connect(fd, p->ai_addr, p->ai_addrlen); // 在windows上直接调用此语句会返回-1,还未查到原因?
#else
		struct sockaddr_in server_addr;
		memset(&server_addr, 0, sizeof(server_addr));
		server_addr.sin_family = AF_INET;
		server_addr.sin_port = htons(server_port_);
		auto ret = inet_pton(AF_INET, server_ip_, &server_addr.sin_addr);
		if (ret != 1) {
			fprintf(stderr, "fail to inet_pton: %d\n", ret);
			return -1;
		}
		ret = connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
#endif
		if ( ret < 0) {
			auto err_code = get_error_code();
			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to connect: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());
			close(fd);
			continue;
		}

		break;
	}

	if (p == nullptr) {
		freeaddrinfo(si);
		fprintf(stderr, "fail to socket or connect\n");
		return INVALID_SOCKET;
	}

	freeaddrinfo(si);
	return fd;
}

// 接收数据
int sock_read(void *ctx, unsigned char *buf, size_t len)
{
	for (;;) {
#ifdef _MSC_VER
		auto rlen = recv(*(int*)ctx, (char*)buf, len, 0);
#else
		auto rlen = recv(*(int*)ctx, (char*)buf, len, 0);
#endif
		//fprintf(stderr, "recv length: %d\n", rlen);
		if (rlen <= 0) {
			if (rlen < 0 && errno == EINTR) {
				continue;
			}

			auto err_code = get_error_code();
			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to recv: %d, err code: %d, message: %s\n", rlen, err_code, ec.message().c_str());
			return -1;
		}
		return (int)rlen;
	}
}

// 发送数据
int sock_write(void *ctx, const unsigned char *buf, size_t len)
{
	for (;;) {
#ifdef _MSC_VER
		auto wlen = send(*(int *)ctx, (const char*)buf, len, 0);
#else
		// MSG_NOSIGNAL: 禁止send函数向系统发送异常消息
		auto wlen = send(*(int *)ctx, buf, len, MSG_NOSIGNAL);
#endif
		//fprintf(stderr, "send length: %d\n", wlen);
		if (wlen <= 0) {
			if (wlen < 0 && errno == EINTR) {
				continue;
			}

			auto err_code = get_error_code();
			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to send: %d, err code: %d, message: %s\n", wlen, err_code, ec.message().c_str());
			return -1;
		}
		return (int)wlen;
	}
}

// 服务器端绑定、监听
SOCKET server_bind_listen(const char *host, const char *port)
{
	struct addrinfo hints, *si;
	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	auto ret = getaddrinfo(host, port, &hints, &si);
	if (ret != 0) {
		fprintf(stderr, "fail to getaddrinfo: %s\n", gai_strerror(ret));
		return INVALID_SOCKET;
	}

	SOCKET fd = INVALID_SOCKET;
	struct addrinfo* p = nullptr;
	for (p = si; p != nullptr; p = p->ai_next) {
		struct sockaddr *sa = (struct sockaddr *)p->ai_addr;
		if (sa->sa_family != AF_INET) // 仅处理AF_INET
			continue;

		struct in_addr addr;
		addr.s_addr = ((struct sockaddr_in *)(p->ai_addr))->sin_addr.s_addr;
		// inet_ntoa: 将二进制类型的IP地址转换为字符串类型
		fprintf(stdout, "server ip: %s, family: %d, socktype: %d, protocol: %d\n",
			inet_ntoa(addr), p->ai_family, p->ai_socktype, p->ai_protocol);

		// 创建流式套接字
		fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
		if (fd < 0 || fd == INVALID_SOCKET) {
			auto err_code = get_error_code();
			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to socket: %d, error code: %d, message: %s\n", fd, err_code, ec.message().c_str());
			continue;
		}

		int opt = 1;
#ifdef _MSC_VER
		ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof opt);
#else
		ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
#endif
		if (ret < 0) {
			fprintf(stderr, "fail to setsockopt: send\n");
			return INVALID_SOCKET;
		}

		// 绑定地址端口
		ret = bind(fd, sa, sizeof(*sa));
		if (ret < 0) {
			auto err_code = get_error_code();
			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to bind: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());
			close(fd);
			continue;
		}

		break;
	}

	if (p == nullptr) {
		freeaddrinfo(si);
		fprintf(stderr, "fail to socket or bind\n");
		return INVALID_SOCKET;
	}

	freeaddrinfo(si);

	ret = listen(fd, server_listen_queue_length_);
	if (ret < 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "fail to listen: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());
		close(fd);
		return INVALID_SOCKET;
	}

	return fd;
}

// 服务器端接收客户端的连接
SOCKET server_accept(SOCKET server_fd)
{
	struct sockaddr_in sa;
	socklen_t sa_len = sizeof sa;
	auto fd = accept(server_fd, (struct sockaddr*)&sa, &sa_len);
	if (fd < 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "fail to accept: %d, error code: %d, message: %s\n", fd, err_code, ec.message().c_str());
		return -1;
	}

	if (sa.sin_family != AF_INET) {
		fprintf(stderr, "fail: sa_family should be equal AF_INET: %d\n", sa.sin_family);
		return -1;
	}

	struct in_addr addr;
	addr.s_addr = sa.sin_addr.s_addr;
	// inet_ntoa: 将二进制类型的IP地址转换为字符串类型
	fprintf(stdout, "client ip: %s\n", inet_ntoa(addr));
	return fd;
}

// Check whether we closed properly or not
void check_ssl_error(const br_ssl_client_context& sc)
{
	if (br_ssl_engine_current_state(&sc.eng) == BR_SSL_CLOSED) {
		auto ret = br_ssl_engine_last_error(&sc.eng);
		if (ret == 0) {
			fprintf(stdout, "closed properly\n");
		} else {
			fprintf(stderr, "SSL error %d\n", ret);
		}
	} else {
		fprintf(stderr, "socket closed without proper SSL termination\n");
	}
}

void check_ssl_error(const br_ssl_server_context& ss)
{
	if (br_ssl_engine_current_state(&ss.eng) == BR_SSL_CLOSED) {
		auto ret = br_ssl_engine_last_error(&ss.eng);
		if (ret == 0) {
			fprintf(stdout, "closed properly\n");
		} else {
			fprintf(stderr, "SSL error %d\n", ret);
		}
	} else {
		fprintf(stderr, "socket closed without proper SSL termination\n");
	}
}

// reference: bearssl/samples/custom_profile.c
void set_ssl_engine_suites(br_ssl_client_context sc)
{
	static const uint16_t suites[] = {
		BR_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
		BR_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
		BR_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
		BR_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
		BR_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
		BR_TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
		BR_TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
		BR_TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
		BR_TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
		BR_TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
		BR_TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
		BR_TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
		BR_TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
		BR_TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
		BR_TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256,
		BR_TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256,
		BR_TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384,
		BR_TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384,
		BR_TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256,
		BR_TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256,
		BR_TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384,
		BR_TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384,
		BR_TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA,
		BR_TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,
		BR_TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA,
		BR_TLS_ECDH_RSA_WITH_AES_256_CBC_SHA,
		BR_TLS_RSA_WITH_AES_128_GCM_SHA256,
		BR_TLS_RSA_WITH_AES_256_GCM_SHA384,
		BR_TLS_RSA_WITH_AES_128_CBC_SHA256,
		BR_TLS_RSA_WITH_AES_256_CBC_SHA256,
		BR_TLS_RSA_WITH_AES_128_CBC_SHA,
		BR_TLS_RSA_WITH_AES_256_CBC_SHA,
		BR_TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA,
		BR_TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
		BR_TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA,
		BR_TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA,
		BR_TLS_RSA_WITH_3DES_EDE_CBC_SHA
	};

	br_ssl_engine_set_suites(&sc.eng, suites, (sizeof suites) / (sizeof suites[0]));
}

} // namespace

int test_bearssl_self_signed_certificate_client()
{
#ifdef _MSC_VER
	init_server_trust_anchors();
#endif

	const char* host = server_ip_;
	const char* port = std::to_string(server_port_).c_str();

	// Open the socket to the target server
	SOCKET fd = client_connect(host, port);
	if (fd < 0 || fd == INVALID_SOCKET) {
		fprintf(stderr, "fail to client connect: %d\n", fd);
		return -1;
	}

	// Initialise the client context
	br_ssl_client_context sc;
	br_x509_minimal_context xc;
	br_ssl_client_init_full(&sc, &xc, SERVER_TAs, SERVER_TAs_NUM);

	set_ssl_engine_suites(sc);

	// 以下单条语句用于双向认证中
	br_ssl_client_set_single_rsa(&sc, CLIENT_CHAIN, CLIENT_CHAIN_LEN, &CLIENT_RSA, br_rsa_pkcs1_sign_get_default());

	// Set the I/O buffer to the provided array
	unsigned char iobuf[BR_SSL_BUFSIZE_BIDI];
	br_ssl_engine_set_buffer(&sc.eng, iobuf, sizeof iobuf, 1);

	// Reset the client context, for a new handshake
	// 若设置br_ssl_client_reset函数的第二个参数为nullptr,则客户端无需验证服务器端的ip或域名,
	// 若第二个参数不为nullptr,则这里的host与使用命令生成server.csr时的CN要保持一致
	auto ret = br_ssl_client_reset(&sc, host/*nullptr*/, 0);
	if ( ret == 0) {
		check_ssl_error(sc);
		fprintf(stderr, "fail to br_ssl_client_reset: %d\n", ret);
		return -1;
	}

	// Initialise the simplified I/O wrapper context
	br_sslio_context ioc;
	br_sslio_init(&ioc, &sc.eng, sock_read, &fd, sock_write, &fd);

	// Write application data unto a SSL connection
	const char* source_buffer = "https://blog.csdn.net/fengbingchun";
	auto length = strlen(source_buffer) + 1; // 以空字符作为发送结束的标志
	ret = br_sslio_write_all(&ioc, source_buffer, length);
	if (ret < 0) {
		check_ssl_error(sc);
		fprintf(stderr, "fail to br_sslio_write_all: %d\n", ret);
		return -1;
	}

	// SSL is a buffered protocol: we make sure that all our request bytes are sent onto the wire
	ret = br_sslio_flush(&ioc);
	if (ret < 0) {
		check_ssl_error(sc);
		fprintf(stderr, "fail to br_sslio_flush: %d\n", ret);
		return -1;
	}

	// Read the server's response
	std::vector<char> vec;
	for (;;) {
		unsigned char tmp[512];
		ret = br_sslio_read(&ioc, tmp, sizeof tmp);
		if (ret < 0) {
			check_ssl_error(sc);
			fprintf(stderr, "fail to br_sslio_read: %d\n", ret);
			return -1;
		}

		bool flag = false;
		std::for_each(tmp, tmp + ret, [&flag, &vec](const char& c) {
			if (c == '\0') flag = true; // 以空字符作为接收结束的标志
			else vec.emplace_back(c);
		});

		if (flag == true) break;
	}

	fprintf(stdout, "server's response: ");
	std::for_each(vec.data(), vec.data() + vec.size(), [](const char& c){
		fprintf(stdout, "%c", c);
	});
	fprintf(stdout, "\n");

	// Close the SSL connection
	if (fd >= 0) {
		ret = br_sslio_close(&ioc);
		if (ret < 0) {
			check_ssl_error(sc);
			fprintf(stderr, "fail to br_sslio_close: %d\n", ret);
			return -1;
		}
	}

	// Close the socket
	close(fd);

	return 0;
}

int test_bearssl_self_signed_certificate_server()
{
#ifdef _MSC_VER
	init_client_trust_anchors();
#endif

	// Open the server socket
	SOCKET fd = server_bind_listen(server_ip_, std::to_string(server_port_).c_str());
	if (fd < 0 || fd == INVALID_SOCKET) {
		fprintf(stderr, "fail to server_bind_listen: %d\n", fd);
		return -1;
	}

	// Process each client, one at a time
	for (;;) {
		SOCKET fd2 = server_accept(fd);
		if (fd2 < 0 || fd2 == INVALID_SOCKET) {
			fprintf(stderr, "fail to server_accept: %d\n", fd2);
			return -1;
		}

		// Initialise the context with the cipher suites and algorithms
		// SSL server profile: full_rsa
		br_ssl_server_context sc;
		br_ssl_server_init_full_rsa(&sc, SERVER_CHAIN, SERVER_CHAIN_LEN, &SERVER_RSA);

		// 以下8条语句用于双向认证中
		br_x509_minimal_context xc;
		br_x509_minimal_init(&xc, &br_sha1_vtable, CLIENT_TAs, CLIENT_TAs_NUM);
		br_ssl_engine_set_default_rsavrfy(&sc.eng);
		br_ssl_engine_set_default_ecdsa(&sc.eng);
		br_x509_minimal_set_rsa(&xc, br_rsa_pkcs1_vrfy_get_default());
		br_x509_minimal_set_ecdsa(&xc, br_ec_get_default(), br_ecdsa_vrfy_asn1_get_default());
		br_ssl_engine_set_x509(&sc.eng, &xc.vtable);
		br_ssl_server_set_trust_anchor_names_alt(&sc, CLIENT_TAs, CLIENT_TAs_NUM);

		// Set the I/O buffer to the provided array
		unsigned char iobuf[BR_SSL_BUFSIZE_BIDI];
		br_ssl_engine_set_buffer(&sc.eng, iobuf, sizeof iobuf, 1);

		// Reset the server context, for a new handshake
		auto ret = br_ssl_server_reset(&sc);
		if (ret == 0) {
			check_ssl_error(sc);
			fprintf(stderr, "fail to br_ssl_server_reset: %d\n", ret);
			return -1;
		}

		// Initialise the simplified I/O wrapper context
		br_sslio_context ioc;
		br_sslio_init(&ioc, &sc.eng, sock_read, &fd2, sock_write, &fd2);

		std::vector<char> vec;
		for (;;) {
			unsigned char tmp[512];
			ret = br_sslio_read(&ioc, tmp, sizeof tmp);
			if (ret < 0) {
				check_ssl_error(sc);
				fprintf(stderr, "fail to br_sslio_read: %d\n", ret);
				return -1;
			}

			bool flag = false;
			std::for_each(tmp, tmp + ret, [&flag, &vec](const char& c) {
				if (c == '\0') flag = true; // 以空字符作为接收结束的标志
				else vec.emplace_back(c);
			});

			if (flag == true) break;
		}

		fprintf(stdout, "message from the client: ");
		std::for_each(vec.data(), vec.data() + vec.size(), [](const char& c){
			fprintf(stdout, "%c", c);
		});
		fprintf(stdout, "\n");

		// Write a response and close the connection
		auto str = std::to_string(vec.size());
		std::vector<char> vec2(str.size() + 1);
		memcpy(vec2.data(), str.data(), str.size());
		vec2[str.size()] = '\0'; // 以空字符作为发送结束的标志
		ret = br_sslio_write_all(&ioc, vec2.data(), vec2.size());
		if (ret < 0) {
			check_ssl_error(sc);
			fprintf(stderr, "fail to br_sslio_write_all: %d\n", ret);
			return -1;
		}
		ret = br_sslio_close(&ioc);
		if (ret < 0) {
			check_ssl_error(sc);
			fprintf(stderr, "fail to br_sslio_close: %d\n", ret);
			return -1;
		}

		close(fd2);
	}

	return 0;
}

执行结果如下:服务器端为linux,客户端为windows,客户端发送"https://blog.csdn.net/fengbingchun",并以空格作为结束标志,服务器端接收后计算字符串的长度,然后将长度返回给客户端,客户端将从服务器端收到的信息print。以上代码中注释掉br_ssl_client_set_single_rsa和br_ssl_server_set_trust_anchor_names_alt等函数即可实现单向认证。

以上测试的完整代码见:GitHub OpenSSL_Test/funset_bearssl.cpp

GitHub:https://github.com/fengbingchun/OpenSSL_Test

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值