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 后,会做这几件事:

  1. 检查 VP 的 proof:用持有者的 DID 解析出公钥,验证 VP 签名。同时检查 challenge 是否匹配。
  2. 检查 VC 的 proof:用发行者的 DID 解析出公钥,验证 VC 签名。
  3. 检查有效期:确认 VC 没有过期。
  4. 检查撤销状态:查询发行者的撤销列表,确认 VC 没有被撤销。
  5. 检查内容:确认 VC 里的声明满足业务需求。

嗯,这里要注意:第 4 步「检查撤销状态」是最容易被忽略的。我见过不少项目,只验证了签名和有效期,结果被人用已撤销的 VC 钻了空子。

我的建议:撤销列表最好用区块链或分布式存储来维护,别用中心化数据库。不然发行者服务器一挂,所有 VC 都验证不了了。

好了,VC 和 VP 的核心内容就这些。说白了,VC 是「别人给你发的证明」,VP 是「你给别人看的证明」。理解了这个区别,后面的内容就好办了。