• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

k8s admission 学习使用 go 语言创建自签证书和签发证书

武飞扬头像
oceanweave
帮助1

使用go 创建证书

参考

流程

首先梳理一下流程 —— 创建秘钥 > 创建CA > 生成要颁发证书的秘钥 > 使用CA签发证书

  • 正常情况下,电脑会内置 CA 机构证书(用于验证其他证书),但这个 CA 机构证书是需要花钱注册的,所以我们自行创建个 CA 机构,用于签发和验证证书
  • 同时我们需要考虑将 CA 的机构证书,分发给需要验证证书的客户端(相当于内置到电脑中,只不过这一步是需要我们自己做,付费的 CA 机构证书是让厂商或浏览器直接嵌入了)
  1. 创建 CA 机构。
    1. 填写证书签名请求 CSR,
    2. 创建公私钥,
    3. 之后利用自己 csr 和私钥 对自己的 csr 和 公钥进行签名,也就是自签名,生成 CA 机构证书(证书内包含就是一些个人信息、网站信息、有效期和最重要的公钥(用于验证其他证书))
  2. 签名其他证书。
    1. 填写证书签名请求 CSR,
    2. 创建公私钥,
    3. 之后利用 CA 机构的 csr 和私钥 对自己的 csr 和 公钥进行签名,实现证书的签发

创建证书流程

创建证书的颁发机构

首先,会从将从创建 CA 开始。CA 会被用来签署其他证书

// 对证书进行签名
ca := &x509.Certificate{
	SerialNumber: big.NewInt(2019),
	Subject: pkix.Name{
        CommonName:    "domain name",
		Organization:  []string{"Company, INC."},
		Country:       []string{"US"},
		Province:      []string{""},
		Locality:      []string{"San Francisco"},
		StreetAddress: []string{"Golden Gate Bridge"},
		PostalCode:    []string{"94016"},
	},
	NotBefore:             time.Now(),  // 生效时间
	NotAfter:              time.Now().AddDate(10, 0, 0), // 过期时间 年月日
	IsCA:                  true, // 表示用于CA
    // openssl 中的 extendedKeyUsage = clientAuth, serverAuth 字段
	ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
    // openssl 中的 keyUsage 字段
	KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
	BasicConstraintsValid: true,
}
学新通

接下来需要对证书生成公钥和私钥

caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
	return err
}

然后生成证书:

// 第一个参数 rand.Reader 就是生成个随机数
// 第二个参数 ca 是 待签证书(此处后续改为其他证书,实现对其他证书的签名,此处是自签,用于构造 ca 机构证书)
// 第三个参数 ca 是 ca机构证书
// 第四个参数 &caPrivKey.PublicKey 对应着 第二个参数申请证书对应的公钥(因为证书就是为了安全传输公钥)
// 第五个参数 caPrivKey 是 ca 机构的私钥,用于签名证书,这样第二个证书被签名后,传给客户,客户就可以通过内置ca机构证书中的公钥,解开并验证第二个证书
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
if err != nil {
	return err
}

我们看到的证书内容是PEM编码后的,现在caBytes我们有了生成的证书,我们将其进行 PEM 编码以供以后使用:

caPEM := new(bytes.Buffer)
pem.Encode(caPEM, &pem.Block{
	Type:  "CERTIFICATE",
	Bytes: caBytes,
})caPrivKeyPEM := new(bytes.Buffer)
pem.Encode(caPrivKeyPEM, &pem.Block{
	Type:  "RSA PRIVATE KEY",
	Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey),
})

创建证书

证书的 x509.Certificate 与CA的 x509.Certificate 属性有稍微不同,需要进行一些修改

cert := &x509.Certificate{
	SerialNumber: big.NewInt(1658),
	Subject: pkix.Name{
        CommonName:    "domain name",
		Organization:  []string{"Company, INC."},
		Country:       []string{"US"},
		Province:      []string{""},
		Locality:      []string{"San Francisco"},
		StreetAddress: []string{"Golden Gate Bridge"},
		PostalCode:    []string{"94016"},
	},
    IPAddresses:  []net.IP{}, // 这里就是openssl配置文件中 subjectAltName 里的 IP:/IP=
    DNSNames:     []string{}, // 这里就是openssl配置文件中 subjectAltName 里的 DNS:/DNS=
	NotBefore:    time.Now(),
	NotAfter:     time.Now().AddDate(10, 0, 0),
	SubjectKeyId: []byte{1, 2, 3, 4, 6},
    // 这里就是openssl中的extendedKeyUsage
	ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
	KeyUsage:     x509.KeyUsageDigitalSignature,
}
学新通

注:这里会在证书中特别添加了 DNSIP (这个不是必须的),这个选项的增加代表的我们的证书可以支持多域名

为该证书创建私钥和公钥:

certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
	return err
}

使用CA签署证书

有了上述的内容后,可以创建证书并用CA进行签名

certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
if err != nil {
	return err
}

要保存成证书格式需要做PEM编码

certPEM := new(bytes.Buffer)
pem.Encode(certPEM, &pem.Block{
	Type:  "CERTIFICATE",
	Bytes: certBytes,
})certPrivKeyPEM := new(bytes.Buffer)
pem.Encode(certPrivKeyPEM, &pem.Block{
	Type:  "RSA PRIVATE KEY",
	Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})

把上面内容融合为一起

创建一个 ca.go 里面是创建ca和颁发证书的逻辑

package mainimport (
	"bytes"
	cr "crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"math/rand"
	"net"
	"os"
	"time"
)type CERT struct {
	CERT       []byte
	CERTKEY    *rsa.PrivateKey
	CERTPEM    *bytes.Buffer
	CERTKEYPEM *bytes.Buffer
	CSR        *x509.Certificate
}func CreateCA(sub *pkix.Name, expire int) (*CERT, error) {
	var (
		ca  = new(CERT)
		err error
	)	if expire < 1 {
		expire = 1
	}
	// 为ca生成私钥
	ca.CERTKEY, err = rsa.GenerateKey(cr.Reader, 4096)
	if err != nil {
		return nil, err
	}	// 对证书进行签名
	ca.CSR = &x509.Certificate{
		SerialNumber: big.NewInt(rand.Int63n(2000)),
		Subject:      *sub,
		NotBefore:    time.Now(),                       // 生效时间
		NotAfter:     time.Now().AddDate(expire, 0, 0), // 过期时间
		IsCA:         true,                             // 表示用于CA
		// openssl 中的 extendedKeyUsage = clientAuth, serverAuth 字段
		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		// openssl 中的 keyUsage 字段
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		BasicConstraintsValid: true,
	}
	// 创建证书
	// caBytes 就是生成的证书
	ca.CERT, err = x509.CreateCertificate(cr.Reader, ca.CSR, ca.CSR, &ca.CERTKEY.PublicKey, ca.CERTKEY)
	if err != nil {
		return nil, err
	}
	ca.CERTPEM = new(bytes.Buffer)
	pem.Encode(ca.CERTPEM, &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: ca.CERT,
	})
	ca.CERTKEYPEM = new(bytes.Buffer)
	pem.Encode(ca.CERTKEYPEM, &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(ca.CERTKEY),
	})	// 进行PEM编码,编码就是直接cat证书里面内容显示的东西
	return ca, nil
}func Req(ca *x509.Certificate, sub *pkix.Name, expire int, dns []string, ip []net.IP) (*CERT, error) {
	var (
		cert = &CERT{}
		err  error
	)
	cert.CERTKEY, err = rsa.GenerateKey(cr.Reader, 4096)
	if err != nil {
		return nil, err
	}
	if expire < 1 {
		expire = 1
	}
	cert.CSR = &x509.Certificate{
		SerialNumber: big.NewInt(rand.Int63n(2000)),
		Subject:      *sub,
		IPAddresses:  ip,
		DNSNames:     dns,
		NotBefore:    time.Now(),
		NotAfter:     time.Now().AddDate(expire, 0, 0),
		SubjectKeyId: []byte{1, 2, 3, 4, 6},
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
		KeyUsage:     x509.KeyUsageDigitalSignature,
	}	cert.CERT, err = x509.CreateCertificate(cr.Reader, cert.CSR, ca, &cert.CERTKEY.PublicKey, cert.CERTKEY)
	if err != nil {
		return nil, err
	}	cert.CERTPEM = new(bytes.Buffer)
	pem.Encode(cert.CERTPEM, &pem.Block{
		Type:  "CERTIFICATE",
		Bytes: cert.CERT,
	})
	cert.CERTKEYPEM = new(bytes.Buffer)
	pem.Encode(cert.CERTKEYPEM, &pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(cert.CERTKEY),
	})
	return cert, nil
}func Write(cert *CERT, file string) error {
	keyFileName := file   ".key"
	certFIleName := file   ".crt"
	kf, err := os.Create(keyFileName)
	if err != nil {
		return err
	}
	defer kf.Close()	if _, err := kf.Write(cert.CERTKEYPEM.Bytes()); err != nil {
		return err
	}	cf, err := os.Create(certFIleName)
	if err != nil {
		return err
	}
	if _, err := cf.Write(cert.CERTPEM.Bytes()); err != nil {
		return err
	}
	return nil
}
学新通

如果需要使用的话,可以引用这些函数

package mainimport (
	"crypto/x509/pkix"
	"log"
	"net"
)func main() {
	subj := &pkix.Name{
		CommonName:    "chinamobile.com",
		Organization:  []string{"Company, INC."},
		Country:       []string{"US"},
		Province:      []string{""},
		Locality:      []string{"San Francisco"},
		StreetAddress: []string{"Golden Gate Bridge"},
		PostalCode:    []string{"94016"},
	}
	ca, err := CreateCA(subj, 10)
	if err != nil {
		log.Panic(err)
	}	Write(ca, "./ca")	crt, err := Req(ca.CSR, subj, 10, []string{"test.default.svc", "test"}, []net.IP{})	if err != nil {
		log.Panic(err)
	}	Write(crt, "./tls")
}
学新通

遇到的问题

panic: x509: unsupported public key type: rsa.PublicKey

这里是因为 x509.CreateCertificate 的参数 privatekey 需要传入引用变量,而传入的是一个普通变量

注:x509: only RSA and ECDSA public keys supported

一些参数的意思

extendedKeyUsage :增强型密钥用法(参见"new_oids"字段):服务器身份验证、客户端身份验证、时间戳。

extendedKeyUsage = critical,serverAuth, clientAuth, timeStamping

keyUsage : 密钥用法,防否认(nonRepudiation)、数字签名(digitalSignature)、密钥加密(keyEncipherment)。

keyUsage = nonRepudiation, digitalSignature, keyEncipherment

Reference

golang ca and signed cert go

package x509

其他参考资料

生成私钥证书

不管是根证书,中级证书还是终端域名证书,都需要先生成一个私钥,然后通过私钥来获取公钥再进行证书签名,OpenSSL 可以通过 ecparam 子命令生成 ECC 私钥证书,Go 的标准库 crypto/ecdsa 也提供了 ECC 生成私钥的方法,我们稍微进行一下封装:

// 生成 ECC 私钥
func GeneratePrivateKey() (key *ecdsa.PrivateKey) {
    key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    return
}

这里的 elliptic.P256() 就是 prime256v1,这样我们就有了一个通用的生成 ECC 私钥的方法了。

构建证书签名请求 CSR

Go 提供了标准库 crypto/x509 给我们提供了 x509 签证的能力,我们可以先通过 x509.Certificate 构建证书签名请求 CSR 然后再进行签证。

x509.Certificate 参数解析

x509.Certificate 有以下参数可配置:

  • Version: 证书的版本,数字1,2,3
  • SerialNumber:证书序列号,标识证书的唯一整数,重复的编号无法安装到系统里。
  • SignatureAlgorithm: 签证书的算法标识,缺省。
  • PublicKeyAlgorithm: 生成公钥的算法,缺省。
  • Subject:证书持有者的信息。
    • Country: 国家,CN。
    • Province: 省。
    • Locality: 市。
    • Organization: 证书持有者组织名称。
    • OrganizationalUnit: 证书持有者组织唯一标识。
    • CommonName: 证书持有者通用名,需保持唯一,否则验证会失败。
  • NotBefore: 证书有效期开始时间。
  • NotAfter: 证书过期时间。
  • EmailAddresses: 需要颁发证书的邮箱地址。
  • DNSNames: 需要颁发证书的 DNS,也就是域名。
  • IPAddresses: 需要颁发证书的 IP 地址。
  • URIs: 需要颁发证书的 URI。
  • BasicConstraintsValid: 为true表示IsCA/MaxPathLen/MaxPathLenZero有效,为false忽略这几个配置。
  • IsCA: 是否为CA证书,CA证书可以为下级证书签证,为false代表是终端证书,不能继续签证,根证书和中级证书都应该为true
  • MaxPathLen: 表示证书链中可在此证书之后的非自颁发中级证书的最大层级,我们只需要1个中级证书就可以了,根证书设置为1,中级证书设置为0,那么中级证书就不能继续签署中级证书了。-1 表示未设置,且MaxPathLenZero == false && MaxPathLen == 0视为-1
  • MaxPathLenZero: MaxPathLen == 0
  • KeyUsage: 定义了证书包含的密钥的用途。
    • KeyUsageDigitalSignature: 用于数字签名,常用于具有完整性的实体身份验证和数据源身份验证。可以用于CA证书或终端证书。
    • KeyUsageContentCommitment: 公钥可用于提供不可否认服务,这可以防止签名实体错误地拒绝某些操作。当发生冲突时,应该有一个可靠的第三方来对签名的数据进行辨伪。
    • KeyUsageKeyEncipherment: 用于加密对称密钥,目标解密密钥,随后使用它来加密和解密实体之间的数据。
    • KeyUsageDataEncipherment: 用于加密和解密实际应用程序数据。
    • KeyUsageKeyAgreement: 使用密钥协商协议与目标建立对称密钥,然后可以使用对称密钥来加密和解密实体之间发送的数据。
    • KeyUsageCertSign: 用于校验公钥证书的签名,只能用于 CA 证书。
    • KeyUsageCRLSign: 用于验证证书吊销列表的签名,只能用于 CA 证书。
    • KeyUsageEncipherOnly: 公钥仅用于在执行密钥协商时加密数据。
    • KeyUsageDecipherOnly: 公钥仅用于在执行密钥协商时解密数据。
  • ExtKeyUsage: 该扩展表示被认证的公钥的用途,可以替换或作为KeyUsage扩展的补充
    • ExtKeyUsageAny: 未知。
    • ExtKeyUsageServerAuth: 建立 TLS 连接时进行服务器身份验证。
    • ExtKeyUsageClientAuth: 建立 TLS 连接时进行客户端验证。
    • ExtKeyUsageCodeSigning: 对可下载执行的代码签名。
    • ExtKeyUsageEmailProtection: 安全电子邮件签名,允许发送和接收加密的电子邮件。
    • ExtKeyUsageIPSECEndSystem: IP 安全终端系统,已弃用。
    • ExtKeyUsageIPSECTunnel: IP 安全隧道,已弃用。
    • ExtKeyUsageIPSECUser: IP 安全用户,已弃用。
    • ExtKeyUsageTimeStamping: 可信时间戳。
    • ExtKeyUsageOCSPSigning: OCSP 签名。
    • ExtKeyUsageMicrosoftServerGatedCrypto: 未知。
    • ExtKeyUsageNetscapeServerGatedCrypto: 未知。
    • ExtKeyUsageMicrosoftCommercialCodeSigning: 未知。
    • ExtKeyUsageMicrosoftKernelCodeSigning: 未知。

我们使用这些字段就够了,其他字段缺省即可。可以使用这些参数分别签证生成根证书、中级证书和终端域名证书的签证 CSR。

根证书
var rootCsr = &x509.Certificate{
    Version:      3,
    SerialNumber: big.NewInt(time.Now().Unix()),
    Subject: pkix.Name{
        Country:            []string{"CN"},
        Province:           []string{"Shanghai"},
        Locality:           []string{"Shanghai"},
        Organization:       []string{"JediLtd"},
        OrganizationalUnit: []string{"JediProxy"},
        CommonName:         "Jedi Root CA",
    },
    NotBefore:             time.Now(),
    NotAfter:              time.Now().AddDate(10, 0, 0),
    BasicConstraintsValid: true,
    IsCA:                  true,
    MaxPathLen:            1,
    MaxPathLenZero:        false,
    KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
学新通

根证书的配置如上,CommonNameJedi Root CA,用来区分这是一个根证书,过期时间设置为十年,根证书十年足够了,MaxPathLen 为1,只能签发一级中级CA证书。

KeyUsageKeyUsageCertSignKeyUsageCRLSign,支持签发和吊销中级证书。这里不需要 ExtKeyUsage

中级证书

中级证书的配置基本与根证书基本一致,只是需要改一下 MaxPathLen为0,过期时间短一些:

var interCsr = &x509.Certificate{
    Version:      3,
    SerialNumber: big.NewInt(time.Now().Unix()),
    Subject: pkix.Name{
        Country:            []string{"CN"},
        Province:           []string{"Shanghai"},
        Locality:           []string{"Shanghai"},
        Organization:       []string{"JediLtd"},
        OrganizationalUnit: []string{"JediProxy"},
        CommonName:         "Jedi Inter CA",
    },
    NotBefore:             time.Now(),
    NotAfter:              time.Now().AddDate(1, 0, 0),
    BasicConstraintsValid: true,
    IsCA:                  true,
    MaxPathLen:            0,
    MaxPathLenZero:        true,
    KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
学新通
终端证书

终端域名证书的配置需要做一些变更,配置如下:

var csr = &x509.Certificate{
    Version:      3,
    SerialNumber: big.NewInt(time.Now().Unix()),
    Subject: pkix.Name{
        Country:            []string{"CN"},
        Province:           []string{"Shanghai"},
        Locality:           []string{"Shanghai"},
        Organization:       []string{"JediLtd"},
        OrganizationalUnit: []string{"JediProxy"},
        CommonName:         "foreverz.cn",
    },
    DNSNames:              []string{"foreverz.cn"},
    NotBefore:             time.Now(),
    NotAfter:              time.Now().AddDate(1, 0, 0),
    BasicConstraintsValid: true,
    IsCA:                  false,
    KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
    ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
学新通

因为是终端域名证书,所以DNSNames需要设置为对应的域名,可以使用通配符*支持所有的三级域名,例如这里可以写成*.foreverz.cn,也可以使用一个整数签发多个域名。

因为不是CA证书了,所以IsCA设为false,也不需要配置MaxPathLenMaxPathLenZero了。

KeyUsageKeyUsageDigitalSignatureKeyUsageKeyEncipherment,可用于身份验证和数据加密传输。ExtKeyUsageExtKeyUsageServerAuth 可以进行服务端身份验证。

这样我们就配置好了三级证书的签名请求了,这里都是分开写的,大部分代码都是一样的,可以写成一个方法,这里就不扩展了。

证书签名

证书签名可以使用标准库 crypto/x509CreateCertificate 方法来签名。该方法需要以下5个参数:

  • rand: 随机数,使用 rand.Reader 即可。
  • template: 证书签名请求,即上面的 CSR。
  • parent: 父级证书,根证书是自签的,直接用自己的 csr,中级证书用根证书来签名,终端证书使用中级证书签名。
  • pub: 第一步生成的私钥对应的公钥证书,可以使用 key.Public() 获取。
  • priv: 父级证书私钥。

具体签证方法如下:

rootDer, err := x509.CreateCertificate(rand.Reader, rootCsr, rootCsr, rootKey.Public(), rootKey)

rootCert, err := x509.ParseCertificate(rootDer)

CreateCertificate 返回的是一个 []byte,是二进制DER编码的证书,可以使用 x509.ParseCertificate 转为 *x509.Certificate 格式。

生成了根证书就可以使用根证书签证中级证书了:

interDer, err := x509.CreateCertificate(rand.Reader, interCsr, rootCert, interKey.Public(), rootKey)

interCert, err := x509.ParseCertificate(interDer)

终端证书的生成与中级证书基本一致:

der, err := x509.CreateCertificate(rand.Reader, csr, interCert, key.Public(), interKey)

cert, err := x509.ParseCertificate(der)

保存 PEM 文件

上面生成的证书是 *x509.Certificate 格式的,我们需要转为 PEM 并存为 .pem 文件到本地才能安装到电脑上,可以通过以下方法进行转换:

签证证书:

certBlock := &pem.Block{
    Type:  "CERTIFICATE",
    Bytes: cert.Raw,
}

pemData := pem.EncodeToMemory(certBlock)

if err = ioutil.WriteFile("xx.cert.pem", pemData, 0644); err != nil {
        panic(err)
}

私钥证书:

keyDer, err := x509.MarshalECPrivateKey(key)

keyBlock := &pem.Block{
    Type:  "EC PRIVATE KEY",
    Bytes: keyDer,
}

keyData :=  := pem.EncodeToMemory(certBlock)

if err = ioutil.WriteFile("xx.key.pem", keyData, 0644); err != nil {
        panic(err)
}

从 PEM 文件读取证书

上面我们把证书文件转为了 PEM 文件,那么有时候就会从 PEM 文件读取证书,我们没必要手写文件读取方法,crypto/tls 提供了 LoadX509KeyPair 方法可以帮助我们从文件读取证书,然后稍微做一下转换就行了。

func LoadPair(certFile, keyFile string) (cert *x509.Certificate, err error) {
    if len(certFile) == 0 && len(keyFile) == 0 {
        return nil, errors.New("cert or key has not provided")
    }

    // load cert and key by tls.LoadX509KeyPair
    tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return
    }

    cert, err = x509.ParseCertificate(tlsCert.Certificate[0])
    return
}

如上,我们完成了证书的生成、存储和读取等能力,证书相关的处理就结束了。

参考

  1. OpenSSL Certificate Authority
  2. Hyperledger Fabric-ca配置文件解读
  3. OpenSSL CA keyUsage 扩展
  4. RFC 5280 section 4.2.1.12 Extended Key Usage

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgfafee
系列文章
更多 icon
同类精品
更多 icon
继续加载