在上一篇文章中,我们介绍了用户具有多个身份属性时,选择性的把其中的一个属性暴露出来,而不会造成其他信息的暴露。更进一步的情况,某些时候我们只需要验证用户的年龄达到多少岁,或者小于多少岁,但是并不关心用户的具体年龄和出生日期,比如在购买烟酒时,商家需要验证用户的年龄大于18岁。除了年龄,住址、民族等都可能会有对某个断言进行验证的情况。比如某旅游景点,对本市所有居民免费,所以居民只需要证明自己身份证上的住址在某市,而不需要暴露具体的居住地址。这些只给出证明的答案,而不暴露其他任何身份信息的情况,都是零知识证明的范畴。
传统的零知识证明算法很复杂,难以理解,也难以实现,而且就算实现了,也不能很好的满足我们数字身份中关于身份属性验证的问题,因为这里涉及到三方:发证方、持证方、验证方。在发证方将证件(可验证声明VC)发给持证方时,发证方并不知道持证方以后会遇到验证大于18岁,还是验证大于65岁,另外持证方在生成亮证(可验证表达VP)时,只需要持证方和验证方进行交互,不应该在此时引入发证方。所以我们可以认为:
1.生成VC和生成VP是两个独立的事件,生成VP时不应该有发证方参与。
2.验证方信任的是发证方,而不是持证方,所以持证方只是简单的证明自己大于18岁,但是这个证明没有发证方的背书,是不可信的。
基于以上条件,本文提出了一种身份证明的零知识证明方法,该方法将VC和VP独立,生成VP时发证方不需要参与,而且VP中给出的证明具有零知识性,而且有发证方背书。
0x0. Issuer:根据采样粒度与采样范围进行数据的断言构建
发证方Issuer要对某个属性做出证明,首先需要在该数据所在的作用域进行采样,采样包括采样的粒度和范围。以用户的生日属性为例,其作用范围虽然可以是历史上任意一天,但是我们考虑实际情况,可以将采样范围定义在1900-1-1到2020-1-1,然后是采样粒度的问题,如果我们以年为粒度,也就是说我们只关心用户出生的那个年份来确定年龄,如果我们以年月粒度,那么就可以根据具体出生在几月来确定年龄,最细的粒度就是到天。这里简单起见,我们就以年为采用粒度,这样我们就建立了一个从1900到2020的数组:
[1900,1901,1902,…,2019,2020]
然后我们还可以增加下区间”<1900”和上区间”>2020”两个元素,从而覆盖年份的所有取值范围。
我们有了年份采样的数组,接下来是根据用户的实际情况,为每个元素增加断言,这里我以当年用户是否已经出生为断言,所以我们的数组变为,以小明1985年出生为例:
[<1900未出生, 1900未出生,1901未出生,1902未出生,…1984未出生,1985已出生,1986已出生,…,2019已出生,2020已出生,>2020已出生]
有了这么一个断言数组,我们可以简化断言为更简洁一些的形式:
Min=1900,Max=2020,Step=1,OtherRange=Both,Assert=[0,0,0,0,……,0,1,1,1,…,1,1],Format=“{0}:{1}"
这里定义了数组的范围,采样粒度,边界处理,以及断言的结果。Format是定义了断言字符串的格式。
0x1. Issuer:断言默克尔树的构建
现在我们已经构建好了断言数组,接下来就只需要将断言数组作为默克尔树的叶子节点,并采用上一篇文章中说的加盐方法,防止哈希碰撞,从而构建一个加盐断言默克尔树。
0x2. Issuer:默克尔根签名,VC生成
这棵树构建好了,得到了默克尔根,发证方接下来使用自己的私钥对这个默克尔根进行签名,并将签名、默克尔树生成办法、随机种子等信息放到VC中,以供用户认证。以下是*部门针对用户生成的身份证VC,并在其中包含了出生年份的断言。
{ // VC内容所遵循的JSON-LD标准 "@context": [ "https://www.w3.org/2018/credentials/v1", "https://studyzyexamples.com/identity/v1" ], // 本VC的唯一标识,也就是证书ID "id": "vc511112200001010015", // VC内容的格式 "type": ["VerifiableCredential", "Identity"], // 本VC的发行人 "issuer": "did:*部门ID", // 本VC的发行时间 "issuanceDate": "2010-07-01T19:73:24Z", // VC声明的具体内容 "credentialSubject": { // 被声明的人的DID "id": "did:cid:511112200001010015", // 声明内容:姓名、性别、生日、民族、住址等 "name":"小明", "gender":"男", "birthdate":"2000-01-01", "nation":"汉", "address":"A省B市C区D街道xxx号", //接下来是种子数、默克尔根、*的签名 "seed":"23523865082340324", "merkleRoot":"ea59a369466be42d1a4783f09ae0721a5a157d6dba9c4b053d407b5a4b9af145", "rootSignature":"3066022051757c2de7032a0c887c3fcef02ca3812fede7ca748254771b9513d8e266", "signer":"did:*部门ID#keys-1" //接下来是对出生年份断言默克尔树的构建和签名 "birthYearAssert":{ "min":1900, "max":2020, "step":1, "otherRange":"Both", "assert":[0,0,0,0,0,0,......0,1,1,1,1,1.......1], "format":"{0}:{1}", "seed":"9013492332268116070", "merkleRoot":"806de4a868682738bb328a4801c09d88a93ce1301e3dbff08f5b0881be01fddb", "rootSignature":"3077022051757c2de7032a0c887c3fcef02ca3812fede7ca748254771b9513d8e255", "signer":"did:*部门ID#keys-1" } }, // 对本VC的证明 "proof": { "creator": "did:*部门ID#keys-1", "type": "Secp256k1", "signatureValue": "3044022051757c2de7032a0c887c3fcef02ca3812fede7ca748254771b9513d8e2bb" } }
0x3. Verifier:生成断言请求
现在卖烟酒的商家Verifier要求顾客证明自己大于18岁,换句话说,以当前2020年来说,就是要证明18年前 2002年已经出生。所以商家需要顾客证明的断言是:
2002:1
商家将这个断言生成断言请求,并发送给顾客的数字身份APP中,比如商家可以把断言请求生成二维码,让所有顾客都扫码,生成VP。
0x4. Holder:生成零知识证明VP
现在顾客Holder已经得知了Verifier的断言请求,于是根据断言请求,找到该断言在断言默克尔树中的位置,并形成:断言内容、断言位置、默克尔验证路径、Salt、默克尔根、*机关DID和签名的验证判断所需的全部元素,而这一切都是基于之前*机关颁发给用户的VC生成的,并不需要联网,更不需要*机关在本次验证过程中的参与。
{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://studyzyexamples.com/identity/v1" ], "type": "VerifiablePresentation", // 本VP包含的VC的内容 "verifiableCredential": [{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://studyzyexamples.com/identity/v1" ], "id": "vc511112200001010015", "type": ["VerifiableCredential", "Identity"], "issuer": "did:*部门ID", "issuanceDate": "2010-07-01T19:73:24Z", "credentialSubject": { "id": "did:cid:511112200001010015", //以下是断言的内容,意思是2002年已经出生 "assert":"2002:1", //以下是验证披露字段有效性的数据 //数据在默克尔树中的索引 "dataIndex":103, //本数据加盐的值 "salt":"5ff63326ca055a6dca267985c8ca03732b907b4197eb16a60c723bd567883650", //默克尔验证路径 "merklesibling":"b6234c998b586914c76ccabd35c97be779074ea2ea7d03e81b25dc80547ee799 02a9c8c6a60eaab5765f526b4a1792f37e3b399cdab2a9492653dac432f2ccd3 5109396c42763abe94aa92acf11e9b2a034cdd1a63a4493ae0cce9fffb632f81 5bd2b306f156bdab383e352dc31c62cb2e18ada75957398fdd0c369ef3850d97 73af5df5f4c36d1377bd976a4b7be2b88878befd21b75962863d22299d3023a6 537ad98582a606ef422881bf766550bf22dbd8f06eb91d6ea59a57643f02c22a 4b590775b2fddd7fa7b6fe428e5ff83256dfbf6a15fa2c30b643a41785c245a0", //默克尔根哈希 "merkleRoot":"806de4a868682738bb328a4801c09d88a93ce1301e3dbff08f5b0881be01fddb", //*机关对默克尔根的签名 "rootSignature":"3077022051757c2de7032a0c887c3fcef02ca3812fede7ca748254771b9513d8e255", //用的*机关哪个Key进行的签名 "signer":"did:*部门ID#keys-1" }, }], // Holder小明对本VP的签名信息 "proof": { "type": "Secp256k1", "created": "2010-07-02T21:19:10Z", "proofPurpose": "authentication", "verificationMethod": "did:cid:511112200001010015#keys-1", // challenge和domain是为了防止重放攻击而设计的 "challenge": "1f44d55f-f161-4938-a659-f8026467f126", "domain": "4jt78h47fh47", "jws": "eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..kTCYt5 XsITJX1CxPCT8yAV-TVIw5WEuts01mq-pQy7UJiN5mgREEMGlv50aqzpqh4Qq_PbChOMqs LfRoPsnsgxD-WUcX16dUOqV0G_zS245-kronKb78cPktb3rk-BuQy72IFLN25DYuNzVBAh 4vGHSrQyHUGlcTwLtjPAnKb78" } }
0x5. Verifier:验证VP
商家在收到顾客生成的VP后,可以直接通过签名验证默克尔根是否是*机关签名的,然后通过默克尔验证来证明”2002:1“这个断言是正确的。具体验证的细节我在上一篇文章中已经讲过,这里也是一样的过程,就不再重复了。
0x6. 小结
本文提出的零知识证明方法基于范围数据构建和加盐默克尔树验证,对于无法定范围的场景,可能并不是很适用。比如对于姓名字段,我们可以拆分成姓和名,姓字段是有一个范围的,大概率都在百家姓里面 ,而名字段就太广泛了,基本上所有的汉字都可以作为名,而且名有多个字,排列组合的情况是天文数字,所以我们可以对姓字段建立零知识证明,而对于名字段无法使用。除了生日外,民族是一个可数的范围(56个民族+其他),住址的省、市、县都是一个可数的范围,我们都可以使用本文中的零知识证明方法。
本零知识证明还有个局限性,就是只能证明范围数据构建时的取值,而无法证明中间的取值。比如我们对家庭年收入字段建立范围取值,每10W一个值,所以默克尔树叶子节点就是:[0,10W,20W,30W …...]
比如小明的实际家庭年收入是38W,我们可以基于这个取值来证明家庭年收入大于30W,但是我们无法证明家庭年收入大于35W。如果想要证明,我们就需要将划分范围的粒度变细,比如变成每一个范围是1W,这样构建了一个新的长了很多的叶子节点列表[0,1W,2W,3W…….]如果我们将范围上限定在1000W,那么就需要1000个叶子节点,但是我们也只能证明年收入大于35W,无法证明更细的粒度,比如年收入是否达到38.5W?
基于前面几篇文章介绍的数字身份DID技术的基础知识,下一篇我们将介绍DID的应用场景。