Identity-Hub

1. 背景

在DID的生态体系中,需要有一个服务用于保存和管理用户的数据,这就是Identity Hub。Identity Hub的实现满足几个要求:

  • 用户可控。Identity Hub可以由用户选择部署于任何地方,包括用户自己的手机、PC等。
  • 用户数据加密保存。
  • Identity Hub不保存任何私钥。
  • 用户数据的访问需要认证。
  • 经过用户授权后可允许第三方访问用户数据。

2. 应用场景

Claim 相关内容参考:verifiable-claim

  • Claim 生产方:权威机构,也有自己的 DID
  • Claim 申请方:DID 用户
  • Claim 使用方:第三方应用

2.1. Claim 的申请与签发

  1. DID 用户(申请者)向权威机构申请相关认证的 claim
  2. 权威机构验证通过后,用自己的 DID 签发claim 并存储在 Identity hub 中
  3. 权威机构授权申请者访问claim 数据,这里权威机构自己也可以访问该数据
  4. 申请者获取 claim,之后可以存储在本地或者自己的 hub 中

2.2. 授权第三方获取 Claim

  1. 用户使用自己 DID 的私钥和第三方应用 DID 的公钥生成重加密密钥
  2. 向 Identity hub 授权第三方应用 DID 访问,附加重加密密钥
  3. Identity hub 用重加密密钥对claim 密文和密钥密文重加密
  4. Identity hub 生成 endpoint 给被授权的第三方 DID 访问

对以上的场景进行分析,Identity hub主要需要提供以下的几种功能:

  • 存储:存储 DID 相关的 claim 密文和密钥密文
  • 鉴权:访问数据需要权限认证
  • 授权:添加、删除授权给第三方访问 claim
  • 同步: Identity hub可以由用户选择部署于任何地方,包括用户自己的手机、PC等 Identity hub 之间同步 数据访问不绑定在特定在Identity hub实例上

3. 架构

4. 详细介绍

4.1. 数据存储

Identity hub 中的数据以 commit 的形式存储,每个 commit 表示对数据的一次操作,将所有的 commits 合并就可以得到数据的现有状态。采用这种形式犹如下的好处:

  • 方便不同的 Identity hub 实例间同步数据
  • 能够对数据的操作历史进行审计(每个 commit 需要由 DID 签名)
  • 它简化了需要脱机数据修改并需要解决冲突的用例的实现

需要注意的是这里的数据包括 Claim 数据以及 Permission 数据,他们都以 commit 形式存在。对于 commit 的操作,包括三种:写入数据、读取 commit 和读取完整的 object。

每一个 commit 都是一个 JWT 结构的数据(如下),其中 Payload 为要发送的原始数据。

type Header struct {
    CommittedAt    string      `json:"committedAt"`        // commit created time
    Iss            string      `json:"iss"`                // ctrator DID
    Kid            string      `json:"kid"`                // key used to sign the commit
    Sub            string      `json:"sub"`                // owner DID
    Interface      string      `json:"interface"`          // Profile, Actions, Stores, Collections, Services(data type)
    Context        string      `json:"context"`            // schema context, "http://schema.org"
    Type           string      `json:"type"`               // WriteRequest, CommitQueryRequest, ObjectQueryRequest
    Operation      string      `json:"operation"`          // create, update, delete
    CommitStrategy string      `json:"commitStrategy"`     // "basic"
    ObjectId       string      `json:"objectId,omitempty"` // optional
    Revision       string      `json:"revision"`           // commit hash
    Meta           interface{} `json:"meta,omitempty"`     // application-specific metadata
}
type Commit struct {
    Protected Header        `json:"protected"`
    Unprotected interface{} `json:"header"`
    Payload   string        `json:"payload"` // encrypted data
    Signature string        `json:"signature"`
}

严格来说,hub 中存储的是 Commit 序列化后的二进制数据。

对于 Claim 数据的存储,我们将加密后的Claim 密文放在 payload中,而加密后的密钥密文放在 UnprotectedHeader 的 Meta 中。这样做的考虑是授权第三方访问数据时,服务端要对密钥密文进行重加密计算,而 payload 信息是受 signature 保护,修改后无法保证数据的真实性。

这里采用 secp256k1 形式的椭圆曲线密钥,对称加密采用 A128GCM,非对称加密采用 ES256K

4.1.1. 写入数据

当我们发送写入数据的请求时,需要构造一个 JWT 格式的请求数据WriteRequest。WriteRequest的结构如下:

type WriteRequest struct {
    Context    string          `json:"@context` // "https://schema.identity.foundation/0.1"
    Type       string          `json:"@type"`   // "WriteRequest"
    Iss        string          `json:"iss"`     // client DID, sender
    Sub        string          `json:"sub"`     // target DID, data owner
    Aud        string          `json:"aud"`     // hub DID, receiver
    CommitData SearlizedCommit `json:"commit"`  // searlized Commit object
}

不管是写入数据还是下面的读取数据,都是以 HTTP POST 请求发送,序列化后的 request 数据放在 body 中,依靠 Type 字段来说识别当前是什么操作。

当 hub 收到写入 commits 请求后,按照以下逻辑执行:

  1. 权限验证、授权验证(owner 写入)
  2. 根据 objectId 在数据库中查找相关 commits
  3. 如果已有同样的 object,返回错误
  4. 否则向数据库中写入 commit
  5. 将该 commit 的revisions(hash) 打包,签名加密并发送回复

如果是更新数据,等同于写入一个新的 commit,但是会返回 之前所有 commits的 revisions 信息。

4.1.2. 获取数据

4.1.2.1. 获取 commits

同一个数据的一系列 commits 以 objectId 关联起来,当 hub 收到获取 commits 的请求后,按照以下逻辑执行:

  1. 权限验证、授权验证
  2. 根据 objectId 在数据库中查找所有 commits
  3. 如果找不到相应的 object,返回错误
  4. 验证所有 commits 的签名
  5. 打包返回数据,签名加密并发送回复
4.1.2.2. 获取 object

所有 commits以一定的策略合并得到完整的 object,这里采用 basic 策略,也即每个 commit 都有一个 object 的完整形态,取最新的 commit 最为 object ,执行逻辑如下:

  1. 权限验证、授权验证
  2. 根据 objectId 在数据库中查找所有满足 basic 策略的 commits
  3. 如果找不到相应的 object,返回错误
  4. 验证所有 commits 的签名
  5. 取第一个和最后一个 commit,元信息中的创建时间等字段取自于第一个 commit,而 meta 和 payload 信息取自于最后一个 commit(最新状态)
  6. 打包返回数据,签名加密并发送回复

响应数据的结构可以参考第5节。

4.2. 鉴权

Identity hub 对访问请求的鉴权遵循 DIF/W3C DID Auth schemes,同样采用挑战-响应的机制,认证机制主要实现以下两个任务:

  • 在客户端和 Hub 之间利用各自的 DID 和密钥相互认证。
  • 加密所有Hub请求和响应,使其内容仅对消息交换中涉及的DID密钥的持有者可用。

也即存储到 hub 中的数据不仅是加密存储的,也是加密传输的。而加密传输的过程首先需要构造认证数据结构。

假如我们有上述 WriteRequest 形式的数据,还需要构造如下的 AuthEnvelope 认证结构。

type AuthHeader struct {
    Alg             string `json:"alg"`
    Kid             string `json:"kid"` // sender's did publickey descriptor
    DIDRequestNonce string `json:"did-requester-nonce"`
    DIDAccessToken  string `json:"did-access-token"`
}
type AuthEnvlope struct {
    Header    AuthHeader `json:"header"`
    Payload   string     `json:"payload"` //data content
    Signature string     `json:"signature"`
}

其中 Payload 为 WriteRequest 序列化后的数据。

其中 header 必须是以下的几个字段。

字段 描述
alg 标准JWT头。表示用于签署JWT的算法。
kid 标准JWT头。该值应采用该形式{did}#{key-id}。另一个应用程序可以获取此值,解析DID,并找到可用于提交的签名验证的指示公钥。
did-requester-nonce 随机生成的字符串,必须在客户端缓存。此字符串将用于在以下部分中验证来自Hub的响应。
did-access-token 应在客户端缓存并包含在发送到集线器的每个请求中的令牌。由于我们还没有访问令牌,因此请在初始请求中保留此属性。

之后通过 JSON Web Encryption (JWE) standard用 hub 的公钥加密请求。

发送请求时需要did-access-token这个字段,但是初次发送请求时客户端是没有这个 token 的,所以先发送一个不带 token 的请求,hub 会拒绝这个请求,但是在回复中会包含这个 token(JWE 形式,hub解密 request JWE后提取发送者的公钥来加密,需要获取 DID Document)。客户端提取这个 token (完成挑战)之后就可以放在后续的请求中。token 的过期时间会放在exp字段中,token 过期后继续发送请求,hub 会返回错误,此时需要重复上面的方法获取新的 token。

token 本身也是一个 JWT 结构,did-access-token字段就是 token 序列化后的内容。

假设我们已有请求数据内容JWT,例如下文的 WriteRequest(其本身就是一个 JWT,其 payload也是一个序列化后的 JWT),先构造成认证请求 JWT,进行签名序列化,再加密成 JWE,然后加密发送给 hub,整个流程如下图所示:

4.3. 授权分享

授权数据在 hub 中以 PermissionGrant 的形式存储。

type PermissionGrant struct {
    Owner    string `json:"owner"`   // Owner DID granting permission
    Grantee  string `json:"grantee"` // DID the permission is granted to
    Context  string `json:"context"` // the data schema context
    ObjectId string `json:"objectId"`
    Allow    string `json:"allow"` // Permissioned allowed in the form of a "CRUD" string, or "----" for no permission
    Rk       string `json:"key"`   // proxy re-encryption key
    Exp      Int    `json:"expiration"` //expiration time for permission
}

目前支持的操作为创建授权、读取授权、修改授权和删除授权。

4.3.1. 创建授权

创建授权的过程也就是写入数据的过程,其中PermissionGrant作为 Commit 的 payload。其执行逻辑如下:

  1. 根据被授权的 DID 从 resolver 中查找其公钥
  2. 用自己 DID 的私钥和被授权 DID 的公钥生成重加密密钥
  3. 填写 PermissionGrant 作为 Commit 的payload 写入 hub

4.3.2. 验证授权

当 hub 收到客户端发来的请求时,进行如下的验证:

  1. 从请求中获取 schema、操作、客户端 DID、objectId
  2. 查询所有的PermissionGrants,找到objectId匹配 且 DID 匹配的授权
  3. 查看操作是否允许
  4. 返回授权成功/失败

如果请求 JWT 中的 iss(请求发送者) 和 sub(数据所有者)一致,说明是 hub owner 发来的请求(还要进行验签),能够访问任意数据。

由于权限验证是利用Auth 层的 kid 来进行验证的,所以这里还要验证 kid 与 iss 为同一 DID 所有者才能证明是 hub owner 发来的请求。

通过授权后,hub 在返回数据时会附加 PermissionGrant 中的 key(重加密密钥),hub 客户端收到数据后,先对密钥密文进行重加密计算,然后再解密真实数据。

这里没有将重加密过程放在创建授权时,是因为存储在 hub 中的数据可能会有多个版本,我们希望授权给第三方 DID 后,第三方 DID 在授权有效期内总能获得最新数据,这样每次有数据更新,都要另外创建一份授权重加密数据,该过程比较麻烦,不适用。

而将重加密过程放在服务端,在被授权方 DID 获取数据时,返回最新数据的重加密版本,可以保证被授权方每次都获取最新数据。

4.4.3. 撤销授权

撤销授权需要 hub owner 修改PermissionGrant的权限,也即写入一个新的PermissionGrant,将其对应的权限置为空就可以。

4.4. 同步

Identity hub 只要遵循统一的标准,可以由不同的语言和数据库来实现。一个用户可以在多个不同的设备上部署自己的 Identity hub,例如可以在手机端部署Java/Swift 版本,在云端部署 Node 版本,在音乐、游戏设备上部署.Net版本。属于同一个账户的 hub 实例需要互相同步数据和请求以保证一致性。

这里可以采用 couchdb 的同步机制来实现,couchdb 可以实现对等节点的同步,而不是主从复制的模式,并且可以自解决冲突,适合多个 hub 实例数据的同步。而且在移动端使用 punchdb 也可以和 couchdb 进行同步。

同步逻辑为:
1. hub 定时从 DID resolver 中获取 owner 的 DID document
2. 从 DID document 中的 service 字段解析所有 hub 实例地址
3. 向其他实例地址查询使用的 couchdb 地址
4. 其他实例根据鉴权结果检测请求方的 DID 是否和自己一样,如果一致,则返回 couchdb 地址,否则返回错误
5. 收到couchdb 地址后,与对方的数据库进行同步

5. 主要数据结构

type Header struct {
    CommittedAt    string      `json:"committedAt"`        // commit created time
    Iss            string      `json:"iss"`                // ctrator DID
    Kid            string      `json:"kid"`                 // key used to sign the commit
    Sub            string      `json:"sub"`                // owner DID
    Interface      string      `json:"interface"`          // Profile, Actions, Stores, Collections, Services(data type)
    Context        string      `json:"context"`            // schema context, "http://schema.org"
    Type           string      `json:"type"`               // WriteRequest, CommitQueryRequest, ObjectQueryRequest
    Operation      string      `json:"operation"`          // create, update, delete
    CommitStrategy string      `json:"commitStrategy"`     // "basic"
    ObjectId       string      `json:"objectId,omitempty"` // optional
    Revision       string      `json:"revision"`           // commit hash
    Meta           interface{} `json:"meta,omitempty"`     //application-specific metadata
}
type Commit struct {
    Protected Header        `json:"protected"`
    Unprotected interface{} `json:"header"`
    Payload   string        `json:"payload"` // encrypted data
    Signature string        `json:"signature"`
}


type SearlizedCommit struct {
    Protected string      `json:"protected"`
    Payload   string      `json:"payload"`
    Header    interface{} `json:"header"`
    Signature string      `json:"signature"`
}

type WriteRequest struct {
    Context    string          `json:"@context"` // "https://schema.identity.foundation/0.1"
    Type       string          `json:"@type"`   // "WriteRequest"
    Iss        string          `json:"iss"`     // client DID, sender
    Sub        string          `json:"sub"`     // target DID, data owner
    Aud        string          `json:"aud"`     // hub DID, receiver
    CommitData SearlizedCommit `json:"commit"`  // searlized Commit object
}

type WriteResponse struct {
    Context   string   `json:"@context"` // "https://schema.identity.foundation/0.1"
    Type      string   `json:"@type"`   // "WriteResponse"
    Message   string   `json:"message"` // data
    Revisions []string `json:"revisions"`
}

type CommitQuery struct {
    ObjectId []string `json:"objectId"`
}

type CommitQueryRequest struct {
    Context string      `json:"@context"` // "https://schema.identity.foundation/0.1"
    Type    string      `json:"@type"`   // "CommitQueryRequest"
    Iss     string      `json:"iss"`     // client DID, sender
    Sub     string      `json:"sub"`     // target DID, data owner
    Aud     string      `json:"aud"`     // hub DID, receiver
    Query   CommitQuery `json:"query"`
}

type CommitQueryResponse struct {
    Context string   `json:"@context"` // "https://schema.identity.foundation/0.1"
    Type    string   `json:"@type"`   // "CommitQueryResponse"
    Message string   `json:"message"` // responsed data, such as "completely optional"
    Commits []Commit `json:"commits"`
}

type ObjectQuery struct {
    Interface string   `json:"interface"`         // Profile, Actions, Stores, Collections, Services(data interface type)
    Context   string   `json:"context,omitempty"` // schema context, "http://schema.org"
    Type      string   `json:"type,omitempty"`    // detailed data type
    ObjectId  []string `json:"objectId"`
}
type ObjectQueryRequest struct {
    Context string      `json:"@context"` // "https://schema.identity.foundation/0.1"
    Type    string      `json:"@type"`   // "ObjectQueryRequest"
    Iss     string      `json:"iss"`     // client DID, sender
    Sub     string      `json:"sub"`     // target DID, data owner
    Aud     string      `json:"aud"`     // hub DID, receiver
    Query   ObjectQuery `json:"query"`
}
type ObjectMetaData struct {
    Interface      string      `json:"interface"`         // Profile, Actions, Stores, Collections, Services(data interface type)
    Context        string      `json:"context,omitempty"` // schema context, "http://schema.org"
    Type           string      `json:"type,omitempty"`    // detailed data type
    ObjectId       string      `json:"objectId"`
    CreatedBy      string      `json:"createdBy"`
    CreatedAt      string      `json:"createdAt"`
    Sub            string      `json:"sub"`            // target DID, data owner
    CommitStrategy string      `json:"commitStrategy"` //"basic"
    Meta           interface{} `json:"meta,omitempty"`
}
type CommitObjectResponse struct {
    Context string   `json:"@context"` // "https://schema.identity.foundation/0.1"
    Type    string   `json:"@type"`   // "CommitQueryResponse"
    Message string   `json:"message"` // responsed data, such as "completely optional"
    Commits []Commit `json:"commits"`
}

type HubError struct {
    Context string `json:"@context"` // "https://schema.identity.foundation/0.1"
    Type    string `json:"@type"`   // "ErrorResponse"
    Message string `json:"message"` // data
    Code    string `json:"code"`    // status code
}

type AuthHeader struct {
    Alg             string `json:"alg"`
    Kid             string `json:"kid"` // sender's did publickey descriptor
    DIDRequestNonce string `json:"did-requester-nonce"`
    DIDAccessToken  string `json:"did-access-token"`
}
type AuthEnvlope struct {
    Header    AuthHeader `json:"header"`
    Payload   string     `json:"payload"` //data content
    Signature string     `json:"signature"`
}

type PermissionGrant struct {
    Owner    string `json:"owner"`   // Owner DID granting permission
    Grantee  string `json:"grantee"` // DID the permission is granted to
    Context  string `json:"context"` // the data schema context
    ObjectId string `json:"objectId"`
    Allow    string `json:"allow"` // Permissioned allowed in the form of a "CRUD" string, or "----" for no permission
    Rk       string `json:"key"`   // proxy re-encryption key
    Exp      Int    `json:"expiration"` //expiration time for permission
}

6. RoadMap

本项目基于微软的 Identity hub 开发,补充完整了一些新功能,具体计划如下:

2019.10.15
1. 主体框架逻辑
2. did-auth-jose 添加 secp256k1支持
3. 小程序对接
4. 数据的基本增删改查
5. 添加数据授权
6. 代理重加密

2019.12.31
1. hub 与 DIF resolver 对接
2. 提供与 hub 交互 sdk
3. 部署 hub 流程
4. 整理代码提到 github 与百度开源仓库

新添加的主要特性包括: 1. 对 secp256k1密钥的支持 2. 授权模型扩展,支持对单个 object 的授权 3. 数据加密存储,通过代理重加密分享 4. 删除 object 的逻辑优化 5. 对百度 DID resolver 的适配 6. sdk 适配 web 和小程序

7. Reference