X.509证书与证书请求生成原理及其应用(C/C++代码实现)

在网络安全领域,X.509 证书和证书请求发挥着关键作用,为通信双方的身份验证和数据传输安全筑牢根基。本文将深入探讨 X.509 证书与证书请求的生成原理,并对相关领域的关键细节进行剖析。

一、X.509证书与证书请求概述

X.509证书是遵循X.509国际标准的数字证书,广泛应用于网络安全、身份认证等领域。它是一种权威的电子文档,用于证明某个实体(如个人、组织、设备等)的身份和公钥的合法性。证书请求则是实体向证书颁发机构(CA)申请证书时提交的请求,包含了实体的公钥、身份信息等。

X.509证书的结构主要包括以下几个部分:

  1. 版本(Version):指示证书所遵循的X.509版本,常见的有v3等。
  2. 序列号(Serial Number):由CA分配的唯一标识符,用于区分不同的证书。
  3. 签名算法(Signature Algorithm):指定用于签署证书的算法,如SHA-256 with RSA等。
  4. 颁发者(Issuer):证书颁发机构的身份信息。
  5. 有效期(Validity):包括证书的起始时间和过期时间,限定证书的有效时间范围。
  6. 主体(Subject):证书持有者的身份信息,如域名、组织名称等。
  7. 公钥信息(Subject Public Key Info):包含证书持有者的公钥及其算法。
  8. 扩展(Extensions):可选的扩展项,用于增加证书的功能和信息,如主题备用名称(Subject Alternative Name)、密钥用法(Key Usage)等。
  9. 签名值(Signature Value):CA使用其私钥对证书内容进行签名后的值,用于验证证书的完整性和真实性。

证书请求的结构与证书类似,但不包含签名值和一些由CA填充的信息,如序列号、颁发者等。它主要包含主体信息、公钥信息以及可选的扩展等。

二、生成X.509证书的原理

(一)证书生成流程

  1. 生成密钥对:实体首先生成一对密钥,包括私钥和公钥。私钥由实体自己保存,公钥将包含在证书请求中。
  2. 创建证书请求:实体使用生成的公钥和自身的身份信息创建证书请求。证书请求中包含主体信息、公钥信息等。
  3. 提交给CA:实体将证书请求提交给可信的CA。CA会对请求中的信息进行验证,如主体身份的真实性等。
  4. CA签署证书:CA使用自己的私钥对证书请求进行签名,生成正式的X.509证书。签名过程涉及到对证书内容的哈希计算,然后用CA的私钥对哈希值进行加密。
  5. 颁发证书:CA将签署后的证书颁发给实体,实体可以将其用于各种需要身份验证和安全通信的场景。

(二)关键技术细节

  • 数字签名:CA使用私钥对证书内容进行签名,接收方可以通过CA的公钥验证签名,从而确认证书的完整性和真实性。数字签名基于非对称加密算法,如RSA、ECDSA等。以RSA为例,签名过程是将证书内容进行哈希运算,然后用CA的私钥对哈希值进行加密,得到签名值。验证时,接收方使用CA的公钥对签名值解密,得到原始哈希值,并对证书内容重新计算哈希,比较两者是否一致。
  • 公钥基础设施(PKI):X.509证书是PKI的核心组件之一。PKI包括证书颁发机构(CA)、注册机构(RA)、证书库等组件,共同协作实现数字证书的颁发、管理、撤销等功能。CA是PKI中的信任锚点,负责证书的签署和管理。RA协助CA进行证书申请者的身份验证等工作。证书库用于存储已颁发的证书,供用户查询和验证。
  • 证书链:在实际应用中,可能存在多个层级的CA,形成证书链。每个CA的证书由其上级CA签署,最终追溯到根CA。根CA的证书通常是自签名的。当验证一个证书时,需要构建证书链,从该证书的颁发者证书开始,逐级向上验证,直到根CA证书,确保整个链条的可信性。

三、生成X.509证书请求的原理

(一)证书请求生成流程

  1. 准备主体信息:申请者确定自己的身份信息,如组织名称、域名、地理位置等,这些信息将包含在证书请求中。
  2. 生成公钥和私钥:申请者使用加密算法生成一对公钥和私钥。私钥必须妥善保管,不能泄露。
  3. 构建证书请求:将主体信息、公钥以及可选的扩展信息组合成证书请求的结构。在构建过程中,可能需要对某些信息进行编码,如将主体信息按照X.500格式编码。
  4. 签名证书请求:申请者使用自己的私钥对证书请求进行签名,以证明请求的真实性。签名过程与证书签名类似,是对证书请求内容的哈希值进行加密。
  5. 提交给CA:将签名后的证书请求发送给CA,等待CA的审核和签署。

(二)关键技术细节

  • 主体备用名称(SAN)扩展:在现代应用中,尤其是涉及多个域名或不同类型的标识时,SAN扩展非常重要。它允许在证书中包含多个不同的主体标识,如多个域名、IP地址、电子邮件地址等。例如,一个网站可能需要同时支持example.com和www.example.com两个域名,通过SAN扩展可以将这两个域名都包含在证书中,使证书在访问这两个域名时都有效。
  • 密钥用法和扩展密钥用法:这些扩展定义了证书中公钥的使用方式,如是否用于数字签名、密钥交换、数据加密等。扩展密钥用法进一步细化了公钥在特定场景中的用途,如服务器认证、客户端认证、代码签名等。这些信息对于确保证书的正确使用和安全策略的实施至关重要。
  • 加密算法选择:在生成证书请求时,需要选择合适的加密算法来生成公钥和私钥对,以及进行签名。不同的算法在安全性、性能等方面有不同的特点。例如,RSA算法在较长密钥长度下具有较高的安全性,但计算速度相对较慢;而ECDSA算法在较短密钥长度下也能提供足够的安全性,且计算效率较高。选择合适的算法需要综合考虑应用场景、安全性要求和性能需求等因素。

四、X.509证书与证书请求在不同领域的应用

(一)网络安全领域

  • HTTPS协议:在网站安全方面,X.509证书是HTTPS协议的核心组件。当用户访问采用HTTPS的网站时,浏览器会验证网站服务器提供的X.509证书,确保与服务器的通信是加密的,并且服务器的身份是可信的。证书验证包括检查证书是否由可信的CA颁发、是否在有效期内、是否与访问的域名匹配等。只有验证通过后,浏览器才会建立安全的HTTPS连接,保护用户数据在传输过程中的安全。
  • VPN连接:在虚拟专用网络(VPN)中,X.509证书用于对VPN服务器和客户端进行身份认证。客户端在连接VPN服务器时,双方会交换并验证各自的证书,确保存在的连接是合法的,防止中间人攻击等安全威胁。通过证书认证,VPN连接可以建立安全的加密通道,使远程用户能够安全地访问企业内部网络资源。

(二)身份认证领域

  • 公钥基础设施(PKI)体系:在PKI中,X.509证书作为身份凭证,广泛应用于各种身份认证场景。用户、设备等实体通过持有有效的X.509证书来证明自己的身份。例如,在企业内部的员工身份认证系统中,员工可以使用包含在智能卡中的X.509证书进行登录和身份验证,相比传统的用户名和密码方式,证书提供了更高的安全性和抗攻击性。
  • 电子政务和电子商务:在电子政务和电子商务领域,X.509证书用于确保交易双方的身份真实性和合法性。政府部门在提供在线服务时,可以要求用户使用X.509证书进行身份认证,确保服务的安全性和数据的保密性。在电子商务中,商家和消费者之间通过证书进行身份验证和安全通信,保护交易信息和支付数据的安全,防止欺诈和信息泄露。

(三)物联网(IoT)领域

  • 设备身份认证:在物联网环境中,大量的设备需要接入网络并进行通信。X.509证书可以为这些设备提供可靠的身份认证机制。每个物联网设备可以拥有自己的X.509证书,用于在连接到物联网平台或与其他设备通信时证明自己的身份。这有助于防止未经授权的设备接入网络,保障物联网系统的安全性和稳定性。
  • 数据安全传输:物联网设备之间以及设备与服务器之间的数据传输需要保证安全性和完整性。通过使用X.509证书建立安全的TLS/SSL连接,可以对数据进行加密传输,防止数据在传输过程中被窃取或篡改。这对于保护敏感的物联网数据,如用户个人信息、设备运行数据等,具有重要意义。

五、用于生成X.509证书和证书请求代码实现

...
typedef size_t x509cert_encoder(const struct x509cert_item *, unsigned char *);
enum {
	X509CERT_ASN1_INTEGER         = 0x02,
	X509CERT_ASN1_BITSTRING       = 0x03,
	X509CERT_ASN1_OCTETSTRING     = 0x04,
	X509CERT_ASN1_NULL            = 0x05,
	X509CERT_ASN1_OID             = 0x06,
	X509CERT_ASN1_UTF8STRING      = 0x0c,
	X509CERT_ASN1_PRINTABLESTRING = 0x13,
	X509CERT_ASN1_IA5STRING       = 0x16,
	X509CERT_ASN1_UTCTIME         = 0x17,
	X509CERT_ASN1_GENERALIZEDTIME = 0x18,
	X509CERT_ASN1_SEQUENCE        = 0x30,
	X509CERT_ASN1_SET             = 0x31,
};

/* ASN.1 item */
struct x509cert_item {
	int tag;
	size_t len;
	const void *val;
	x509cert_encoder *enc;
};

size_t x509cert_encode(const struct x509cert_item *, unsigned char *);

struct x509cert_skey {
	int type;
	union {
		const br_rsa_private_key *rsa;
		const br_ec_private_key *ec;
	} u;
};

/* X.501 RelativeDistinguishedName */
struct x509cert_rdn {
	const unsigned char *oid;
	struct x509cert_item val;
};

/* X.501 DistinguishedName */
struct x509cert_dn {
	struct x509cert_rdn *rdn;
	size_t rdn_len;
};

enum {
	X509CERT_SAN_OTHERNAME  = 0xa0,  /* SEQUENCE { OID, ANY } */
	X509CERT_SAN_RFC822NAME = 0x81,  /* IA5String */
	X509CERT_SAN_DNSNAME    = 0x82,  /* IA5String */
	X509CERT_SAN_URI        = 0x86,  /* IA5String */
	X509CERT_SAN_IPADDRESS  = 0x87,  /* OCTET STRING */
};

/* PKCS#10 CertificateRequestInfo */
struct x509cert_req {
	struct x509cert_item subject;
	br_x509_pkey pkey;
	struct x509cert_item *alts;
	size_t alts_len;
};

/* X.509 TBSCertificate */
struct x509cert_cert {
	struct x509cert_req *req;
	unsigned char serial[20];
	int key_type;  /* BR_KEYTYPE_* */
	int hash_id;  /* br_*_ID */
	struct x509cert_item issuer;
	time_t notbefore, notafter;
	int ca;
};

extern x509cert_encoder x509cert_dn_encoder;
extern x509cert_encoder x509cert_req_encoder;
extern x509cert_encoder x509cert_cert_encoder;
extern const unsigned char x509cert_oid_CN[];
extern const unsigned char x509cert_oid_L[];
extern const unsigned char x509cert_oid_ST[];
extern const unsigned char x509cert_oid_O[];
extern const unsigned char x509cert_oid_OU[];
extern const unsigned char x509cert_oid_C[];
extern const unsigned char x509cert_oid_STREET[];
extern const unsigned char x509cert_oid_DC[];
extern const unsigned char x509cert_oid_UID[];
size_t x509cert_encode_dn(const struct x509cert_dn *, unsigned char *);
size_t x509cert_dn_string_rdn_len(const char *);
int x509cert_parse_dn_string(struct x509cert_rdn *, char *);
size_t x509cert_encode_req(const struct x509cert_req *, unsigned char *);
size_t x509cert_encode_cert(const struct x509cert_cert *, unsigned char *);
size_t x509cert_sign(const struct x509cert_item *, const struct x509cert_skey *, const br_hash_class *, unsigned char *);
...
static void compute_pkey(br_x509_pkey *pkey, const struct x509cert_skey *skey)
{
...

	switch (skey->type) {
	case BR_KEYTYPE_RSA:
		mod = br_rsa_compute_modulus_get_default();
		exp = br_rsa_compute_pubexp_get_default();
		len = mod(NULL, skey->u.rsa);
		if (len == 0) {
			fputs("failed to compute RSA public key modulus\n", stderr);
			exit(1);
		}
		e = exp(skey->u.rsa);
		if (e == 0) {
			fputs("failed to compute RSA public exponent\n", stderr);
			exit(1);
		}

		len += 4;
		buf = xmalloc(len);
		pkey->key.rsa.e = buf;
		pkey->key.rsa.elen = 4;
		buf[0] = e >> 24;
		buf[1] = e >> 16;
		buf[2] = e >> 8;
		buf[3] = e;
		pkey->key.rsa.n = buf + pkey->key.rsa.elen;
		pkey->key.rsa.nlen = mod(pkey->key.rsa.n, skey->u.rsa);
		break;
	case BR_KEYTYPE_EC:
		ec = br_ec_get_default();
		len = br_ec_compute_pub(ec, NULL, NULL, skey->u.ec);
		if (len == 0) {
			fputs("failed to compute EC public key", stderr);
			exit(1);
		}
		buf = xmalloc(len);
		br_ec_compute_pub(ec, &pkey->key.ec, buf, skey->u.ec);
		break;
	}
	pkey->key_type = skey->type;
}

static br_rsa_private_key *clone_rsa_skey(const br_rsa_private_key *s)
{
	struct {
		br_rsa_private_key key;
		unsigned char buf[];
	} *d;

	d = xmalloc(sizeof(*d) + s->plen + s->qlen + s->dplen + s->dqlen + s->iqlen);
	d->key = *s;
	d->key.p = d->buf;
	d->key.q = d->key.p + d->key.plen;
	d->key.dp = d->key.q + d->key.qlen;
	d->key.dq = d->key.dp + d->key.dplen;
	d->key.iq = d->key.dq + d->key.dqlen;
	memcpy(d->key.p, s->p, s->plen);
	memcpy(d->key.q, s->q, s->qlen);
	memcpy(d->key.dp, s->dp, s->dplen);
	memcpy(d->key.dq, s->dq, s->dqlen);
	memcpy(d->key.iq, s->iq, s->iqlen);
	return &d->key;
}

static br_ec_private_key *clone_ec_skey(const br_ec_private_key *s)
{
	struct {
		br_ec_private_key key;
		unsigned char buf[];
	} *d;

	d = xmalloc(sizeof(*d) + s->xlen);
	d->key = *s;
	d->key.x = d->buf;
	memcpy(d->key.x, s->x, s->xlen);
	return &d->key;
}

static void append_skey(void *ctx, const void *src, size_t len)
{
	br_skey_decoder_push(ctx, src, len);
}

static void load_key(const char *name, br_x509_pkey *pkey, struct x509cert_skey *skey)
{
...

	f = fopen(name, "r");
	if (!f) {
		fprintf(stderr, "open %s: %s\n", name, strerror(errno));
		exit(1);
	}

	br_pem_decoder_init(&pemctx);
	br_skey_decoder_init(&keyctx);
	tmpkey.type = 0;
	while (!tmpkey.type) {
		if (len == 0) {
			if (feof(f))
				break;
			len = fread(buf, 1, sizeof(buf), f);
			if (ferror(f)) {
				fprintf(stderr, "read %s: %s\n", name, strerror(errno));
				exit(1);
			}
			pos = buf;
		}
		n = br_pem_decoder_push(&pemctx, pos, len);
		pos += n;
		len -= n;
		switch (br_pem_decoder_event(&pemctx)) {
		case BR_PEM_BEGIN_OBJ:
			pemname = br_pem_decoder_name(&pemctx);
			if (strcmp(pemname, BR_ENCODE_PEM_PKCS8) == 0 ||
			    strcmp(pemname, BR_ENCODE_PEM_RSA_RAW) == 0 ||
			    strcmp(pemname, BR_ENCODE_PEM_EC_RAW) == 0)
			{
				br_pem_decoder_setdest(&pemctx, append_skey, &keyctx);
				found = 1;
			}
			break;
		case BR_PEM_END_OBJ:
			if (!found)
				break;
			err = br_skey_decoder_last_error(&keyctx);
			if (err) {
				fprintf(stderr, "parse %s: error %d\n", name, err);
				exit(1);
			}
			tmpkey.type = br_skey_decoder_key_type(&keyctx);
			break;
		case BR_PEM_ERROR:
			fprintf(stderr, "parse %s: PEM decoding error\n", name);
			exit(1);
		}
	}

	switch (tmpkey.type) {
	case BR_KEYTYPE_RSA:
		tmpkey.u.rsa = br_skey_decoder_get_rsa(&keyctx);
		if (skey)
			skey->u.rsa = clone_rsa_skey(tmpkey.u.rsa);
		break;
	case BR_KEYTYPE_EC:
		tmpkey.u.ec = br_skey_decoder_get_ec(&keyctx);
		if (skey)
			skey->u.ec = clone_ec_skey(tmpkey.u.ec);
		break;
	default:
		fprintf(stderr, "parse %s: unsupported key type\n", name);
		exit(1);
	}
	if (skey)
		skey->type = tmpkey.type;
	if (pkey)
		compute_pkey(pkey, &tmpkey);
}

static void append_dn(void *ctx, const void *buf, size_t len)
{
	struct x509cert_item *item = ctx;

	if (sizeof(issuerbuf) - item->len < len) {
		fprintf(stderr, "issuer DN is too long");
		exit(1);
	}
	memcpy(issuerbuf + item->len, buf, len);
	item->len += len;
}

static void append_x509(void *ctx, const void *buf, size_t len)
{
	br_x509_decoder_push(ctx, buf, len);
}

static void load_cert(const char *name, struct x509cert_item *item)
{
...

	f = fopen(name, "r");
	if (!f) {
		fprintf(stderr, "open %s: %s\n", name, strerror(errno));
		exit(1);
	}

	br_pem_decoder_init(&pemctx);
	br_x509_decoder_init(&x509ctx, append_dn, item);
	for (;;) {
		if (len == 0) {
			if (feof(f))
				break;
			len = fread(buf, 1, sizeof(buf), f);
			if (ferror(f)) {
				fprintf(stderr, "read %s: %s\n", name, strerror(errno));
				exit(1);
			}
			pos = buf;
		}
		n = br_pem_decoder_push(&pemctx, pos, len);
		pos += n;
		len -= n;
		switch (br_pem_decoder_event(&pemctx)) {
		case BR_PEM_BEGIN_OBJ:
			if (strcmp(br_pem_decoder_name(&pemctx), "CERTIFICATE") == 0) {
				br_pem_decoder_setdest(&pemctx, append_x509, &x509ctx);
				found = 1;
			}
			break;
		case BR_PEM_END_OBJ:
			if (!found)
				break;
			err = br_x509_decoder_last_error(&x509ctx);
			if (err) {
				fprintf(stderr, "parse %s: error %d\n", name, err);
				exit(1);
			}
			if (!br_x509_decoder_isCA(&x509ctx)) {
				fprintf(stderr, "issuer certificate is not a CA\n");
				exit(1);
			}
			break;
		case BR_PEM_ERROR:
			fprintf(stderr, "parse %s: PEM decoding error\n", name);
			exit(1);
		}
	}

	item->tag = 0;
	item->val = issuerbuf;
}

static int hex(int c)
{
	if ('0' <= c && c <= '9')
		return c - '0';
	switch (c) {
	case 'a': case 'A': return 10;
	case 'b': case 'B': return 11;
	case 'c': case 'C': return 12;
	case 'd': case 'D': return 13;
	case 'e': case 'E': return 14;
	case 'f': case 'F': return 15;
	}
	fprintf(stderr, "invalid hex character '%c'", c);
	exit(1);
}

static void parse_serial(const char *s)
{
	if (s) {
		unsigned char *dst;
		const char *end = s + strlen(s);

		if (end == s || (end - s) % 2 != 0) {
			fprintf(stderr, "invalid serial\n");
			exit(1);
		}
		if ((end - s) / 2 > sizeof(cert.serial)) {
			fprintf(stderr, "serial is too large\n");
			exit(1);
		}
		dst = cert.serial + sizeof(cert.serial) - (end - s) / 2;
		for (; s != end; s += 2)
			*dst++ = hex(s[0]) << 4 | hex(s[1]);
	} else if (getentropy(cert.serial + sizeof(cert.serial) - 16, 16) != 0) {
		perror("getentropy");
		exit(1);
	}
}

int main(int argc, char *argv[])
{
...
	if (argc > 3)
		req.alts = xmallocarray(argc - 3, sizeof(req.alts[0]));

	argv0 = argc ? argv[0] : "x509cert";
	ARGBEGIN {
	case 'a':
		add_alt(EARGF(usage()));
		break;
	case 'C':
		cert.ca = 1;
		break;
	case 'c':
		certfile = EARGF(usage());
		break;
	case 'b':
		cert.notbefore = strtoul(EARGF(usage()), &end, 0);
		if (*end)
			usage();
		break;
	case 'd':
		duration = strtoul(EARGF(usage()), &end, 0);
		switch (*end) {
		case 'd': duration *= 86400; ++end; break;
		case 'y': duration *= 31536000; ++end; break;
		}
		if (*end)
			usage();
		break;
	case 'k':
		keyfile = EARGF(usage());
		break;
	case 'r':
		rflag = 1;
		break;
	case 's':
		serial = EARGF(usage());
		break;
	default:
		usage();
	} ARGEND
	if (argc < 1 || argc > 2 || (rflag && (certfile || cert.ca)) || !certfile != !keyfile)
		usage();

	if (argc > 1) {
		subject.rdn_len = x509cert_dn_string_rdn_len(argv[1]);
		subject.rdn = xmallocarray(subject.rdn_len, sizeof(subject.rdn[0]));
		if (!x509cert_parse_dn_string(subject.rdn, argv[1])) {
			fputs("invalid subject name\n", stderr);
			return 1;
		}
	}

	if (keyfile) {
		load_key(argv[0], &req.pkey, NULL);
		load_key(keyfile, NULL, &skey);
		load_cert(certfile, &cert.issuer);
	} else {
		load_key(argv[0], &req.pkey, &skey);
		cert.issuer = req.subject;
	}

	if (rflag) {
		banner = "CERTIFICATE REQUEST";
		item.enc = x509cert_req_encoder;
		item.val = &req;
	} else {
		banner = "CERTIFICATE";
		parse_serial(serial);
		if (!cert.notbefore)
			cert.notbefore = time(NULL);
		cert.notafter = duration == -1 ? 253402300799 : cert.notbefore + duration;
		cert.key_type = skey.type;
		cert.hash_id = br_sha256_ID;
		item.enc = x509cert_cert_encoder;
		item.val = &cert;
	}
	outlen = x509cert_sign(&item, &skey, &br_sha256_vtable, NULL);
	if (!outlen) {
		fputs("unsupported key\n", stderr);
		return 1;
	}
	out = xmalloc(outlen);
	outlen = x509cert_sign(&item, &skey, &br_sha256_vtable, out);
	if (!outlen) {
		fputs("signing failed\n", stderr);
		return 1;
	}
	pemlen = br_pem_encode(NULL, out, outlen, banner, BR_PEM_LINE64);
	pem = xmalloc(pemlen + 1);
	br_pem_encode(pem, out, outlen, banner, BR_PEM_LINE64);
	if (fwrite(pem, 1, pemlen, stdout) != pemlen || fflush(stdout) != 0) {
		perror("write");
		return 1;
	}
}

If you need the complete source code, please add the WeChat number (c17865354792)

测试:

x509_cert [-C] [-a altname]… [-c issuercert] [-k issuerkey] [-b notbefore] [-d duration] [-s serial] key [subject]
x509_cert -r [-a altname]… key [subject]

总结

在实际应用中,有许多工具和库可以帮助生成和管理X.509证书及证书请求。例如,OpenSSL是一个广泛使用的开源工具包,它提供了丰富的命令行工具和库函数,用于生成密钥、创建证书请求、签署证书等操作。通过OpenSSL,用户可以灵活地按照自己的需求定制证书和证书请求的生成过程。此外,还有其他一些专门的证书管理工具和平台,它们提供了图形化界面和更高级的功能,方便企业和组织进行大规模的证书管理和颁发工作。这些工具和平台通常集成了CA功能、证书数据库管理、自动化的证书颁发和续期流程等功能,提高了证书管理的效率和安全性。

总之,X.509证书和证书请求在现代信息安全领域发挥着至关重要的作用。理解它们的生成原理、结构以及在不同领域的应用,有助于我们更好地运用这些技术来保障网络通信、身份认证和数据安全等方面的需求。随着信息技术的不断发展,X.509证书相关的技术和标准也在不断演进,以适应日益复杂的网络安全环境和多样化的需求。

Welcome to follow WeChat official account【程序猿编码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值