3. Verifiable Credentials (VC) 与 Presentations (VP)
好,咱们今天来聊聊 VC 和 VP。这两个东西,可以说是 DID 生态里的核心资产。你有了 DID,就像有了身份证号,但光有号不行,你得有证明你身份的材料——这就是 VC。而 VP,就是你把这些材料展示给别人看的方式。
我记得刚接触这个领域时,总把 VC 和 VP 搞混。后来踩了几个坑,才真正理解它们的关系。说白了,VC 是你的「原件」,VP 是你的「复印件」。你不可能把原件到处给,对吧?
3.1 VC 的数据结构
一个标准的 VC,长什么样?我直接给你看 W3C 规范里的核心结构:
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/1872",
"type": ["VerifiableCredential", "AlumniCredential"],
"issuer": "did:example:123456789abcdefghi",
"issuanceDate": "2024-01-01T19:23:24Z",
"expirationDate": "2025-01-01T19:23:24Z",
"credentialSubject": {
"id": "did:example:abcdef123456789",
"alumniOf": "Example University"
},
"proof": {
"type": "Ed25519Signature2020",
"created": "2024-01-01T19:23:24Z",
"verificationMethod": "did:example:123456789abcdefghi#keys-1",
"proofPurpose": "assertionMethod",
"proofValue": "z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CkTYxZ3..."
}
}
嗯,这里有几个关键字段,我一个个说:
- @context:这玩意儿定义了 JSON-LD 的上下文。说白了就是告诉解析器,你用的这些字段都是啥意思。我建议你永远把 W3C 的官方 context 放在第一个。
- id:VC 的唯一标识。可以是 URL,也可以是 UUID。我个人习惯用 UUID,因为简单,不用管域名。
- type:声明这个 VC 的类型。至少要有 "VerifiableCredential",然后可以加自定义类型。
- issuer:谁发的?这里填 DID。注意,这个 DID 必须能解析出公钥,不然 proof 没法验证。
- credentialSubject:发给谁的?里面那个 "id" 字段,就是持有者的 DID。
- proof:数字签名。这是 VC 可验证的核心。
核心要点:VC 的本质,就是「一个 DID 对另一个 DID 的声明,加上签名」。没有 proof 的 VC,只是一堆 JSON 数据,毫无意义。
3.2 VP 的数据结构
VP 的结构更简单。它就是把一个或多个 VC 打包,然后由持有者签名:
{
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiablePresentation"],
"verifiableCredential": [
{ ... 这里是完整的 VC 对象 ... }
],
"proof": {
"type": "Ed25519Signature2020",
"created": "2024-06-15T10:00:00Z",
"verificationMethod": "did:example:abcdef123456789#keys-1",
"proofPurpose": "authentication",
"challenge": "c0a1b2c3-d4e5-f6a7-b8c9-d0e1f2a3b4c5",
"proofValue": "z5DdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CkTYxZ4..."
}
}
注意看,VP 的 proof 和 VC 的 proof 有个关键区别:VP 的 proof 里多了个 challenge 字段。为什么?
你想想看,如果 VP 没有 challenge,那别人拿到你的 VP 后,可以反复使用——这就是重放攻击。challenge 是一个随机数,由验证方生成,每次都不一样。这样 VP 就只能用一次。
实战技巧:我在做项目时,challenge 一般用 UUID v4 生成,有效期设 5 分钟。太短了用户体验差,太长了不安全。
3.3 VC 的生命周期
一个 VC 从生到死,大概经历这几个阶段:
| 阶段 | 描述 | 关键操作 |
|---|---|---|
| 签发 | 发行者创建 VC 并签名 | issuer 用私钥签名,生成 proof |
| 存储 | 持有者收到 VC 并保存 | 通常存在数字钱包里,加密存储 |
| 展示 | 持有者生成 VP 给验证方 | 持有者用私钥签名 VP |
| 验证 | 验证方检查 VC 和 VP 的有效性 | 验证签名、有效期、发行者 DID |
| 撤销 | 发行者声明 VC 失效 | 更新撤销列表或状态机 |
| 过期 | VC 达到 expirationDate | 自动失效,无需额外操作 |
这里有个坑,我曾经踩过:撤销不等于过期。过期是自动的,撤销是主动的。比如你毕业了,学校给你发了学位证 VC。后来发现你论文造假,学校可以主动撤销这个 VC,即使它还没过期。
3.4 可验证表达
什么叫「可验证表达」?说白了,就是你能证明「我是我,而且我有这个资格」,但不用把所有信息都抖出来。
举个例子:你去酒吧,需要证明你年满 18 岁。传统做法是掏出身份证,上面有你的姓名、住址、身份证号、出生日期——信息全暴露了。用 VC/VP 怎么做?
// 你只需要生成一个 VP,里面包含一个「年龄证明」VC
// 这个 VC 只声明:持有者年龄 >= 18
// 不包含具体出生日期,更不包含姓名和住址
{
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiablePresentation"],
"verifiableCredential": [{
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiableCredential", "AgeOver18Credential"],
"issuer": "did:example:government-issuer",
"credentialSubject": {
"id": "did:example:user123",
"ageOver18": true
},
"proof": { ... }
}],
"proof": { ... }
}
看到了吗?你只暴露了「是否年满 18」这个布尔值,其他信息一概不透露。这就是零知识证明的思想——虽然这里用的是最简单的布尔声明,但原理是一样的。
注意:零知识证明在 VC/VP 中的应用,目前还比较前沿。大多数生产环境用的还是「选择性披露」——就是只展示 VC 里的部分字段。真正的零知识证明,比如用 BBS+ 签名,实现起来复杂得多。
3.5 验证流程
验证方拿到 VP 后,会做这几件事:
- 检查 VP 的 proof:用持有者的 DID 解析出公钥,验证 VP 签名。同时检查 challenge 是否匹配。
- 检查 VC 的 proof:用发行者的 DID 解析出公钥,验证 VC 签名。
- 检查有效期:确认 VC 没有过期。
- 检查撤销状态:查询发行者的撤销列表,确认 VC 没有被撤销。
- 检查内容:确认 VC 里的声明满足业务需求。
嗯,这里要注意:第 4 步「检查撤销状态」是最容易被忽略的。我见过不少项目,只验证了签名和有效期,结果被人用已撤销的 VC 钻了空子。
我的建议:撤销列表最好用区块链或分布式存储来维护,别用中心化数据库。不然发行者服务器一挂,所有 VC 都验证不了了。
好了,VC 和 VP 的核心内容就这些。说白了,VC 是「别人给你发的证明」,VP 是「你给别人看的证明」。理解了这个区别,后面的内容就好办了。