This specification describes mechanisms for ensuring the authenticity and integrity of [=verifiable credentials=] and similar types of constrained digital documents using cryptography, especially through the use of digital signatures and related mathematical proofs.

Comments regarding this specification are welcome at any time. Please file issues directly on GitHub, or send them to public-vc-comments@w3.org if that is not possible. (subscribe, archives).

Introduction

This specification describes mechanisms for ensuring the authenticity and integrity of [=verifiable credentials=] and similar types of constrained digital documents using cryptography, especially through the use of digital signatures and related mathematical proofs. Cryptographic proofs enable functionality that is useful to implementors of distributed systems. For example, proofs can be used to:

Full Disclosure

From the [[[VC-DATA-MODEL-2.0]]], a [=verifiable credential=] is a set of one or more [=claims=] made by the same [=issuer=]. A particular [=holder=] can have verifiable credentials from many issuers. When presenting to a verifier, a holder can choose just one or a subset from their existing verifiable credentials to form a verifiable presentation. By minimizing the subset of credentials in a [=verifiable presentation=] presented to a given [=verifier=], privacy is enhanced.

In the simplest deployment, only entire verifiable credentials can be shared through verifiable presentations. In other words, to share a single claim from a given verifiable credential, the entire verifiable credential has to be part of the verifiable presentation. This approach is referred to as a full disclosure.

How Full Disclosure Works

The operation of Data Integrity is conceptually simple. To create a cryptographic proof, the following steps are performed: 1) Transformation, 2) Hashing, and 3) Proof Generation.


Diagram showing the three steps involved in the creation of a cryptographic
proof. The diagram is laid out left to right with a blue box labeled 'Data'
on the far left. The blue box travels, left to right, through three subsequent
yellow arrows labeled 'Transform Data', 'Hash Data', and 'Generate Proof'. The
resulting blue box at the far right is labeled 'Data with Proof'.
To create a cryptographic proof, data is transformed, hashed, and cryptographically protected.

Transformation is a process described by a transformation algorithm that takes input data and prepares it for the hashing process. One example of a possible transformation is to take a record of people's names that attended a meeting, sort the list alphabetically by the individual's family name, and rewrite the names on a piece of paper, one per line, in sorted order. Examples of transformations include canonicalization and binary-to-text encoding.

Hashing is a process described by a hashing algorithm that calculates an identifier for the transformed data using a cryptographic hash function. This process is conceptually similar to how a phone address book functions, where one takes a person's name (the input data) and maps that name to that individual's phone number (the hash). Examples of cryptographic hash functions include SHA-3 and BLAKE-3.

Proof Generation is a process described by a proof serialization algorithm that calculates a value that protects the integrity of the input data from modification or otherwise proves a certain desired threshold of trust. This process is conceptually similar to the way a wax seal can be used on an envelope containing a letter to establish trust in the sender and show that the letter has not been tampered with in transit. Examples of proof serialization functions include digital signatures, proofs of stake, and proofs of knowledge, in general.

To verify a cryptographic proof, the following steps are performed: 1) Transformation, 2) Hashing, and 3) Proof Verification.


Diagram showing the three steps involved in the verification of a cryptographic
proof. The diagram is laid out left to right with a blue box labeled
'Data with Proof' on the far left. The blue box travels, left to right, through
three subsequent yellow arrows labeled 'Transform Data', 'Hash Data', and
'Verify Proof'. The resulting blue box at the far right is labeled 'Data with
Proof'.
To verify a cryptographic proof, data is transformed, hashed, and checked for correctness.

During verification, the [=transformation=] and [=hashing=] steps are conceptually the same as described above.

Proof Verification is a process that is described by a proof verification algorithm that applies a cryptographic proof verification function to see if the input data can be trusted. Possible proof verification functions include digital signatures, proofs of stake, and proofs of knowledge, in general.

This specification details how cryptographic software architects and implementers can package these processes together into things called [=cryptographic suites=] and provide them to application developers for the purposes of protecting the integrity of application data in transit and at rest.

Selective Disclosure

[=Full disclosure=] has obvious and significant implications for the privacy of both subjects and holders. To further enhance the privacy of a holder, an issuer can create a verifiable credential that supports selective disclosure. This is a cryptographic technique that allows an [=issuer=] to create a [=verifiable credential=] that enables a holder to selectively reveal only a subset of [=claims=] from the original credential in a derived credential that will be presented to a [=verifier=]. This is achieved with assurances from the issuer to the verifier of the integrity and authenticity of the data in the [=derived credential=].

Note that [=selective disclosure=] typically applies at the [=claim=] level. A holder cannot alter the meaning of an issued claim and preserve the verifiability of the [=derived credential=]. They can only limit which claims end up in the [=derived credential=] via a set of selective disclosure constraints. However, in some situations, more advanced techniques, such as committed disclosure combined with Zero Knowledge Proofs (ZKPs), can allow a holder to reveal less than "full claim" information while maintaining issuer-based authenticity and data integrity assurances. Furthermore, the issuer can optionally specify that certain claims from the original credential need to be revealed via mandatory disclosure constraints. Though it is typically the responsibility of the verifier to indicate which claims they understand and want to verify, they are expected to always ask for what they need, regardless of the possibility of this constraint.

How Selective Disclosure Works

In Section [[[#how-it-works]]], the fundamental processes of creating and verifying a cryptographic proof are explained. In selective disclosure, there are three processes rather than two: 1) the issuer creates a base proof, 2) the holder creates a derived proof from the [=base proof=], and 3) the verifier verifies the [=derived proof=]. The reason for this extra step is that methods for creating a [=derived proof=] are much more efficient when used in combination with a [=base proof=] that is created in a particular manner. Holders are expected never to reveal [=base proofs=] when presenting, as some of the information in the base proofs might be intended only for the holder's use, for example, to create privacy-preserving [=derived proofs=].

In [[[#hiw-base-creation]]], the issuer may define [=mandatory disclosure constraints=] that separate the claims between mandatory claims (i.e., claims that have to appear in the derived credential regardless of other constraints) from the non-mandatory claims that do not have to appear in the [=derived credential=]. The imposition of mandatory disclosure constraints is optional.

The [=transformation=] and [=hashing=] steps are similar to the full disclosure case except that the [=mandatory claims|mandatory=] and [=non-mandatory claims|non-mandatory=] claims are transformed and hashed separately. The details of the these steps depend on the underlying cryptographic algorithm.

  • The simple approach (referred to as [=SIC=], i.e., "Sign Individual Claims"), involves the generation of an ephemeral cryptographic key-pair. This key-pair is scoped to the specific [=base proof|proof=], and its private key is thrown away once the [=base proof=] is generated. This ephemeral key-pair is used to sign the block of [=mandatory claims|mandatory=] and, one-by-one, the [=non-mandatory claims|non-mandatory=] claims. The data structure for the [=base proof=], which is then signed, includes these signature values, alongside the public key of the ephemeral key-pair.
  • [=SIC=] has the downside of possibly producing very large data structures for the [=base proof=]. This happens if the signature sizes are large, which is the case of some quantum resistant signature schemes, and this may become prohibitive for some applications. An alternative approach (referred to as [=SHoC=], i.e., "Salted Hashes of Claims") may be used in this case: a salt value (i.e., a random nonce) is created for each [=claim=], then a hash function is applied to the salt+claim pair. The data structure for the [=base proof=], which is then signed, includes the list of hashes, one for each salt+claim, paired with the corresponding salt values. The [=SHoC=] approach has the downside of a more complex implementation, it can leak information about the total number of signed claims, and that all of the hashes have to be included in a proof, even for claims that are not revealed, increasing its size (but based on hash size, not signature size).

See and [[[#SDApproachTable]]] for the technical details and comparisons of [=SIC=] and [=SHoC=] (plus a third approach, adapted to a very specific signature scheme, i.e., [[VC-DI-BBS]]).


Diagram showing the three steps involved in the creation of a cryptographic
base proof. The diagram is laid out left to right with a blue box labeled 'Data'
on the far left. The blue box travels, left to right, through three subsequent
yellow arrows labeled 'Transform Data', 'Hash Data', and 'Generate Base Proof'.
The resulting blue box at the far right is labeled 'Data with Base Proof'. An
additional blue box  labeled  'Mandatory Disclosure Constraints' also connects
to the yellow arrow labeled 'Transform Data'.
To create a cryptographic base proof, data is transformed, subject to [=mandatory disclosure constraints=], hashed, and cryptographically protected.

In [[[#hiw-derived-creation]]], the holder gets to specify what subset of information gets revealed to the verifier via the selective disclosure constraints. These are used to produce a [=derived credential=] via transformation and [=derived proof=] generation steps. They are carried out in such a way as to preserve the authenticity and integrity of the derived data with respect to the issuer's intent. The [=derived proof=] data structure contains a reference to each [=claim=] that is either [=mandatory claim|mandatory=] or is determined by the [=selective disclosure constraints=]. The details depend on whether the [=SIC=] or [=SHoC=] approach is used. In the [=SIC=] case this contains the list of signed claims. In the [=SHoC=] case the structure is more complex; it includes the list of the selected salt+claim entries, their corresponding hash values, and the overall list of hash+salt pairs for all [=non-mandatory claims=] (needed for verification).

The selective disclosure constraints would typically be set to meet the minimum requirements for acceptance of the [=derived credential=] by the verifier.


Diagram showing the three steps involved in the creation of a cryptographic
derived proof. The diagram is laid out left to right with a blue box labeled
'Data with Base Proof' on the far left. The blue box travels, left to right,
through two subsequent yellow arrows labeled 'Transform Data' and
'Generate Derived Proof'. The resulting blue box at the far right is labeled
'Selectively Disclosed Data with Derived Proof'. An
additional blue box  labeled  'Selective Disclosure Constraints' also connects
to the yellow arrow labeled 'Transform Data'.
To create a selectively disclosed document with cryptographic derived proof, data, together with a base proof, is transformed, subject to [=selective disclosure constraints=] and appropriate cryptographic processing.

Finally, in [[[#hiw-derived-verification]]], the process of verifying the selectively disclosed data is shown.

Diagram showing
the three steps involved in the verification of a cryptographic derived
proof. The diagram is laid out left to right with a blue box labeled
'Selectively Disclosed Data with Derived Proof' on the far left. The blue box
travels, left to right, through three subsequent yellow arrows labeled
'Transform Data', 'Hash Data', and 'Verify Derived Proof'. The resulting green
and red diamond at the far right is labeled 'Valid/Invalid'.
To verify a selectively disclosed document with cryptographic [=derived proof=], data is transformed, hashed, and checked for correctness.

Design Goals and Rationale

This specification optimizes for the following design goals:

Simplicity
The technology is designed to be easy to use for application developers, without requiring significant training in cryptography. It optimizes for the following priority of constituencies: application developers over cryptographic suite implementers, over cryptographic suite designers, over cryptographic algorithm specification authors. The solution focuses on sensible defaults to prevent the selection of ineffective protection mechanisms. See section [[[#protecting-application-developers]]] and [[[#versioning-cryptography-suites]]] for further details.
Composability
A number of historical digital signature mechanisms have had monolithic designs which limited use cases by combining data transformation, syntax, digital signature, and serialization into a single specification. This specification layers each component such that a broader range of use cases are enabled, including generalized selective disclosure and serialization-agnostic signatures. See section [[[#transformations]]], section [[[#data-opacity]]], and [[[#versioning-cryptography-suites]]] for further rationale.
Resilience
Since digital proof mechanisms might be compromised without warning due to technological advancements, it is important that [=cryptographic suites=] provide multiple layers of protection and can be rapidly upgraded. This specification provides for both algorithmic agility and cryptographic layering, while still keeping the digital proof format easy for developers to understand and use. See section [[[#agility-and-layering]]] to understand the particulars.
Progressive Extensibility
Creating and deploying new cryptographic protection mechanisms is designed to be a deliberate, iterative, and careful process that acknowledges that extension happens in phases from experimentation, to implementation, to standardization. This specification strives to balance the need for an increase in the rate of innovation in cryptography with the need for stable production-grade cryptography suites. See section [[[#cryptographic-suites]]] for instructions on establishing new types of cryptographic proofs.
Serialization Flexibility
Cryptographic proofs can be serialized in many different but equivalent ways and have often been tightly bound to the original document syntax. This specification enables one to create cryptographic proofs that are not bound to the original document syntax, which enables more advanced use cases such as being able to use a single digital signature across a variety of serialization syntaxes such as JSON and CBOR without the need to regenerate the cryptographic proof. See section [[[#transformations]]] for an explanation of the benefits of such an approach.

While this specification primarily focuses on [=verifiable credentials=], the design of this technology is generalized, such that it can be used for other use cases. In these instances, implementers are expected to perform their own due diligence and expert review as to the applicability of the technology to their use case.

A conforming secured document is any [=byte sequence=] that can be converted to a JSON document that follows the relevant normative requirements in Sections [[[#proofs]]], [[[#proof-purposes]]], [[[#resource-integrity]]], [[[#contexts-and-vocabularies]]], and [[[#dataintegrityproof]]].

A conforming cryptographic suite specification is any specification that follows the relevant normative requirements in Section [[[#cryptographic-suites]]].

A conforming processor is any algorithm realized as software and/or hardware that generates and/or consumes a [=conforming secured document=] according to the relevant normative statements in Section [[[#algorithms]]]. Conforming processors MUST produce errors when non-conforming documents are consumed.

Terminology

Some terminology used throughout this document is defined in the Terminology section of the [[[VC-DATA-MODEL-2.0]]] specification as well as the Terminology section of the [[[CID]]] specification. This section defines addition terms used throughout this specification.

data integrity proof
A set of attributes that represent a digital proof and the parameters required to verify it. A digital signature is a type of data integrity proof.
public key
Cryptographic material that can be used to verify digital proofs created with a corresponding [=secret key=].
secret key
Cryptographic material, sometimes referred to as a private key, that is not to be shared with anyone, and is used to generate digital proofs and/or digital signatures.
proof purpose
The specific intent for the proof; the reason why an entity created it. The protected declaration acts as a safeguard to prevent the proof from being misused for a purpose other than the one it was intended for.
cryptographic suite
A specification defining the usage of specific cryptographic primitives in order to achieve a particular security goal. These documents are often used to specify [=verification methods=], digital signature types, their identifiers, and other related properties. See Section [[[#cryptographic-suites]]] for further detail.

Data Model

This section specifies the data model that is used for expressing [=data integrity proofs=] and the integrity of related resources.

All of the data model properties and types in this specification map to URLs. The vocabulary where these URLs are defined is the [[[?SECURITY-VOCABULARY]]]. The explicit mechanism that is used to perform this mapping in a secured document is the `@context` property.

The mapping mechanism is defined by [[[JSON-LD11]]]. To ensure a document can be interoperably consumed without the use of a JSON-LD library, document authors are advised to ensure that domain experts have 1) specified the expected order for all values associated with a `@context` property, 2) published cryptographic hashes for each `@context` file, and 3) deemed that the contents of each `@context` file are appropriate for the intended use case.

When a document is processed by a processor that does not utilize JSON-LD libraries, and there is a requirement to use the same semantics as those used in a JSON-LD environment, implementers are advised to 1) enforce the expected order and values in the `@context` property, and 2) ensure that each `@context` file matches the known cryptographic hashes for each `@context` file.

Using static, versioned, `@context` files with published cryptographic hashes in conjunction with JSON Schema is one acceptable approach to implementing the mechanisms described above, which ensures proper term identification, typing, and order, when a processor that does not utilize a JSON-LD library is used. See the section on Type-Specific Processing in [[[?VC-DATA-MODEL-2.0]]] for more details.

Proofs

A [=data integrity proof=] provides information about the proof mechanism, parameters required to verify that proof, and the proof value itself. All of this information is provided using Linked Data vocabularies such as [[[?SECURITY-VOCABULARY]]].

When expressing a [=data integrity proof=] on an object, a `proof` property MUST be used. The `proof` property within a [=verifiable credential=] is a [=named graph=]. If present, its value MUST be either a single object, or an unordered set of objects, expressed using the properties below:

id
An optional identifier for the proof, which MUST be a URL [[URL]], such as a UUID as a URN (`urn:uuid:6a1676b8-b51f-11ed-937b-d76685a20ff5`). The usage of this property is further explained in Section [[[#proof-chains]]].
type
The specific type of proof MUST be specified as a [=string=] that maps to a URL [[URL]]. Examples of proof types include `DataIntegrityProof` and `Ed25519Signature2020`. Proof types determine what other fields are required to secure and verify the proof.
proofPurpose
The reason the proof was created MUST be specified as a [=string=] that maps to a URL [[URL]]. The proof purpose acts as a safeguard to prevent the proof from being misused by being applied to a purpose other than the one that was intended. For example, without this value the creator of a proof could be tricked into using cryptographic material typically used to create a Verifiable Credential (`assertionMethod`) during a login process (`authentication`) which would then result in the creation of a [=verifiable credential=] they never meant to create instead of the intended action, which was to merely log in to a website.
verificationMethod
A verification method is the means and information needed to verify the proof. If included, the value MUST be a [=string=] that maps to a [[URL]]. Inclusion of `verificationMethod` is OPTIONAL, but if it is not included, other properties such as `cryptosuite` might provide a mechanism by which to obtain the information necessary to verify the proof. Note that when `verificationMethod` is expressed in a [=data integrity proof=], the value points to the actual location of the data; that is, the `verificationMethod` references, via a URL, the location of the [=public key=] that can be used to verify the proof. This [=public key=] data is stored in a [=controlled identifier document=], which contains a full description of the verification method.
cryptosuite
An identifier for the cryptographic suite that can be used to verify the proof. See [[[#cryptographic-suites]]] for more information. If the proof `type` is `DataIntegrityProof`, `cryptosuite` MUST be specified; otherwise, `cryptosuite` MAY be specified. If specified, its value MUST be a string.
created
The date and time the proof was created is OPTIONAL and, if included, MUST be specified as an [[XMLSCHEMA11-2]] `dateTimeStamp` string, either in Universal Coordinated Time (UTC), denoted by a Z at the end of the value, or with a time zone offset relative to UTC. A [=conforming processor=] MAY chose to consume time values that were incorrectly serialized without an offset. Incorrectly serialized time values without an offset are to be interpreted as UTC.
expires
The `expires` property is OPTIONAL and, if present, specifies when the proof expires. If present, it MUST be an [[XMLSCHEMA11-2]] `dateTimeStamp` string, either in Universal Coordinated Time (UTC), denoted by a Z at the end of the value, or with a time zone offset relative to UTC. A [=conforming processor=] MAY chose to consume time values that were incorrectly serialized without an offset. Incorrectly serialized time values without an offset are to be interpreted as UTC.
domain
The `domain` property is OPTIONAL. It conveys one or more security domains in which the proof is meant to be used. If specified, the associated value MUST be either a string, or an unordered set of strings. A verifier SHOULD use the value to ensure that the proof was intended to be used in the security domain in which the verifier is operating. The specification of the `domain` parameter is useful in challenge-response protocols where the verifier is operating from within a security domain known to the creator of the proof. Example domain values include: `domain.example` (DNS domain), `https://domain.example:8443` (Web origin), `mycorp-intranet` (bespoke text string), and `b31d37d4-dd59-47d3-9dd8-c973da43b63a` (UUID).
challenge
A [=string=] value that SHOULD be included in a proof if a `domain` is specified. The value is used once for a particular [=domain=] and window of time. This value is used to mitigate replay attacks. Examples of a challenge value include: `1235abcd6789`, `79d34551-ae81-44ae-823b-6dadbab9ebd4`, and `ruby`.
proofValue
A [=string=] value that expresses base-encoded binary data necessary to verify the digital proof using the `verificationMethod` specified. The value MUST use a header and encoding as described in Section 2.4 Multibase of the [[[CID]]] specification to express the binary data. The contents of this value are determined by a specific cryptosuite and set to the proof value generated by the Add Proof Algorithm for that cryptosuite. Alternative properties with different encodings specified by the cryptosuite MAY be used, instead of this property, to encode the data necessary to verify the digital proof.
previousProof
The `previousProof` property is OPTIONAL. If present, it MUST be a string value or an unordered list of string values. Each value identifies another [=data integrity proof=], all of which MUST also verify for the current proof to be considered verified. This property is used in Section [[[#proof-chains]]].
nonce
An OPTIONAL [=string=] value supplied by the proof creator. One use of this field is to increase privacy by decreasing linkability that is the result of deterministically generated signatures.

A proof can be added to a JSON document like the following:

{
  "myWebsite": "https://hello.world.example/"
};
        

The following proof secures the document above using the `eddsa-jcs-2022` cryptography suite [[?DI-EDDSA]], which produces a verifiable digital proof by transforming the input data using the JSON Canonicalization Scheme (JCS) [[?RFC8785]] and then digitally signing it using an Edwards Digital Signature Algorithm (EdDSA).

{
  "myWebsite": "https://hello.world.example/",
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "eddsa-jcs-2022",
    "created": "2023-03-05T19:23:24Z",
    "verificationMethod": "https://di.example/issuer#z6MkjLrk3gKS2nnkeWcmcxiZPGskmesDpuwRBorgHxUXfxnG",
    "proofPurpose": "assertionMethod",
    "proofValue": "zQeVbY4oey5q2M3XKaxup3tmzN4DRFTLVqpLMweBrSxMY2xHX5XTYV8nQApmEcqaqA3Q1gVHMrXFkXJeV6doDwLWx"
  }
}
        

Similarly, a proof can be added to a JSON-LD data document like the following:

{
  "@context": {"myWebsite": "https://vocabulary.example/myWebsite"},
  "myWebsite": "https://hello.world.example/"
};
        

The following proof secures the document above by using the `ecdsa-rdfc-2019` cryptography suite [[?DI-ECDSA]], which produces a verifiable digital proof by transforming the input data using the RDF Dataset Canonicalization Scheme [[?RDF-CANON]] and then digitally signing it using the Elliptic Curve Digital Signature Algorithm (ECDSA).

{
  "@context": [
    {"myWebsite": "https://vocabulary.example/myWebsite"},
    "https://w3id.org/security/data-integrity/v2"
  ],
  "myWebsite": "https://hello.world.example/",
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "ecdsa-rdfc-2019",
    "created": "2020-06-11T19:14:04Z",
    "verificationMethod": "https://ldi.example/issuer#zDnaepBuvsQ8cpsWrVKw8fbpGpvPeNSjVPTWoq6cRqaYzBKVP",
    "proofPurpose": "assertionMethod",
    "proofValue": "zXb23ZkdakfJNUhiTEdwyE598X7RLrkjnXEADLQZ7vZyUGXX8cyJZRBkNw813SGsJHWrcpo4Y8hRJ7adYn35Eetq"
  }
}
        

This specification enables the expression of dates and times, such as through the `created` and `expires` properties. This information might be indirectly exposed to an individual if a proof is processed and is detected to be outside an allowable time range. When displaying date and time values related to the validity of cryptographic proofs, implementers are advised to respect the locale and local calendar preferences of the individual [[?LTLI]]. Conversion of timestamps to local time values are expected to consider the time zone expectations of the individual. See for more details about representing time values to individuals.

{
  "@context": [
    {"myWebsite": "https://vocabulary.example/myWebsite"},
    "https://w3id.org/security/data-integrity/v2"
  ],
  "myWebsite": "https://hello.world.example/",
  "proof": {
    "type": "DataIntegrityProof",
    "cryptosuite": "ecdsa-rdfc-2019",
    "created": "2020-06-11T19:14:04Z",
    // the proof expires a month after it was created
    "expires": "2020-07-11T19:14:04Z",
    "verificationMethod": "https://ldi.example/issuer#zDnaepBuvsQ8cpsWrVKw8fbpGpvPeNSjVPTWoq6cRqaYzBKVP",
    "proofPurpose": "assertionMethod",
    "proofValue": "z98X7RLrkjnXEADJNUhiTEdwyE5GXX8cyJZRLQZ7vZyUXb23ZkdakfRJ7adYY8hn35EetqBkNw813SGsJHWrcpo4"
  }
}
        

The Data Integrity specification supports the concept of multiple proofs in a single document. There are two types of multi-proof approaches that are identified: Proof Sets (un-ordered) and Proof Chains (ordered).

Proof Sets

A proof set is useful when the same data needs to be secured by multiple entities, but where the order of proofs does not matter, such as in the case of a set of signatures on a contract. A proof set, which has no order, is represented by associating a set of proofs with the `proof` key in a document.

{
  "@context": [
    {"myWebsite": "https://vocabulary.example/myWebsite"},
    "https://w3id.org/security/data-integrity/v2"
  ],
  "myWebsite": "https://hello.world.example/",
  "proof": [{
    // This is one of the proofs in the set
    "type": "DataIntegrityProof",
    "cryptosuite": "eddsa-rdfc-2022",
    "created": "2020-11-05T19:23:24Z",
    "verificationMethod": "https://ldi.example/issuer/1#z6MkjLrk3gKS2nnkeWcmcxiZPGskmesDpuwRBorgHxUXfxnG",
    "proofPurpose": "assertionMethod",
    "proofValue": "z4oey5q2M3XKaxup3tmzN4DRFTLVqpLMweBrSxMY2xHX5XTYVQeVbY8nQAVHMrXFkXJpmEcqdoDwLWxaqA3Q1geV6"
  }, {
    // This is the other proof in the set
    "type": "DataIntegrityProof",
    "cryptosuite": "eddsa-rdfc-2022",
    "created": "2020-11-05T13:08:49Z",
    "verificationMethod": "https://pfps.example/issuer/2#z6MkGskxnGjLrk3gKS2mesDpuwRBokeWcmrgHxUXfnncxiZP",
    "proofPurpose": "assertionMethod",
    "proofValue": "z5QLBrp19KiWXerb8ByPnAZ9wujVFN8PDsxxXeMoyvDqhZ6Qnzr5CG9876zNht8BpStWi8H2Mi7XCY3inbLrZrm95"
  }]
}
        

Proof Chains

A proof chain is useful when the same data needs to be signed by multiple entities and the order of when the proofs occurred matters, such as in the case of a notary counter-signing a proof that had been created on a document. A proof chain, where proof order needs to be preserved, is expressed by providing at least one proof with an `id`, such as a UUID [[?RFC9562]], and another proof with a `previousProof` value that identifies the previous proof.

{
  "@context": [
    {"myWebsite": "https://vocabulary.example/myWebsite"},
    "https://w3id.org/security/data-integrity/v2"
],
  "myWebsite": "https://hello.world.example/",
  "proof": [{
    // The 'id' value identifies this specific proof
    "id": "urn:uuid:60102d04-b51e-11ed-acfe-2fcd717666a7",
    "type": "DataIntegrityProof",
    "cryptosuite": "eddsa-rdfc-2022",
    "created": "2020-11-05T19:23:42Z",
    "verificationMethod": "https://ldi.example/issuer/1#z6MkjLrk3gKS2nnkeWcmcxiZPGskmesDpuwRBorgHxUXfxnG",
    "proofPurpose": "assertionMethod",
    "proofValue": "zVbY8nQAVHMrXFkXJpmEcqdoDwLWxaqA3Q1geV64oey5q2M3XKaxup3tmzN4DRFTLVqpLMweBrSxMY2xHX5XTYVQe"
  }, {
    "type": "DataIntegrityProof",
    "cryptosuite": "eddsa-rdfc-2022",
    "created": "2020-11-05T21:28:14Z",
    "verificationMethod": "https://pfps.example/issuer/2#z6MkGskxnGjLrk3gKS2mesDpuwRBokeWcmrgHxUXfnncxiZP",
    "proofPurpose": "assertionMethod",
    "proofValue": "z6Qnzr5CG9876zNht8BpStWi8H2Mi7XCY3inbLrZrm955QLBrp19KiWXerb8ByPnAZ9wujVFN8PDsxxXeMoyvDqhZ",
    // The 'previousProof' value identifies which proof is verified before this one
    "previousProof": "urn:uuid:60102d04-b51e-11ed-acfe-2fcd717666a7"
  }]
}
        

Proof Graphs

When securing data in a document, it is important to clearly delineate the data being protected, which is every graph expressed in the document except the one containing the data associated with a securing mechanism, which is called a proof graph. Creating this separation enables the processing algorithms to deterministically protect and verify a secured document.

The information contained in an input document before a [=data integrity proof=] is added to the document is expressed in one or more graphs. To ensure that information from different [=data integrity proofs=] is not accidentally co-mingled, the concept of a [=proof graph=] is used to encapsulate each [=data integrity proof=]. Each value associated with the `proof` property of the document identifies a separate graph, which is sometimes referred to as a named graph, of type ProofGraph, which contains a single [=data integrity proof=].

Using these graphs has a concrete effect when performing JSON-LD processing, as this properly separates statements expressed in one graph from those in another graph. Implementers that limit their processing to other media types, such as JSON, YAML, or CBOR, will need to keep this in mind if they merge data from one document with data from another, such as when an `id` value string is the same in both documents. It is important to not merge objects that seem to have similar properties, when those objects do not have an `id` property and/or use a global identifier type such as a URL, as without these, is not possible to tell whether two such objects are expressing information about the same entity.

Proof Purposes

A proof that describes its purpose helps prevent it from being misused for some other purpose. [=Proof purposes=] enable [=verifiers=] to know the intent of the creator of a proof so a message cannot be accidentally abused for another purpose. For example, a message signed for the purpose of merely making an assertion (perhaps intended to be widely shared) being abused as a message to authenticate to a service or take some action (such as invoking a capability to do something).

It is important to note that [=proof purposes=] are a different mechanism from the `key_ops` restrictions in [[[?RFC7517]]], the `KeyUsage` restriction in the [[[?WEBCRYPTOAPI]]] and the [[[?RFC5280]]]. [=Proof purposes=] are expressions on why a [=proof=] was created and its intended domain of usage whereas the other mechanisms mentioned are intended to limit what a private key can be used to do. A [=proof purpose=] "travels" with the [=proof=] while a key restriction does not.

The following is a list of commonly used [=proof purpose=] values.

authentication
Indicates that a given proof is only to be used for the purposes of an authentication protocol.
assertionMethod
Indicates that a proof can only be used for making assertions, for example signing a [=verifiable credential=].
keyAgreement
Indicates that a proof is used for for key agreement protocols, such as Elliptic Curve Diffie Hellman key agreement used by popular encryption libraries.
capabilityDelegation
Indicates that the proof can only be used for delegating capabilities. See the Authorization Capabilities [[?ZCAP]] specification for more detail.
capabilityInvocation
Indicates that the proof can only be used for invoking capabilities. See the Authorization Capabilities [[?ZCAP]] specification for more detail.

Resource Integrity

When a link to an external resource is included in a [=conforming secured document=], it is desirable to know whether the resource that is identified has changed since the proof was created. This applies to cases where there is an external resource that is remotely retrieved as well as to cases where the [=verifier=] might have a locally cached copy of the resource.

To enable confirmation that a resource referenced by a [=conforming secured document=] has not changed since the document was secured, an implementer MAY include a property named `digestMultibase` in any object that includes an `id` property. If present, the `digestMultibase` value MUST be a single [=string=] value, or an [=list=] of [=string=] values, each of which is a Multibase-encoded Multihash value.

JSON-LD context authors are expected to add `digestMultibase` to contexts that will be used in documents that refer to other resources and to include an associated cryptographic digest. For example, the [[[?VC-DATA-MODEL-2.0]]] base context (`https://www.w3.org/ns/credentials/v2`) includes the `digestMultibase` property.

An example of a resource integrity protected object is shown below:

{
  ...
  "image": {
    "id": "https://university.example.org/images/58473",
    "digestMultibase": "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"
  },
  ...
}
        

Implementers are urged to consult appropriate sources, such as the FIPS 180-4 Secure Hash Standard and the Commercial National Security Algorithm Suite 2.0 to ensure that they are choosing a hash algorithm that is appropriate for their use case.

Contexts and Vocabularies

Implementations that perform JSON-LD processing MUST treat the following JSON-LD context URLs as already resolved, where the resolved document matches the corresponding hash values below:

Context URL and Hash
URL: https://w3id.org/security/data-integrity/v2
SHA2-256 Digest:
URL: https://w3id.org/security/multikey/v1
SHA2-256 Digest:
URL: https://w3id.org/security/jwk/v1
SHA2-256 Digest:

It is possible to confirm the cryptographic digests listed above by running a command like the following (replacing `<DOCUMENT_URL>` with the appropriate value) through a modern UNIX-like OS command line interface: `curl -sL -H "Accept: application/ld+json" <DOCUMENT_URL> | openssl dgst -sha256`

The security vocabulary terms that the JSON-LD contexts listed above resolve to are in the https://w3id.org/security# namespace. That is, all security terms in this vocabulary are of the form `https://w3id.org/security#TERM`, where `TERM` is the name of a term.

Implementations that perform RDF processing MUST treat the JSON-LD serialization of the vocabulary URL as already dereferenced, where the dereferenced document matches the corresponding hash value below.

Beyond the security terms defined by this specification, the https://w3id.org/security# namespace also includes the terms defined in the [[[CID]]] [[CID]] specification, with the corresponding mappings in the context files listed above.

When dereferencing the https://w3id.org/security# URL, the media type of the data that is returned depends on HTTP content negotiation. These are as follows:

Media Type Description and Hash
application/ld+json The vocabulary in JSON-LD format [[?JSON-LD11]].

SHA2-256 Digest:
text/turtle The vocabulary in Turtle format [[?TURTLE]].

SHA2-256 Digest:
text/html The vocabulary in HTML+RDFa Format [[?HTML-RDFA]].

SHA2-256 Digest:

It is possible to confirm the cryptographic digests listed above by running a command like the following (replacing `<MEDIA_TYPE>` and `<DOCUMENT_URL>` with the appropriate values) through a modern UNIX-like OS command line interface: `curl -sL -H "Accept: <MEDIA_TYPE>" <DOCUMENT_URL> | openssl dgst -sha256`

Authors of application-specific vocabularies and specifications SHOULD ensure that their JSON-LD context and vocabulary files are permanently cacheable using the approaches to caching described above or a functionally equivalent mechanism.

Implementations MAY load application-specific JSON-LD context files from the network during development, but SHOULD permanently cache JSON-LD context files used by [=conforming secured documents=] in production settings, to increase their security and privacy characteristics. Goals of processing speed MAY be achieved through caching approaches such as those described above or functionally equivalent mechanisms.

Some applications, such as digital wallets, that are capable of holding arbitrary [=verifiable credentials=] or other data-integrity-protected documents, from any issuer and using any contexts, might need to be able to load externally linked resources, such as JSON-LD context files, in production settings. This is expected to increase user choice, scalability, and decentralized upgrades in the ecosystem over time. Authors of such applications are advised to read the security and privacy sections of this document for further considerations.

For further information regarding processing of JSON-LD contexts and vocabularies, see Verifiable Credentials v2.0: Base Context and Verifiable Credentials v2.0: Vocabularies.

Validating Contexts

It is necessary to ensure that a consuming application has explicitly approved of the types, and therefore the semantics, of input documents that it will process. Not checking JSON-LD context values against known good values can lead to security vulnerabilities, due to variance in the semantics that they convey. Applications MUST use the algorithm in Section [[[#context-validation]]], or one that achieves equivalent protections, to validate contexts in a [=conforming secured document=]. Context validation MUST be run after running the applicable algorithm in either Section [[[#verify-proof]]] or Section [[[#verify-proof-sets-and-chains]]].

While the algorithm described in Section [[[#context-validation]]] provides one way of checking context values, and one optional way of safely processing unknown context values, implementers MAY use alternative approaches, or a different ordering of the steps, that provide the same protections.

For example, if no JSON-LD processing is to occur, then, rather than performing this check, an application could follow the guidance in whatever trusted documentation is provided out of band for properly understanding the semantics of that type of document.

Another approach would be to configure an application to use a JSON-LD Context loader, sometimes referred to as a document loader, to use only local copies of approved context files. This would guarantee that neither the context files nor their cryptographic hashes would ever change, effectively resulting in the same result as the algorithm in Section [[[#context-validation]]].

Another alternative approach, also effectively equivalent to the algorithm in Section [[[#context-validation]]], would be for an application to keep a list of well known context URLs and their associated approved cryptographic hashes, without storing every context file locally. This would allow these contexts to be safely loaded from the network without compromising the security expectations of the application.

Yet another valid approach would be for a transmitting application to compact a document to exactly what a receiving application requests, via a protocol such as one requesting a [=verifiable presentation=], omitting additional sender-specific context values that were used when securing the original document. As long as the cryptography suite's verification algorithm provides a successful verification result, such transformations are valid and would result in full URLs for terms that were previously compacted by the omitted context. That is, a term that was previously compacted to `foo` based on a sender-supplied context that is unknown to a receiver (e.g., ``https://ontology.example/v1`) would instead be "expanded" to a URL like `https://ontology.example#foo`, which would then be "compacted" to the same URL, once the unknown context is omitted and the JSON-LD compaction algorithm is applied by the receiving application.

Context Injection

The `@context` property is used to ensure that implementations are using the same semantics when terms in this specification are processed. For example, this can be important when properties like `type` are processed and its value, such as `DataIntegrityProof`, are used.

When an application is securing a document, if an `@context` property is not provided in the document or the Data Integrity terms used in the document are not mapped by existing values in the `@context` property, implementations SHOULD inject or append an `@context` property with a value of `https://w3id.org/security/data-integrity/v2` or one or more contexts with at least the same declarations, such as the Verifiable Credential Data Model v2.0 context (`https://www.w3.org/ns/credentials/v2`).

Implementations that do not intend to use JSON-LD processing MAY choose to not include an `@context` declaration at the top-level of the document. However, if an `@context` declaration is not included, extensions (such as the addition of new properties) related to this specification or corresponding cryptosuites MUST NOT be made.

Securing Data Losslessly

HTML processors are designed to continue processing if recoverable errors are detected. JSON-LD processors operate in a similar manner. This design philosophy was meant to ensure that developers could use only the parts of the JSON-LD language that they find useful, without causing the processor to throw errors on things that might not be important to the developer. Among other effects, this philosophy led to JSON-LD processors being designed to not throw errors, but rather warn developers, when encountering things such as undefined terms.

When converting from JSON-LD to an RDF Dataset, such as when canonicalizing a document [[?RDF-CANON]], undefined terms and relative URLs can be dropped silently. When values are dropped, they are not protected by a digital proof. This creates a mismatch of expectations, where a developer, who is unaware of how a JSON-LD processor works, might think that certain data was being secured, and then be surprised to find that it was not, when no error was thrown. This specification requires that any recoverable loss of data when performing JSON-LD transformations result in an error, to avoid a mismatch in the security expectations of developers.

Implementations that use JSON-LD processing, such as RDF Dataset Canonicalization [[?RDF-CANON]], MUST throw an error, which SHOULD be `DATA_LOSS_DETECTION_ERROR`, when data is dropped by a JSON-LD processor, such as when an undefined term is detected in an input document.

Similarly, since [=conforming secured documents=] can be transferred from one security domain to another, [=conforming processors=] that process the [=conforming secured document=] cannot assume any particular base URL for the document. When deserializing to RDF, implementations MUST ensure that the base URL is set to null.

Datatypes

This section defines datatypes that are used by this specification.

The `cryptosuiteString` Datatype

This specification encodes cryptographic suite identifiers as enumerable strings, which is useful in processes that need to efficiently encode such strings, such as compression algorithms. In environments that support data types for [=string=] values, such as RDF [[?RDF-CONCEPTS]], cryptographic identifier content is indicated using a literal value whose datatype is set to `https://w3id.org/security#cryptosuiteString`.

The `cryptosuiteString` datatype is defined as follows:

The URL denoting this datatype
`https://w3id.org/security#cryptosuiteString`
The lexical space
The union of all cryptosuite strings, expressed using American Standard Code for Information Interchange [[ASCII]] strings, that are defined by the collection of all Data Integrity cryptosuite specifications.
The value space
The union of all cryptosuite types that are expressed using the `cryptosuite` property, as defined in Section [[[#dataintegrityproof]]].
The lexical-to-value mapping
Any element of the lexical space is mapped to the result of parsing it into an internal representation that uniquely identifies the cryptosuite type from all other possible cryptosuite types.
The canonical mapping
Any element of the value space is mapped to the corresponding string in the lexical space.

Relationship to Linked Data

The term Linked Data is used to describe a recommended best practice for exposing, sharing, and connecting information on the Web using standards, such as URLs, to identify things and their properties. When information is presented as Linked Data, other related information can be easily discovered and new information can be easily linked to it. Linked Data is extensible in a decentralized way, greatly reducing barriers to large scale integration.

With the increase in usage of Linked Data for a variety of applications, there is a need to be able to verify the authenticity and integrity of Linked Data documents. This specification adds authentication and integrity protection to data documents through the use of mathematical proofs without sacrificing Linked Data features such as extensibility and composability.

While this specification provides mechanisms to digitally sign Linked Data, the use of Linked Data is not necessary to gain some of the advantages provided by this specification.

Relationship to Verifiable Credentials

Cryptographic suites that implement this specification can be used to secure [=verifiable credentials=] and [=verifiable presentations=]. Implementers that are addressing those use cases are cautioned that additional checks might be appropriate when processing those types of documents.

There are some use cases where it is important to ensure that the [=verification method=] used in a proof is associated with the `issuer` in a verifiable credential, or the `holder` in a verifiable presentation, during the process of validation. One way to check for such an association is to ensure that the value of the `controller` property of a proof's [=verification method=] matches the URL value used to identify the `issuer` or `holder`, respectively, and that the verification method is expressed under a verification relationship that is acceptable given the proof's purpose. This particular association indicates that the `issuer` or `holder`, respectively, is the controller of the [=verification method=] used to verify the proof.

Document authors and implementers are advised to understand the difference between the validity period of a proof, which is expressed using the `created` and `expires` properties, and the validity period of a credential, which is expressed using the `validFrom` and `validUntil` properties. While these properties might sometimes express the same validity periods, at other times they might not be aligned. When verifying a proof, it is important to ensure that the time of interest (which might be the current time or any other time) is within the validity period for the proof (that is, between `created` and `expires` ). When validating a [=verifiable credential=], it is important to ensure that the time of interest is within the validity period for the credential (that is, betweeen `validFrom` and `validUntil`). Note that a failure to validate either the validity period for the proof, or the validity period for the credential, might result in accepting data that ought to have been rejected.

Finally, implementers are also urged to understand that there is a difference between the revocation information associated with a [=verifiable credential=], and the revocation and expiration times for a [=verification method=]. The revocation and expiration times for a [=verification method=] are expressed using the `revocation` and `expires` properties, respectively; are related to events such as a [=secret key=] being compromised or expiring; and can provide timing information which might reveal details about a controller, such as their security practices or when they might have been compromised. The revocation information for a [=verifiable credential=] is expressed using the `credentialStatus` property; is related to events such as an individual losing the privilege that is granted by the [=verifiable credential=]; and does not provide timing information, which enhances privacy.

Cryptographic Suites

A [=data integrity proof=] is designed to be easy to use by developers and therefore strives to minimize the amount of information one has to remember to generate a proof. Often, just the [=cryptographic suite=] name (such as `eddsa-rdfc-2022`) is required from developers to initiate the creation of a proof. These [=cryptographic suite=]s are often created and reviewed by people that have the requisite cryptographic training to ensure that safe combinations of cryptographic primitives are used. This section specifies the requirements for authoring cryptographic suite specifications.

The requirements for all data integrity cryptographic suite specifications are as follows:

A [=cryptosuite instance=] is instantiated using a [=cryptosuite instantiation algorithm=] and is made available to algorithms in an implementation-specific manner. Implementations MAY use the [[[?VC-EXTENSIONS]]] document to discover known [=cryptosuite instantiation algorithms=].

DataIntegrityProof

A number of [=cryptographic suites=] follow the same basic pattern when expressing a [=data integrity proof=]. This section specifies that general design pattern, a [=cryptographic suite=] type called a `DataIntegrityProof`, which reduces the burden of writing and implementing [=cryptographic suites=] through the reuse of design primitives and source code.

When specifing a [=cryptographic suite=] that utilizes this design pattern, the `proof` value takes the following form:

type
The `type` property MUST contain the [=string=] `DataIntegrityProof`.
cryptosuite
The value of the `cryptosuite` property MUST be a [=string=] that identifies the [=cryptographic suite=]. If the processing environment supports [=string=] subtypes, the subtype of the `cryptosuite` value MUST be the `https://w3id.org/security#cryptosuiteString` subtype.
proofValue
The `proofValue` property MUST be used, as specified in Section [[[#proofs]]].

[=Cryptographic suite=] designers MUST use mandatory `proof` value properties defined in Section [[[#proofs]]], and MAY define other properties specific to their cryptographic suite.

One of the design patterns seen in Data Integrity cryptosuites from 2012 to 2020 was use of the `type` property to establish a specific type for a cryptographic suite; the Ed25519Signature2020 cryptographic suite was one such specification. This led to a greater burden on cryptographic suite implementations, where every new cryptographic suite required specification of a new JSON-LD Context, resulting in a sub-optimal developer experience. A streamlined version of this design pattern emerged in 2020, such that a developer would only need to include a single JSON-LD Context to support all modern cryptographic suites. This encouraged more modern cryptosuites — such as the EdDSA Cryptosuites [[?DI-EDDSA]] and the ECDSA Cryptosuites [[?DI-ECDSA]] — to be built based on the streamlined pattern described in this section.

To improve the developer experience, authors creating new Data Integrity cryptographic suite specifications SHOULD use the modern pattern — where the `type` is set to `DataIntegrityProof`; the `cryptosuite` property carries the identifier for the cryptosuite; and any cryptosuite-specific cryptographic data is encapsulated (i.e., not directly exposed as application layer data) within `proofValue`. A list of cryptographic suite specifications that are known to follow this pattern is provided in the Securing Mechanisms section of the Verifiable Credentials Extensions document.

Algorithms

Document Algorithms

The algorithms defined below operate on documents represented as JSON objects. This specification follows the [[[JSON-LD11-API]]] specification in representing a JSON object as a [=map=]. An unsecured data document is a [=map=] that contains no proof values. An input document is an [=map=] that has not yet had the current proof added to it, but it MAY contain a proof value that was added to it by a previous process. A secured data document is a [=map=] that contains one or more proof values.

Implementers MAY implement reasonable defaults and safeguards in addition to the algorithms below, to help mitigate developer error, excessive resource consumption, newly discovered attack models against which there is a particular protection, and other improvements. The algorithms provided below are the minimum requirements for an interoperable implementation, and developers are urged to include additional measures that could contribute to a safer and more efficient ecosystem.

Processing Model

The processing model used by a [=conforming processor=] and its application-specific software is described in this section. When software is to ensure information is tamper-evident, it performs the following steps:

  1. The software arranges the information into a document, such as a JSON or JSON-LD document.
  2. If the document is a JSON-LD document, the software selects one or more JSON-LD Contexts and expresses them using the `@context` property.
  3. The software selects one or more cryptography suites that meet the needs of the use case, such as one that provides full, selective, or unlinkable disclosure, using acceptable cryptographic key material.
  4. The software uses the applicable algorithm(s) provided in Section [[[#add-proof]]] or Section [[[#add-proof-set-chain]]] to add one or more proofs.

When software needs to use information that was transmitted to it using a mechanism described by this specification, it performs the following steps:

  1. The software transforms the incoming data into a document that can be understood by the applicable algorithm provided in Section [[[#verify-proof]]] or Section [[[#verify-proof-sets-and-chains]]].
  2. The software uses JSON Schema or an equivalent mechanism to validate that the incoming document follows an expected schema used by the application.
  3. The software uses the applicable algorithm(s) provided in Section [[[#verify-proof]]] or Section [[[#verify-proof-sets-and-chains]]] to verify the integrity of the incoming document.
  4. If the document is a JSON-LD document, the software uses the algorithm provided in Section [[[#context-validation]]], or one providing equivalent protections, to validate all JSON-LD Context values used in the document.

Add Proof

The following algorithm specifies how a digital proof can be added to an [=input document=], and can then be used to verify the output document's authenticity and integrity. Required inputs are an [=input document=] ([=map=] |inputDocument|), a [=cryptosuite instance=] ([=struct=] |cryptosuite|), and a set of options ([=map=] |options|). Output is a [=secured data document=] ([=map=]) or an error. Whenever this algorithm encodes strings, it MUST use UTF-8 encoding.

  1. Let |proof| be the result of calling the [=cryptosuite instance/createProof=] algorithm specified in |cryptosuite|.|createProof| with |inputDocument| and |options| passed as a parameters. If the algorithm produces an error, the error MUST be propagated and SHOULD convey the error type.
  2. If one or more of the |proof|.|type|, |proof|.|verificationMethod|, and |proof|.|proofPurpose| values is not set, an error MUST be raised and SHOULD convey an error type of PROOF_GENERATION_ERROR.
  3. If |options| has a non-null |domain| [=struct/item=], it MUST be equal to |proof|.|domain| or an error MUST be raised and SHOULD convey an error type of PROOF_GENERATION_ERROR.
  4. If |options| has a non-null |challenge| [=struct/item=], it MUST be equal to |proof|.|challenge| or an error MUST be raised and SHOULD convey an error type of PROOF_GENERATION_ERROR.
  5. Let |securedDataDocument| be a copy of |inputDocument|.
  6. Set |securedDataDocument|.|proof| to the value of |proof|.
  7. Return |securedDataDocument| as the [=secured data document=].

Add Proof Set/Chain

The following algorithm specifies how to incrementally add a proof to a proof set or proof chain starting with a secured document containing either a proof or proof set/chain. Required inputs are a [=secured data document=] ([=map=] |securedDocument|), a [=cryptographic suite=] ([=cryptosuite instance=] |suite|), and a set of options ([=map=] |options|). Output is a new [=secured data document=] ([=map=]). Whenever this algorithm encodes strings, it MUST use UTF-8 encoding.

  1. Let |proof| be set to |securedDocument|.|proof|. Let |allProofs| be an empty list. If |proof| is a list, copy all the elements of |proof| to |allProofs|. If |proof| is an object add a copy of that object to |allProofs|.
  2. Let the |inputDocument| be a copy of the |securedDocument| with the |proof| attribute removed. Let |output| be a copy of the |inputDocument|.
  3. Let |matchingProofs| be an empty list.
  4. If |options| has a `previousProof` [=struct/item=] that is a string, add the element from |allProofs| with an `id` attribute matching `previousProof` to |matchingProofs|. If a proof with `id` equal to `previousProof` does not exist in |allProofs|, an error MUST be raised and SHOULD convey an error type of PROOF_GENERATION_ERROR.
  5. If |options| has a `previousProof` [=struct/item=] that is an array, add each element from |allProofs| with an `id` attribute that matches an element of that array. If any element of `previousProof` [=list=] has an `id` attribute that does not match the `id` attribute of any element of |allProofs|, an error MUST be raised and SHOULD convey an error type of PROOF_GENERATION_ERROR.
  6. Set |inputDocument|.|proof| to |matchingProofs|.

    This step adds references to the [=named graphs=], as well as adding a copy of all the claims contained in the [=proof graphs=]. The step is critical, as it binds any matching proofs to the document prior to applying the current proof. The |proof| value for the document will be updated in a later step of this algorithm.

  7. Run steps 1 through 6 of the algorithm in section [[[#add-proof]]], passing |inputDocument|, |suite|, and |options|. If no exceptions are raised, append the generated |proof| value to the |allProofs|; otherwise, raise the exception.
  8. Set |output|.|proof| to the value of |allProofs|.
  9. Return |output| as the new [=secured data document=].

Verify Proof

The following algorithm specifies how to check the authenticity and integrity of a [=secured data document=] by verifying its digital proof. The algorithm takes as input:

|mediaType|
A [=MIME type|media type=] as defined in [[MIMESNIFF]]
|documentBytes|
A [=byte sequence=] whose media type is |mediaType|
|cryptosuite|
A [=cryptosuite instance=]
|expectedProofPurpose|
An optional [=string=], used to ensure that the |proof| was generated by the proof creator for the expected reason by the verifier. See [[[#proof-purposes]]] for common values
|domain|
An optional [=set=] of [=strings=], used by the proof creator to lock a proof to a particular security domain, and used by the verifier to ensure that a proof is not being used across different security domains
|challenge|
An optional [=string=] [=challenge=], used by the verifier to ensure that an attacker is not replaying previously created proofs

This algorithm returns a verification result, a [=struct=] whose [=struct/items=] are:

verified
`true` or `false`
verifiedDocument
Null, if [=verification result/verified=] is `false`; otherwise, an [=input document=]
mediaType
Null, if [=verification result/verified=] is `false`; otherwise, a [=MIME type|media type=], which MAY include [=MIME type/parameters=]
warnings
a [=list=] of [=ProblemDetails=], which defaults to an empty [=list=]
errors
a [=list=] of [=ProblemDetails=], which defaults to an empty [=list=]

When a step says "an error MUST be raised", it means that a [=verification result=] MUST be returned with a [=verification result/verified=] value of `false` and a non-empty [=verification result/errors=] list.

  1. Let |securedDocument:map| be the result of running [=parse JSON bytes to an Infra value=] on |documentBytes|.
  2. If either |securedDocument| is not a [=map=] or |securedDocument|.|proof| is not a [=map=], an error MUST be raised and SHOULD convey an error type of PARSING_ERROR.
  3. Let |proof:map| be |securedDocument|.|proof|.
  4. If one or more of |proof|.|type|, |proof|.|verificationMethod|, and |proof|.|proofPurpose| does not [=map/exist=], an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  5. If |expectedProofPurpose| was given, and it does not match |proof|.|proofPurpose|, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  6. If |domain| was given, and it does not contain the same [=strings=] as |proof|.|domain| (treating a single [=string=] as a [=set=] containing just that [=string=]), an error MUST be raised and SHOULD convey an error type of INVALID_DOMAIN_ERROR.
  7. If |challenge| was given, and it does not match |proof|.|challenge|, an error MUST be raised and SHOULD convey an error type of INVALID_CHALLENGE_ERROR.
  8. Let |cryptosuiteVerificationResult| be the result of running the |cryptosuite|.[=cryptosuite instance/verifyProof=] algorithm with |securedDocument| provided as input.
  9. Return a [=verification result=] with [=struct/items=]:
    [=verified=]
    |cryptosuiteVerificationResult|.|verified|
    [=verifiedDocument=]
    |cryptosuiteVerificationResult|.|verifiedDocument|
    [=mediaType=]
    |mediaType|

Verify Proof Sets and Chains

In a [=proof set=] or [=proof chain=], a [=secured data document=] has a `proof` attribute which contains a list of [=proofs=] (|allProofs|). The following algorithm provides one method of checking the authenticity and integrity of a [=secured data document=], achieved by verifying every proof in |allProofs|. Other approaches are possible, particularly if it is only desired to verify a subset of the proofs contained in |allProofs|. If another approach is taken to verify only a subset of the proofs, then it is important to note that any proof in that subset with a `previousProof` can only be considered verified if the proofs it references are also considered verified.

Required input is a [=secured data document=] (|securedDocument|). A list of [=verification results=] corresponding to each proof in |allProofs| is generated, and a single combined [=verification result=] is returned as output. Implementations MAY return any of the other [=verification result=]s and/or any other metadata alongside the combined [=verification result=].

  1. Set |allProofs| to |securedDocument|.|proof|.
  2. Set |verificationResults| to an empty list.
  3. For each |proof| in |allProofs|, do the following steps:
    1. Let |matchingProofs| be an empty list.
    2. If |proof| contains a `previousProof` attribute and the value of that attribute is a [=string=], add the element from |allProofs| with an `id` attribute value matching the value of `previousProof` to `matchingProofs`. If a proof with `id` value equal to the value of `previousProof` does not exist in |allProofs|, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR. If the `previousProof` attribute is a [=list=], add each element from |allProofs| with an `id` attribute value that matches the value of an element of that [=list=]. If any element of `previousProof` [=list=] has an `id` attribute value that does not match the `id` attribute value of any element of |allProofs|, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
    3. Let |inputDocument| be a copy of |securedDocument| with the proof value removed and then set |inputDocument|.|proof| to |matchingProofs|.

      See the note in Step 6 of Section [[[#add-proof-set-chain]]] to learn about what document properties and previous proofs this step secures.

    4. Run steps 4 through 8 of the algorithm in section [[[#verify-proof]]] on |inputDocument|; if no exceptions are raised, append |cryptosuiteVerificationResult| to |verificationResults|.
  4. Set |successfulVerificationResults| to an empty list.
  5. Let |combinedVerificationResult| be an empty struct. Set |combinedVerificationResult|.|status| to `true`, |combinedVerificationResult|.|document| to `null`, and |combinedVerificationResult|.|mediaType| to `null`.
  6. For each |cryptosuiteVerificationResult| in |verificationResults|:
    1. If |cryptosuiteVerificationResult|.|verified| is `false`, set |combinedVerificationResult|.|verified| to `false`.
    2. Otherwise, set |combinedVerificationResult|.|document| to |cryptosuiteVerificationResult|.|verifiedDocument|, set |combinedVerificationResult|.|mediaType| to |cryptosuiteVerificationResult|.|mediaType|, and append |cryptosuiteVerificationResult| to |successfulVerificationResults|.
  7. If |combinedVerificationResult|.|status| is `false`, set |combinedVerificationResult|.|document| to `null` and |combinedVerificationResult|.|mediaType| to `null`.
  8. Return |combinedVerificationResult|, |successfulVerificationResults|.

Context Validation

The following algorithm provides one mechanism that can be used to ensure that an application understands the contexts associated with a document before it executed business rules specific to the input in the document. For more rationale related to this algorithm, see Section [[[#validating-contexts]]]. This algorithm takes inputs of a document ([=map=] |inputDocument|), a set of known JSON-LD Contexts ([=list=] |knownContext|), and a boolean to recompact when unknown contexts are detected ([=boolean=] |recompact|).

This algorithm returns a context validation result, a [=struct=] whose [=struct/items=] are:

validated
`true` or `false`
validatedDocument
Null, if [=context validation result/validated=] is `false`; otherwise, an [=input document=]
warnings
a [=list=] of [=ProblemDetails=], which defaults to an empty [=list=]
errors
a [=list=] of [=ProblemDetails=], which defaults to an empty [=list=]

The context validation algorithm is as follows:

  1. Set |result|.|validated| to `false`, |result|.|warnings| to an empty list, |result|.|errors| to an empty list, |compactionContext| to an empty list; and clone |inputDocument| to |result|.|validatedDocument|.
  2. Let |contextValue| be the value of the `@context` property of |result|.|validatedDocument|, which might be undefined.
  3. If |contextValue| does not deeply equal |knownContext|, any subtree in |result|.|validatedDocument| contains an `@context` property, or any URI in |contextValue| dereferences to a JSON-LD Context file that does not match a known good value or cryptographic hash, then perform the applicable action:
    1. If |recompact| is `true`, set |result|.|validatedDocument| to the result of running the JSON-LD Compaction Algorithm with the |inputDocument| and |knownContext| as inputs. If the compaction fails, add at least one error to |result|.|errors|.
    2. If |recompact| is not `true`, add at least one error to |result|.|errors|.
  4. If |result|.|errors| is empty, set |result|.|validated| to `true`; otherwise, set |result|.|validated| to `false`, and remove the |document| property from |result|.
  5. Return the value of |result|.

Implementations MAY include additional warnings or errors that enforce further validation rules that are specific to the implementation or a particular use case.

Processing Errors

The algorithms described in this specification, as well as in various cryptographic suite specifications, throw specific types of errors. Implementers might find it useful to convey these errors to other libraries or software systems. This section provides specific URLs and descriptions for the errors, such that an ecosystem implementing technologies described by this specification might interoperate more effectively when errors occur.

When exposing these errors through an HTTP interface, implementers SHOULD use [[RFC9457]] to encode the error data structure as a ProblemDetails [=map=]. If [[RFC9457]] is used:

  • The `type` value of the error object MUST be a URL that starts with the value `https://w3id.org/security#` and ends with the value in the section listed below.
  • The `title` value SHOULD provide a short but specific human-readable [=string=] for the error.
  • The `detail` value SHOULD provide a longer human-readable [=string=] for the error.
PROOF_GENERATION_ERROR
A request to generate a proof failed. See Section [[[#add-proof]]], and Section [[[#add-proof-set-chain]]].
PROOF_VERIFICATION_ERROR
An error was encountered during proof verification. See Section [[[#verify-proof]]].
PROOF_TRANSFORMATION_ERROR
An error was encountered during the transformation process.
INVALID_DOMAIN_ERROR
The `domain` value in a proof did not match the expected value. See Section [[[#verify-proof]]].
INVALID_CHALLENGE_ERROR
The `challenge` value in a proof did not match the expected value. See Section [[[#verify-proof]]].

Cryptosuite Common Algorithms

Introduction

These algorithms are for the non-selective disclosure case. There are two key algorithms which are built from a common set of functions. These are "Create Proof" and "Verify Proof" which are called by the [[[#add-proof]]] and [[[#verify-proof]]] general algorithms respectively. These algorithms have a common structure for all non-selective disclosure cryptosuites. We give these algorithms in the following two sections along with the common functions that are used therein. May want to explain all the parameters here, i.e., canonicalization scheme, hash function, signature function, verification function, and proof encoding.

Create Proof

The following algorithm specifies how to create a [=data integrity proof=] given an unsecured data document and properly specified cryptosuite. The choice of cryptosuite sets the values of |canonScheme|, |hashName|, |sigFunc|, |verifyFunc|, and |proofEncoding|, which are used in the algorithm below. Additional required inputs are an unsecured data document ([=map=] |unsecuredDocument|), and a set of proof options ([=map=] |options|). A [=data integrity proof=] ([=map=]), or an error, is produced as output.

  1. Let |proof| be a clone of the proof options, |options|.
  2. Let |proofConfig| be the result of running the algorithm in Section [[[#ProofConfigurationAlg]]] with |options|, the |cypherSuiteName|, |canonScheme|, and |hashName| passed as parameters.
  3. Let |transformedData| be the result of running the algorithm in Section [[[#TransformationAlg]]] with |unsecuredDocument|, |options|, the |cypherSuiteName|, |canonScheme|, and |hashName| passed as parameters.
  4. Let |hashData| be the result of running the algorithm in Section [[[#HashingAlg]]] with |transformedData|, |proofConfig|, and |hashName| passed as a parameters.
  5. Let |proofBytes| be the result of running the algorithm in Section [[[#ProofSerializationAlg]]] with |hashData|, |options|, and |sigFunc| passed as parameters.
  6. Let |proof|.|proofValue| be a base64-url-encoded Multibase encoding of the |proofBytes| if |proofEncoding| is `base64` otherwise let |proof|.|proofValue| be a base58-btc-encoded Multibase value of the |proofBytes| if |proofEncoding| is `base58`.
  7. Return |proof| as the [=data integrity proof=].

Verify Proof

The following algorithm specifies how to verify a [=data integrity proof=] given an secured data document. Required inputs are an secured data document ([=map=] |securedDocument|). This algorithm returns a [=verification result=], which is a [=struct=] whose [=struct/items=] are:

[=verification result/verified=]
`true` or `false`
[=verification result/verifiedDocument=]
Null, if [=verification result/verified=] is `false`; otherwise, an [=unsecured data document=]
  1. Let |unsecuredDocument| be a copy of |securedDocument| with the `proof` value removed.
  2. Let |proofOptions| be a copy of |securedDocument|.|proof| with `proofValue` removed.
  3. Set |cryptosuiteName| to |securedDocument|.|proof|.|cryptosuite|, which must be one of specified cryptosuites. From the cryptosuite specification, set the values of |canonScheme|, |hashName|, |verifyFunc|, and |proofEncode|.
  4. Let |proofBytes| be the Multibase decoded base64-url value in |securedDocument|.|proof|.|proofValue| if |proofEncode| is `base64` otherwise let |proofBytes| be the Multibase decoded base58-btc value.
  5. Let |transformedData| be the result of running the algorithm in Section [[[#TransformationAlg]]] with |unsecuredDocument|, and |cypherSuiteName|, |canonScheme|, and |hashName| |proofOptions| passed as parameters.
  6. Let |proofConfig| be the result of running the algorithm in Section [[[#ProofConfigurationAlg]]] with |options|, |cypherSuiteName|, |canonScheme|, and |hashName| passed as parameters.
  7. Let |hashData| be the result of running the algorithm in Section [[[#HashingAlg]]] with |transformedData|, |proofConfig|, and |hashName| passed as a parameters.
  8. Let |verified:boolean| be the result of running the algorithm in Section [[[#ProofVerificationAlg]]] with |hashData|, |proofBytes|, |proofConfig|, and |verifyFunc| as parameters.
  9. Return a [=verification result=] with [=struct/items=]:
    [=verified=]
    |verified|
    [=verifiedDocument=]
    |unsecuredDocument| if |verified| is `true`, otherwise Null

Common Functions

Hashing

The following algorithm specifies how to cryptographically hash a transformed data document and proof configuration into cryptographic hash data that is ready to be provided as input to the algorithms for proof serialization and proof verification of each of the respective cryptosuites.

The required inputs to this algorithm are a transformed data document (|transformedDocument|), canonical proof configuration (|canonicalProofConfig|), and hash name (|hashName|). A single hash data value represented as a series of bytes is produced as output.

  1. If |hashName| is `SHA-256`, let |transformedDocumentHash| be the result of applying the SHA-256 (SHA-2 with 256-bit output) cryptographic hashing algorithm [[RFC6234]] to the respective |transformedDocument|. Respective |transformedDocumentHash| will be exactly 32 bytes in size.
  2. If |hashName| is `SHA-384`, let |transformedDocumentHash| be the result of applying the SHA-384 (SHA-2 with 384-bit output) cryptographic hashing algorithm [[RFC6234]] to the respective |transformedDocument|. Respective |transformedDocumentHash| will be exactly 48 bytes in size.
  3. If |hashName| is `SHA-512`, let |transformedDocumentHash| be the result of applying the SHA-512 (SHA-2 with 512-bit output) cryptographic hashing algorithm [[RFC6234]] to the respective |transformedDocument|. Respective |transformedDocumentHash| will be exactly 64 bytes in size.
  4. If |hashName| is `SHA-256`, let |proofConfigHash| be the result of applying the SHA-256 (SHA-2 with 256-bit output) cryptographic hashing algorithm [[RFC6234]] to the |canonicalProofConfig|. Respective |proofConfigHash| will be exactly 32 bytes in size.
  5. If |hashName| is `SHA-384`, let |proofConfigHash| be the result of applying the SHA-384 (SHA-2 with 384-bit output) cryptographic hashing algorithm [[RFC6234]] to the |canonicalProofConfig|. Respective |proofConfigHash| will be exactly 48 bytes in size.
  6. If |hashName| is `SHA-512`, let |proofConfigHash| be the result of applying the SHA-512 (SHA-2 with 512-bit output) cryptographic hashing algorithm [[RFC6234]] to the |canonicalProofConfig|. Respective |proofConfigHash| will be exactly 64 bytes in size.
  7. Let |hashData| be the result of joining |proofConfigHash| (the first hash) with |transformedDocumentHash| (the second hash).
  8. Return |hashData| as the hash data.
Proof Configuration

The following algorithm specifies how to generate a proof configuration from a set of proof options that is used as input to the proof hashing algorithm.

The required inputs to this algorithm are proof options (|options|), a cryptosuite identifier (|cryptosuite|), the canonicalization scheme (|canonScheme|) and hash name (|hashName|). The proof options MUST contain a type identifier for the cryptographic suite (|type|) and MUST contain the cryptosuite identifier (|cryptosuite|). A proof configuration object is produced as output.

  1. Let |proofConfig| be a clone of the |options| object.
  2. If |proofConfig|.|type| is not set to `DataIntegrityProof`, |proofConfig|.|cryptosuite| is not set to the cryptosuite, or both, an `INVALID_PROOF_CONFIGURATION` error MUST be raised.
  3. If |proofConfig|.|created| is set to a value that is not a valid [[XMLSCHEMA11-2]] datetime, an `INVALID_PROOF_DATETIME` error MUST be raised.
  4. Set |proofConfig|.|@context| to |unsecuredDocument|.|@context|.
  5. If |canonScheme| is `rdfc`, let |canonicalProofConfig| be the result of applying the Universal RDF Dataset Canonicalization Algorithm [[RDF-CANON]] to the |proofConfig|, with hashing parameter set to |hashName|.
  6. If |canonScheme| is `jcs`, let |canonicalProofConfig| be the result of applying the JSON Canonicalization Scheme [[RFC8785]] to the |proofConfig|.
  7. Return |canonicalProofConfig|.
Transformation

The following algorithm specifies how to transform an unsecured input document into a transformed document that is ready to be provided as input to the hashing algorithm in Section [[[#HashingAlg]]].

Required inputs to this algorithm are an unsecured data document (|unsecuredDocument|), transformation options (|options|), a cryptosuite identifier (|cryptosuite|), the canonicalization scheme (|canonScheme|) and hash name (|hashName|). The transformation options MUST contain a type identifier for the cryptographic suite (|type|) and a cryptosuite identifier (|cryptosuite|). A transformed data document is produced as output. Whenever this algorithm encodes strings, it MUST use UTF-8 encoding.

  1. If |options|.|type| is not set to the string `DataIntegrityProof`, |options|.|cryptosuite| is not set to the cryptosuite value, or both, then a `PROOF_TRANSFORMATION_ERROR` MUST be raised.
  2. If |canonScheme| is `rdfc`, let |canonicalDocument| be the result of applying the Universal RDF Dataset Canonicalization Algorithm [[RDF-CANON]] to the |unsecuredDocument| with hashing parameter set to |hashName|.
  3. If |canonScheme| is `jcs`, let |canonicalDocument| be the result of applying the JSON Canonicalization Scheme [[RFC8785]] to the |unsecuredDocument|.
  4. Set |output| to the value of |canonicalDocument|.
  5. Return |canonicalDocument| as the transformed data document.

Proof Serialization

The following algorithm specifies how to serialize a digital signature from a set of cryptographic hash data. This algorithm is designed to be used in conjunction with the algorithms defined in Section 4: Algorithms of the Data Integrity specification [[VC-DATA-INTEGRITY]]. Required inputs are cryptographic hash data (|hashData|), proof options (|options|), and a signature function (|sigFunc|). The proof options MUST contain a type identifier for the cryptographic suite (|type|) and MAY contain a cryptosuite identifier (|cryptosuite|). A single digital proof value represented as series of bytes is produced as output.

  1. Let |privateKeyBytes| be the result of retrieving the private key bytes (or a signing interface enabling the use of the private key bytes) associated with the verification method identified by the |options|.|verificationMethod| value.
  2. Let |proofBytes| be the result of applying the |sigFunc|, with |hashData| as the data to be signed using the private key specified by |privateKeyBytes|. |proofBytes| will have a length as indicated by the cryptosuite.
  3. Return |proofBytes| as the digital proof.

Proof Verification

The following algorithm specifies how to verify a digital signature from a set of cryptographic hash data. This algorithm is designed to be used in conjunction with the algorithms defined in Section 4: Algorithms of the Data Integrity [[VC-DATA-INTEGRITY]] specification. Required inputs are cryptographic hash data (|hashData|), a digital signature (|proofBytes|), proof options (|options|), and a verification function (|verifyFunc|). A verification result represented as a boolean value is produced as output.

  1. Let |publicKeyBytes| be the result of retrieving the public key bytes associated with the |options|.|verificationMethod| value as described in Section 4: Retrieve Verification Method of the Data Integrity specification [[VC-DATA-INTEGRITY]].
  2. Let |verificationResult| be the result of applying the |verifyFunc|, using the public key specified by |publicKeyBytes|, with |hashData| as the data to be verified against the |proofBytes|.
  3. Return |verificationResult| as the verification result.

Selective Disclosure Algorithms

Introduction

There are three high-level algorithms used with credentials that support cryptographic selective disclosure. First, the issuer uses the Add Base Proof algorithm to create a signed credential that supports selective disclosure. The holder then uses the Add Derived Proof algorithm to create a selectively disclosed version of the issuer-signed credential along with a derived proof. Finally, the verifier uses the Verify Derived Proof algorithm to verify the received selectively disclosed credential with derived proof against the issuer's verification information.

This specification allows for three different implementation approaches to selective disclosure that work primarily at the level of the [=claims=] within a verifiable credential; that is, they allow the holder to selectively disclose, or not, each particular claim in the credential. The three approaches take into account the efficiency of the fundamental cryptographic signature algorithm used. Only one (to be) standardized cryptographic signature algorithm, BBS, natively supports selective disclosure, i.e., it produces a base signature over an ordered list of claims and facilitates the creation of a "derived signature" over a subset of that list of claims. In other cryptographic signature algorithms, two approaches are typically used: (1) provide individual signatures for each claim, or (2) sign a list of "salted hashes" for all claims. We will refer to the first as SIC (Sign Individual Claims), and the second as SHoC (Salted Hashes of Claims). Cases where the signature algorithm naturally supports multiple claims, will be referred to as MCS (Multiple Claim Support).

All approaches use similar mechanisms to protect the proof-related metadata, and to minimize data leakage of non-disclosed information from the derived proof. They all incur roughly similar overhead in the base and derived proofs for these purposes. Similarly, all approaches use the same encoding of the individual data items that go into a proof, CBOR, and the final encoding of raw octets into a string, `base64-url`. Hence, this information is not shown in the summary [[[#SDApproachTable]]].

The [=SIC=] approaches sign individual claims after the transformation step. Hence, their base proof sizes scale as the number of claims times the size of the signature produced by the chosen signature algorithm. Thus this approach is good for signature algorithms that produce small signatures. Along similar lines, the size of the derived proof scales as the number of revealed claims times the signature size, i.e., the fewer claims revealed, the smaller the proof. This is a nice property, not shared by the other two approaches.

When following the [=SHoC=] approach, a salt value (random nonce) is created for each claim. That salt value is concatenated with the claim, and a cryptographic collision-resistant hash function is applied to the list of paired salts+claims, to produce a list of hashes, one for each claim. The newly paired lists of salts and hashes are combined and signed with a selected signature algorithm. Only one signature is needed; thus, this approach is good for signature algorithms with larger signature sizes. A downside of this approach is that the complete list of salts and hashes must be conveyed to both holder and verifier. To verify a revealed claim, the verifier needs first to verify the signature for the combined list of salts and hashes; then they need to be told the index of the claim in that list; and then, using the appropriate salt value from the list, they verify the value of the hash of the salt concatenated with the received claim and the hash from the table.

In one MCS signature algorithm, BBS, the produced base proof is extremely short, 80 bytes, while the derived proof grows by 32 bytes per non-revealed claim.

We summarize the properties of these approaches in [[[#SDApproachTable]]].

Selective Disclosure Approaches.
Name Base Proof Contents Base Proof Size Derived Proof Contents Derived Proof Size
Signed Individual Claims (SIC) list of signatures, one for each claim. Nclaims × (signature size) sub-list of signatures, one for each revealed claim. Nrevealed claims × (signature size)
Signed Hash of Claims (SHoC) list of salts, list of salted hashes, one for each claim, plus signature over these two lists. Nclaims × (salt size + hash size) + signature size list of salts, list of salted hashes, one for each claim, plus one signature over these two lists. Nclaims × (salt size + hash size) + signature size
Multi Claim Signature, BBS BBS signature 80 bytes BBS proof 272 + 32 × (Nclaims - Nrevealed claims) bytes

In the following sections, we define the three main selective disclosure algorithms in terms of sub-algorithms. These sub-algorithms are of two types: those that are independent of the selective disclosure implementation approaches discussed above, and those that have approach-specific implementations.

Create Base Proof

The following algorithm specifies how to create a [=data integrity proof=] given an unsecured data document. Required inputs are an unsecured data document ([=map=] |unsecuredDocument|), and a set of proof options ([=map=] |options|). A [=data integrity proof=] ([=map=]), or an error, is produced as output.

  1. Let |proof| be a clone of the proof options, |options|.
  2. Let |proofConfig| be the result of running the algorithm in Section [[[#ProofConfigurationAlg]]] with |options| passed as a parameter.
  3. Let |transformedData| be the result of running the algorithm in [[[#TransformSD]]] with |unsecuredDocument|, |proofConfig|, and |options| passed as parameters.
  4. Let |hashData| be the result of running the algorithm in [[[#HashSD]]] with |transformedData| and |proofConfig| passed as a parameters.
  5. In a SHoC approach, add to |hashData| the |saltedHash| data that results from running the algorithm in [[[#SaltedHashSD]]].
  6. Let |proofBytes| be the result of running the approach-specific SerializeBase algorithm. For example, use [[[#SerializeBase-SIC]]] for the SIC approach, and use [[[#SerializeBase-SHoC]]] for the SHoC approach. In all cases, pass |hashData| and |options| as parameters.
  7. Let |proof|.|proofValue| be a `base64-url-encoded` Multibase value of the |proofBytes|.
  8. Return |proof| as the [=data integrity proof=].

Add Derived Proof

The following algorithm creates a selective disclosure derived proof. It is called by a holder of a [=verifiable credential=] that is protected by a selective disclosure 'base proof'. The derived proof is to be given to a [=verifier=]. The algorithm inputs include a JSON-LD document (|document|); a selective disclosure base proof (|proof|); an array of JSON pointers to use to selectively disclose statements (|selectivePointers|); and any custom JSON-LD API options, such as a document loader. A single selectively revealed document value, represented as an object, is produced as output.

  1. Let the |proofData| object be the result returned from the approach-specific ParseBase sub-algorithm. For example, for the SIC approach use [[[#ParseBase-SIC]]], and for the SHoC approach use [[[#ParseBase-SHoC]]]. At a minimum, the |proofData| object will contain the fields |hmacKey| and |mandatoryPointers|.
  2. Initialize |disclosureData| to the object returned when calling the algorithm in [[[#CreateDisclosureData]]], passing the |document|, |proofData|, |selectivePointers|, and any custom JSON-LD API options, such as a document loader. The |disclosureData| object will contain the following fields: |revealDocument|, |mandatoryIndexes|, |selectiveIndexes|, |verifierLabelMap| |mandatory|, and |nonMandatory|.
  3. Initialize |newProof| to a shallow copy of |proof|.
  4. Replace |proofValue| in |newProof| with the result of calling the approach-specific SerializeDerived algorithm. For example, for the SIC approach, use [[[#SerializeDerived-SIC]]], and for the SHoC approach, use [[[#SerializeDerived-SHoC]]]. In all cases, pass |proofData| and |disclosureData| as parameters.
  5. Set the value of the "proof" property in |revealDocument| to |newProof|.
  6. Return |revealDocument| as the selectively revealed document.

Verify Derived Proof

The following algorithm attempts verification of a selective disclosure derived proof. This algorithm is called by a verifier of a selective disclosure protected [=verifiable credential=]. Algorithm inputs include a JSON-LD document (|document|); a selective disclosure derived proof (|proof|); and any custom JSON-LD API options, such as a document loader. This algorithm returns a [=verification result=]:

  1. Let |unsecuredDocument| be a copy of |document| with the `proof` value removed.
  2. Initialize the |proofData| object to the result returned by the approach-specific ParseDerived sub-algorithm. For example, for the SIC approach, use [[[#ParseDerived-SIC]]], and for the SHoC approach, use [[[#ParseDerived-SHoC]]]. At a minimum, the |proofData| object will contain the fields |labelMap| and |mandatoryIndexes|.
  3. Initialize the |verifyData| object to the value returned by calling the algorithm in Section [[[#CreateVerifyData]]], passing the |document|, |proofData|, and any custom JSON-LD API options, such as a document loader. The |verifyData| object will contain the following fields: |proofHash|, |mandatoryHash|, and |nonMandatory|.
  4. Initialize |verified| to the result of calling the approach-specific VerifyDerived algorithm. For example, for the SIC approach, use [[[#VerifyDerived-SIC]]], and for the SHoC approach, use [[[#VerifyDerived-SHoC]]]. In all cases, pass |proofData| and |verifyData| as parameters.
  5. Return a [=verification result=] with [=struct/items=]:
    [=verified=]
    The value of |verified|
    [=verifiedDocument=]
    if |verified| is `true`, |unsecuredDocument|; otherwise Null

Selective Disclosure Common Sub-Algorithms

TransformSD

The following algorithm specifies how to transform an unsecured input document into a transformed document that is ready to be provided as input to the hashing algorithm in Section [[[#HashSD]]] and, depending on approach, may be provided as input to the salted hashing algorithm, [[[#SaltedHashSD]]].

Required inputs to this algorithm are an unsecured data document (|unsecuredDocument|) and transformation options (|options|). The transformation options MUST contain a type identifier for the cryptographic suite (|type|), a cryptosuite identifier (|cryptosuite|), and a verification method (|verificationMethod|). The transformation options MUST contain an array of mandatory JSON pointers (|mandatoryPointers|) and MAY contain additional options, such as a JSON-LD document loader. A transformed data document is produced as output. Whenever this algorithm encodes strings, it MUST use UTF-8 encoding.

  1. Initialize |hmac| to an HMAC API using a locally generated and exportable HMAC key. The HMAC uses the same hash algorithm used in the signature algorithm, which is detected via the |verificationMethod| provided to the function, i.e., SHA-256 for a P-256 curve. Per the recommendations of [[RFC2104]], the HMAC key MUST be the same length as the digest size; for SHA-256, this is 256 bits or 32 bytes.
  2. If |cryptosuite| is set to `ecdsa-sd-2023` then initialize |labelMapFactoryFunction| to the result of calling the algorithm of Section [[[#createhmacidlabelmapfunction]]], passing |hmac|; otherwise initialize |labelMapFactoryFunction| to the result of calling the algorithm of Section [[[#createshuffledidlabelmapfunction]]], passing |hmac|;
  3. Initialize |groupDefinitions| to a map with an entry with a key of the string "mandatory" and a value of |mandatoryPointers|.
  4. Initialize |groups| to the result of calling the algorithm in Section [[[#canonicalizeandgroup]]], passing |labelMapFactoryFunction|, |groupDefinitions|, |unsecuredDocument| as |document|, and any custom JSON-LD API options. Note: This step transforms the document into an array of canonical N-Quads with pseudorandom blank node identifiers based on |hmac|, and groups the N-Quad strings according to selections based on JSON pointers.
  5. Initialize |mandatory| to the values in the |groups|.|mandatory|.|matching| map.
  6. Initialize |nonMandatory| to the values in the |groups|.|mandatory|.|nonMatching| map.
  7. Initialize |hmacKey| to the result of exporting the HMAC key from |hmac|.
  8. Return an object with `mandatoryPointers` set to |mandatoryPointers|, `mandatory` set to |mandatory|, `nonMandatory` set to |nonMandatory|, and `hmacKey` set to |hmacKey|.

HashSD

The following algorithm specifies how to cryptographically hash a transformed data document and proof configuration into cryptographic hash data that is ready to be provided as input to the approach specific algorithms such as [[[#SerializeBase-SIC]]] and [[[#SerializeBase-SHoC]]].

The required inputs to this algorithm are a transformed data document (|transformedDocument|) and canonical proof configuration (|canonicalProofConfig|). A hash data value represented as an object is produced as output.

  1. Initialize |proofHash| to the result of calling the RDF Dataset Canonicalization algorithm [[RDF-CANON]] on |canonicalProofConfig| and then cryptographically hashing the result using the same hash that is used by the signature algorithm, i.e., SHA-256 for a P-256 curve. Note: This step can be performed in parallel; it only needs to be completed before this algorithm terminates as the result is part of the return value.
  2. Initialize |mandatoryHash| to the result of calling the the algorithm in Section [[[#hashmandatorynquads]]], passing |transformedDocument|.|mandatory|.
  3. Initialize |hashData| as a deep copy of |transformedDocument| and add |proofHash| as `proofHash` and |mandatoryHash| as `mandatoryHash` to that object.
  4. Return |hashData| as hash data.

CreateDisclosureData

The following algorithm creates data to be used to generate a derived proof. The inputs include a JSON-LD document (|document|), parsed base proof data (|proofData|), an array of JSON pointers to use to selectively disclose statements (|selectivePointers|), and any custom JSON-LD API options, such as a document loader). A single object, disclosure data, is produced as output, which contains the "revealDocument", "mandatoryIndexes", "selectiveIndexes", "verifierLabelMap", "mandatory", and "nonMandatory" fields.

  1. Initialize |hmac| to an HMAC API using |proofData|.|hmacKey|. The HMAC uses the same hash algorithm used in the signature algorithm, i.e., SHA-256 for a P-256 curve.
  2. If |cryptosuite| is set to `ecdsa-sd-2023` then initialize |labelMapFactoryFunction| to the result of calling the algorithm of Section [[[#createhmacidlabelmapfunction]]], passing |hmac|; otherwise initialize |labelMapFactoryFunction| to the result of calling the algorithm of Section [[[#createshuffledidlabelmapfunction]]], passing |hmac|.
  3. Initialize |combinedPointers| to the concatenation of |mandatoryPointers| and |selectivePointers|.
  4. Initialize |groupDefinitions| to a map with the following entries: key of the string `"mandatory"` and value of |proofData|.|mandatoryPointers|, key of the string `"selective"` and value of |selectivePointers|, and key of the string `"combined"` and value of |combinedPointers|.
  5. Initialize |groups| and |labelMap| to their associated values in the result of calling the algorithm in Section [[[#canonicalizeandgroup]]], passing |document|, |labelMapFactoryFunction|, |groupDefinitions|, and any custom JSON-LD API options as parameters. Note: This step transforms the document into an array of canonical N-Quad strings with pseudorandom blank node identifiers based on |hmac|, and groups the N-Quad strings according to selections based on JSON pointers.
  6. Compute the mandatory indexes. Initialize |relativeIndex| to zero.
  7. Initialize |mandatoryIndexes| to an empty array.
  8. For each |absoluteIndex| in the keys in |groups|.|combined|.|matching|, convert the absolute index of any mandatory N-Quad to an index relative to the combined output that is to be revealed:
    1. If |groups|.|mandatory|.|matching| has |absoluteIndex| as a key, then append |relativeIndex| to |mandatoryIndexes|.
    2. Increment |relativeIndex|.
  9. Compute the selective indexes. Initialize |relativeIndex| to zero.
  10. Initialize |selectiveIndexes| to an empty array.
  11. For each |absoluteIndex| in the keys in |groups|.|combined|.|matching|, convert the absolute index of any selective N-Quad to an index relative to the combined output that is to be revealed:
    1. If |groups|.|selective|.|matching| has |absoluteIndex| as a key, then append |relativeIndex| to |selectiveIndexes|.
    2. Increment |relativeIndex|.
  12. Initialize |revealDocument| to the result of the calling the algorithm in Section [[[#selectjsonld]]], passing |document|, and |combinedPointers| as |pointers|.
  13. Run the RDF Dataset Canonicalization Algorithm [[RDF-CANON]] on the joined |combinedGroup.deskolemizedNQuads|, passing any custom options, and get the canonical bnode identifier map, |canonicalIdMap|. Note: This map includes the canonical blank node identifiers that a verifier will produce when they canonicalize the reveal document.
  14. Initialize |verifierLabelMap| to an empty map. This map will map the canonical blank node identifiers the verifier will produce when they canonicalize the revealed document to the blank node identifiers that were originally signed in the base proof.
  15. For each key (|inputLabel|) and value (|verifierLabel|) in |canonicalIdMap|:
    1. Add an entry to |verifierLabelMap| using |verifierLabel| as the key and the value associated with |inputLabel| as a key in |labelMap| as the value.
  16. Return an object with properties matching |revealDocument|, |mandatoryIndexes|, |selectiveIndexes|, |verifierLabelMap|, |groups|.|mandatory|.|matching| for |mandatory|, and |groups|.|mandatory|.|nonMatching| for |nonMandatory|.

CreateVerifyData

The following algorithm creates the additional data needed to perform verification of a selective disclosed [=verifiable credential=] protected with a derived proof. The inputs include a JSON-LD document (|document|), parsed derived proof data (|proofData|), and any custom JSON-LD API options, such as a document loader. A single verify data object value is produced as output containing the following fields: "proofHash", "mandatoryHash", and "nonMandatory".

  1. Let |proofHash| be the result of cryptographically hashing the |canonicalProofConfig| value returned by the algorithm of section [[[#ProofConfigurationAlg]]]. Using the same hash that is used by the signature algorithm, i.e., SHA-256 for a P-256 curve.
  2. Initialize |labelMapFactoryFunction| to the result of calling the algorithm of Section [[[#createlabelmapfunction]]] with the parameter |proofData|.|verifierLabelMap|.
  3. Initialize |nquads| to the result of calling the algorithm of section [[[#labelreplacementcanonicalizejsonld]]], passing |document|, |labelMapFactoryFunction|, and any custom JSON-LD API options. Note: This step transforms the document into an array of canonical N-Quads with pseudorandom blank node identifiers based on |labelMap|.
  4. Initialize |mandatory| to an empty array.
  5. Initialize |nonMandatory| to an empty array.
  6. For each entry (|index|, |nq|) in |nquads|, separate the N-Quads into mandatory and non-mandatory categories:
    1. If |mandatoryIndexes| includes |index|, add |nq| to |mandatory|.
    2. Otherwise, add |nq| to |nonMandatory|.
  7. Initialize |mandatoryHash| to the result of calling the function [[[#hashmandatorynquads]]], passing |mandatory|.
  8. Return an object with properties matching |proofHash|, |nonMandatory|, and |mandatoryHash|.

Signed Individual Claims (SIC) Specific Sub-Algorithms

SerializeBase-SIC

The following algorithm specifies how to create a serialized base proof in the signed individual claims (SIC) approach. Required inputs are cryptographic hash data (|hashData|) and proof options (|options|). The proof options MUST contain a type identifier for the cryptographic suite (|type|) and MAY contain a cryptosuite identifier (|cryptosuite|). A single digital proof value represented as series of bytes is produced as output.

  1. Initialize |proofHash|, |mandatoryPointers|, |mandatoryHash|, |nonMandatory|, and |hmacKey| to the values associated with their property names in |hashData|.
  2. Initialize |proofScopedKeyPair| to a locally generated key pair corresponding to the signature algorithm use by the |crytosuite|. Note: This key pair is scoped to the specific proof; it is not used for anything else and the private key will be destroyed when this algorithm terminates.
  3. Initialize |signatures| to an array where each element holds the result of digitally signing the UTF-8 representation of each N-Quad string in |nonMandatory|, in order. The digital signature algorithm is the same as that for the |cryptosuite| and uses the private key from |proofScopedKeyPair|. Note: This step generates individual signatures for each statement (claim) that can be selectively disclosed using a local, proof-scoped key pair that binds them together; this key pair will be bound to the proof by a signature over its public key using the private key associated with the base proof verification method.
  4. Initialize |publicKeyProofScoped| to the multikey expression of the public key exported from |proofScopedKeyPair|.
  5. Initialize |toSign| to the concatenation of |proofHash|, |publicKeyProofScoped|, and |mandatoryHash|, in that order.
  6. Initialize |baseSignature| to the result of digitally signing |toSign| using the private key associated with the base proof verification method.
  7. If |cryptosuite| is set to `ecdsa-sd-2023` then initialize a byte array, |proofBytes|, so that starts with the legacay ECDSA-SD base proof header bytes 0xd9, 0x5d, and 0x00. Otherwise initialize |proofValue| with the general SIC header bytes TBD.
  8. Initialize |components| to an array with five elements containing the values of: |baseSignature|, |publicKeyProofScoped|, |hmacKey|, |signatures|, and |mandatoryPointers|.
  9. CBOR-encode |components| per [[RFC8949]] where CBOR tagging MUST NOT be used on any of the |components|. Append the produced encoded value to |proofBytes|.
  10. Return |proofBytes|.

ParseBase-SIC

The following algorithm parses the components of a legacy `ecdsa-sd-2023` selective disclosure base proof value or a general signed individual claim (SIC) approach selective disclosure base proof. The required inputs are a proof value (|proofValue|). A single object parsed base proof, containing five elements, using the names `baseSignature`, `publicKeyProofScoped`, `hmacKey`, `signatures`, and `mandatoryPointers`, is produced as output.

  1. If the |proofValue| string does not start with `u`, indicating that it is a multibase-base64url-no-pad-encoded value, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  2. Initialize |decodedProofValue| to the result of base64url-no-pad-decoding the substring after the leading `u` in |proofValue|.
  3. If the |decodedProofValue| does not start with the ECDSA-SD base proof header bytes `0xd9`, `0x5d`, and `0x00`, or the general SIC header bytes TBD, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  4. Initialize |components| to an array that is the result of CBOR-decoding the bytes that follow the three-byte base proof header. Confirm that the result is an array of five elements.
  5. Return an object with properties set to the five elements, using the names `baseSignature`, `publicKeyProofScoped`, `hmacKey`, `signatures`, and `mandatoryPointers`, respectively.

SerializeDerived-SIC

The following algorithm serializes a derived proof value. The required input is a |dislosureData| object containing the following fields: base signature (|baseSignature|), public key (|publicKey|), an array of signatures (|signatures|), a label map (|labelMap|), and an array of mandatory indexes (|mandatoryIndexes|). A single derived proof value, serialized as a byte string, is produced as output.

  1. Initialize |compressedLabelMap| to the result of calling the algorithm in Section [[[#compresslabelmap]]], passing |labelMap| as the parameter.
  2. If |cryptosuite| is set to `ecdsa-sd-2023` then initialize a byte array, |proofValue|, that starts with the ECDSA-SD disclosure proof header bytes `0xd9`, `0x5d`, and `0x01`. Otherwise initialize a byte array, |proofValue|, that starts with the general SIC derived proof header bytes TBD.
  3. Initialize |components| to an array with five elements containing the values of: |baseSignature|, |publicKeyProofScoped|, |signatures|, |compressedLabelMap|, and |mandatoryIndexes|.
  4. CBOR-encode |components| per [[RFC8949]] where CBOR tagging MUST NOT be used on any of the |components|. Append the produced encoded value to |proofValue|.
  5. Return the derived proof as a string with the base64url-no-pad-encoding of |proofValue| as described in the Multibase section of [[[CID]]]. That is, return a string starting with "`u`" and ending with the base64url-no-pad-encoded value of |proofValue|.

ParseDerived-SIC

The following algorithm parses the components of the derived proof value. The required input is a derived proof value (|proofValue|). A single derived proof value value object is produced as output, which contains a set to five elements, using the names "baseSignature", "publicKeyProofScoped", "signatures", "labelMap", and "mandatoryIndexes".

  1. If the |proofValue| string does not start with `u`, indicating that it is a multibase-base64url-no-pad-encoded value, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  2. Initialize |decodedProofValue| to the result of base64url-no-pad-decoding the substring after the leading `u` in |proofValue|.
  3. If the |decodedProofValue| does not start with the ECDSA-SD disclosure proof header bytes `0xd9`, `0x5d`, and `0x01`, or the general SIC approach derived proof header bytes TBD, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  4. Initialize |components| to an array that is the result of CBOR-decoding the bytes that follow the three-byte derived proof header. If the result is not an array of five elements an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  5. Replace the fourth element in |components| using the result of calling the algorithm in Section [[[#decompresslabelmap]]], passing the existing fourth element of |components| as |compressedLabelMap| and |cryptosuite|.
  6. Return derived proof value as an object with properties set to the five elements, using the names "baseSignature", "publicKey", "signatures", "labelMap", and "mandatoryIndexes", respectively.

VerifyDerived-SIC

The following algorithm attempts verification of an `ecdsa-sd-2023` derived proof or a general SIC approach derived proof. This algorithm is called by a verifier of an ECDSA-SD-protected or SIC approach protected [=verifiable credential=]. The inputs include a JSON-LD document (|document|), derived proof data (|proofData|), verification data (|verifyData|), and any custom JSON-LD API options, such as a document loader. This algorithm returns a [=verification result=]:

  1. Let |unsecuredDocument| be a copy of |document| with the `proof` value removed.
  2. Let |publicKeyBytes| be the result of retrieving the public key bytes associated with the |options|.|verificationMethod| value as described in the [[[CID]]] specification, Section: Retrieve Verification Method.
  3. Initialize |baseSignature|, |publicKeyProofScoped|, and |signatures| to the values associated with their property names in the |proofData| object and |proofHash|, |mandatoryHash|, and |nonMandatory| to the values associated with their property names in the |verifyData| object.
  4. If the length of |signatures| does not match the length of |nonMandatory|, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR, indicating that the signature count does not match the non-mandatory message count.
  5. Initialize |toVerify| to the concatenation of |proofHash|, |publicKeyProofScoped|, and |mandatoryHash|, in that order.
  6. Initialize |verified| to true.
  7. Initialize |verificationCheck| be the result of applying the verification algorithm of the signature algorithm appropriate to the |cryptosuite|, with |toVerify| as the data to be verified against the |baseSignature| using the public key specified by |publicKeyBytes|. If |verificationCheck| is `false`, set |verified| to false.
  8. For every entry (|index|, |signature|) in |signatures|, verify every signature for every selectively disclosed (non-mandatory) statement:
    1. Initialize |verificationCheck| to the result of applying the verification algorithm for the signature algorithm of the |cryptosuite|, with the UTF-8 representation of the value at |index| of |nonMandatory| as the data to be verified against |signature| using the public key specified by |publicKeyProofScoped|.
    2. If |verificationCheck| is `false`, set |verified| to false.
  9. Return a [=verification result=] with [=struct/items=]:
    [=verified=]
    The value of |verified|
    [=verifiedDocument=]
    |unsecuredDocument| if |verified| is `true`, otherwise Null

Salted Hash of Claims (SHoC) Specific Sub-Algorithms

SaltedHashSD

The following algorithm specifies how to create salted hash data.

The required inputs to this algorithm are a transformed data document (|transformedDocument|) and |cryptosuite|. A salted hash data value represented as an object is produced as output.

  1. Initialize |salts| as an empty array. For each |nonMandatory| item in the |transformedDocument| create a cryptographically secure random number of byte length half the size of the hash length, e.g., for security category 1 or 2 using SHA-256 the length of each salt would be 16 bytes.
  2. Initialize |saltedHashes| as an empty array. For each |nonMandatory| item in the |transformedDocument| compute the appropriate hash (SHA-256 for security category 1 or 2 signature algorithms) over the concatenation of each |salt| and its corresponding |nonMandatory| item. Add this hash value to the |saltedHashes| array.
  3. Add |salts| as `salts` and |saltedHashes| as `saltedHashes` to the |saltedHashData| object.
  4. Return |saltedHashData|.

To do: provide background on cryptographic random number generation in the security considerations section. For example cite appropriate references such as NIST SP-800-90A,B,C documents and/or BSI AIS 20 and AIS 31 documents.

SerializeBase-SHoC

The following algorithm specifies how to serialize SHoC approach base proof; called by an issuer of an Quantum-Safe-SD-protected Verifiable Credential. The base proof is to be given only to the holder, who is responsible for generating a derived proof from it, exposing only selectively disclosed details in the proof to a verifier. This algorithm is designed to be used in conjunction with the algorithms defined in the Data Integrity [[VC-DATA-INTEGRITY]] specification, Section 4: Algorithms. Required inputs are cryptographic hash data (|hashData|) and proof options (|options|). The proof options MUST contain a type identifier for the cryptographic suite (|type|) and MAY contain a cryptosuite identifier (|cryptosuite|). A single digital proof value represented as series of bytes is produced as output.

  1. Initialize |proofHash|, |mandatoryPointers|, |mandatoryHash|, |salts|, |saltedHashes|, and |hmacKey| to the values associated with their property names |hashData|.
  2. Initialize |toSign| the hash of the concatenation of |proofHash|, |mandatoryHash|, |salts|, and |saltedHashes| where the order of the concatenation MUST be followed. The cryptographic hash algorithm is that specified for signature algorithm in the |cryptosuite|.
  3. Initialize |signature| to the result of digitally signing |toSign| using the private key associated with the base proof verification method.
  4. Initialize a byte array, |proofValue|, that starts with the SHoC approach base proof header bytes 0xd9, 0x5d, and 0x10.
  5. Initialize |components| to an array with five elements containing the values of: |signature|, |hmacKey|, |salts|, |saltedHashes|, and |mandatoryPointers|.
  6. CBOR-encode |components| per [[RFC8949]] where CBOR tagging MUST NOT be used on any of the |components|. Append the produced encoded value to |proofValue|.
  7. Return |proofValue| as proof bytes.

The SHoC approach base proof header bytes 0xd9, 0x5d, and 0x10 were chosen to not collide with any of the [[VC-DI-ECDSA]] or [[VC-DI-BBS]] values. They are currently tentative.

ParseBase-SHoC

The following algorithm parses the components of a SHoC approach selective disclosure base proof value. The required inputs are a proof value (|proofValue|). A single object proof data, containing five elements, using the names `signature`, `hmacKey`, `salts` `saltedHashes`, and `mandatoryPointers`, is produced as output.

  1. If the |proofValue| string does not start with `u`, indicating that it is a multibase-base64url-no-pad-encoded value, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  2. Initialize |decodedProofValue| to the result of base64url-no-pad-decoding the substring after the leading `u` in |proofValue|.
  3. If the |decodedProofValue| does not start with the SHoC approach base proof header bytes `0xd9`, `0x5d`, and `0x10`, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  4. Initialize |components| to an array that is the result of CBOR-decoding the bytes that follow the three-byte SHoC approach base proof header. Confirm that the result is an array of five elements.
  5. Return an object with properties set to the five elements, using the names `signature`, `hmacKey`, `salts`, `saltedHashes`, and `mandatoryPointers`, respectively.

SerializeDerived-SHoC

The following algorithm serializes a SHoC approach derived proof value. The required inputs are a base signature (|signature|), an array of salts (|salts|), an array of salted hashes (|saltedHashes|), a label map (|labelMap|), an array of mandatory indexes (|mandatoryIndexes|), and an array of selective indexes (|selectiveIndexes|). A single derived proof value, serialized as a UTF-8 string, is produced as output.

  1. Initialize |compressedLabelMap| to the result of calling the algorithm in [[[#compresslabelmap]]], passing |labelMap| as the parameter.
  2. Initialize a byte array, |proofValue|, that starts with the SHoC approach derived proof header bytes `0xd9`, `0x5d`, and `0x11`.
  3. Initialize |components| to an array with six elements containing the values of: |signature|, |salts|, |saltedHashes|, |compressedLabelMap|, |mandatoryIndexes|, |selectiveIndexes|.
  4. CBOR-encode |components| per [[RFC8949]] where CBOR tagging MUST NOT be used on any of the |components|. Append the produced encoded value to |proofValue|.
  5. Return the derived proof as a string with the base64url-no-pad-encoding of |proofValue| as described in the Multibase section of [[[CID]]]. That is, return a string starting with "`u`" and ending with the base64url-no-pad-encoded value of |proofValue|.

The SHoC approach derived proof header bytes `0xd9`, `0x5d`, and `0x11` were chosen to not conflict with values used by [[VC-DI-ECDSA]] and [[VC-DI-BBS]] and are tentative.

ParseDerived-SHoC

The following algorithm parses the components of a SHoC approach derived proof value. The required input is a derived proof value (|proofValue|). A single proof data object is produced as output, which contains a set to six elements, using the names "signature", "salts", "saltedHashes", "labelMap", "mandatoryIndexes", and "selectiveIndexes".

  1. If the |proofValue| string does not start with `u`, indicating that it is a multibase-base64url-no-pad-encoded value, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  2. Initialize |decodedProofValue| to the result of base64url-no-pad-decoding the substring after the leading `u` in |proofValue|.
  3. If the |decodedProofValue| does not start with the SHoC approach derived proof header bytes `0xd9`, `0x5d`, and `0x11`, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  4. Initialize |components| to an array that is the result of CBOR-decoding the bytes that follow the three-byte SHoC approach derived proof header. If the result is not an array of the following six elements — a byte array of length corresponding to the signature size; an array of byte arrays, each of length appropriate to the salt size; an array of byte arrays, each of length corresponding to the hash size; a map of integers to byte arrays, each of length 32; an array of integers; and another array of integers — an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.
  5. Replace the fourth element in |components| using the result of calling the algorithm in [[[#decompresslabelmap]]], passing the existing fourth element of |components| as |compressedLabelMap|.
  6. Return proof data as an object with properties set to the five elements, using the names "signature", "salts", "saltedHashes", "labelMap", "mandatoryIndexes", and "selectiveIndexes" respectively.

Should we also mention additional checks on the values of |salts|, |saltedHashes|, |mandatoryIndexes|, and |selectiveIndexes|? In particular, the length of the the |salts| and |saltedHashes| must be the same. The all elements of the |mandatoryIndexes| and |selectiveIndexes| should be in the range of 0 to the length of |salts|. There should not be repeated items in the |mandatoryIndexes| and |selectiveIndexesArrays|. Also the elements of each of these arrays should be increasing order. Note that the minimal additional checks appear in [[[#VerifyDerived-SHoC]]].

VerifyDerived-SHoC

The following algorithm attempts verification of an SHoC approach derived proof. This algorithm is called by a verifier of an SHoC approach protected [=verifiable credential=]. The inputs include a JSON-LD document (|document|), derived proof data (|proofData|), verification data (|verifyData|), and any custom JSON-LD API options, such as a document loader. This algorithm returns a [=verification result=]:

  1. Let |unsecuredDocument| be a copy of |document| with the `proof` value removed.
  2. Initialize |signature|, |salts|, |saltedHashes|, and |selectiveIndexes| to the values associated with their property names in the |proofData| parameter and |proofHash|, |nonMandatory|, and |mandatoryHash| to the values associated with their property names in the |verifyData| parameter.
  3. If the length of |salts| does not match the length of |saltedHashes|, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR, indicating that the salt count does not match the salted hashes count.
  4. The values of the elements of the |selectiveIndexes| array must be between 0 and the length of the |salts| array minus one. If not, an error MUST be raised and SHOULD convey an error type of PROOF_VERIFICATION_ERROR, indicating that the selective indexes are out of bounds.
  5. Initialize |toVerify| to the byte concatenation of the values: |proofHash|, |mandatoryHash|, |salts|, and |saltedHashes| in that order with the array values themselves concatenated together.
  6. Initialize |verified| to true.
  7. Initialize |verificationCheck| be the result of applying the verification algorithm of the appropriate signature algorithms as indicated by the "cryptosuite" property, with |toVerify| as the data to be verified against the |signature|. If |verificationCheck| is `false`, set |verified| to false.
  8. For every entry (|nq|, |index|) in |nonMandatory|, verify the salted hash for every selectively disclosed (non-mandatory) statement:
    1. Let |computedHash| be the hash of the concatenation of |salts|[|selectiveIndexes|[index]] and |nq|.
    2. Initialize |verificationCheck| to the result of comparing the |computedHash| with the value of |saltedHashes|[|selectedIndexes|[index]].
    3. If |verificationCheck| is `false`, set |verified| to false.
  9. Return a [=verification result=] with [=struct/items=]:
    [=verified=]
    The value of |verified|
    [=verifiedDocument=]
    |unsecuredDocument| if |verified| is `true`, otherwise Null

Selective Disclosure Functions

The following section contains a set of functions that are used throughout cryptographic suites that perform selective disclosure.

labelReplacementCanonicalizeNQuads

The following algorithm canonicalizes an array of N-Quad [[N-QUADS]] strings and replaces any blank node identifiers in the canonicalized result using a label map factory function, |labelMapFactoryFunction|. The required inputs are an array of N-Quad strings (|nquads|), and a label map factory function (|labelMapFactoryFunction|). Any custom options can also be passed. An N-Quads representation of the canonicalNQuads as an array of N-Quad strings, with the replaced blank node labels, and a map from the old blank node IDs to the new blank node IDs, labelMap, is produced as output.

  1. Run the RDF Dataset Canonicalization Algorithm [[RDF-CANON]] on the joined |nquads|, passing any custom options, and as output, get the canonicalized dataset, which includes a canonical bnode identifier map, |canonicalIdMap|.
  2. Pass |canonicalIdMap| to |labelMapFactoryFunction| to produce a new bnode identifier map, labelMap.
  3. Use the canonicalized dataset and labelMap to produce the canonical N-Quads representation as an array of N-Quad strings, canonicalNQuads.
  4. Return an object containing labelMap and canonicalNQuads.

labelReplacementCanonicalizeJsonLd

The following algorithm canonicalizes a JSON-LD document and replaces any blank node identifiers in the canonicalized result using a label map factory function, |labelMapFactoryFunction|. The required inputs are a JSON-LD document (|document|) and a label map factory function (|labelMapFactoryFunction|). Additional custom options (such as a document loader) can also be passed. An N-Quads representation of the canonicalNQuads as an array of N-Quad strings, with the replaced blank node labels, and a map from the old blank node IDs to the new blank node IDs, labelMap, is produced as output.

  1. Deserialize the JSON-LD document to RDF, |rdf|, using the Deserialize JSON-LD to RDF algorithm, passing any custom options (such as a document loader).
  2. Serialize |rdf| to an array of N-Quad strings, |nquads|.
  3. Return the result of calling the algorithm in Section [[[#labelreplacementcanonicalizenquads]]], passing |nquads|, |labelMapFactoryFunction|, and any custom options.

createLabelMapFunction

The following algorithm creates a label map factory function that uses an input label map to replace canonical blank node identifiers with another value. The required input is a label map, |labelMap|. A function, labelMapFactoryFunction, is produced as output.

  1. Create a function, |labelMapFactoryFunction|, with one required input (a canonical node identifier map, |canonicalIdMap|), that will return a blank node identifier map, bnodeIdMap, as output. Set the function's implementation to:
    1. Generate a new empty bnode identifier map, bnodeIdMap.
    2. For each map entry, entry, in |canonicalIdMap|:
      1. Use the canonical identifier from the value in entry as a key in |labelMap| to get the new label, newLabel.
      2. Add a new entry, |newEntry|, to bnodeIdMap using the key from entry and newLabel as the value.
    3. Return bnodeIdMap.
  2. Return |labelMapFactoryFunction|.

createShuffledIdLabelMapFunction

The following algorithm creates a label map factory function that uses an HMAC to shuffle canonical blank node identifiers. The required input is an HMAC (previously initialized with a secret key), |HMAC|. A function, labelMapFactoryFunction, is produced as output.

  1. Create a function, |labelMapFactoryFunction|, with one required input (a canonical node identifier map, |canonicalIdMap|), that will return a blank node identifier map, bnodeIdMap, as output. Set the function's implementation to:
    1. Generate a new empty bnode identifier map, bnodeIdMap.
    2. For each map entry, entry, in |canonicalIdMap|:
      1. Perform an HMAC operation on the canonical identifier from the value in entry to get an HMAC digest, digest.
      2. Generate a new string value, b64urlDigest, and initialize it to "u" followed by appending a base64url-no-pad encoded version of the digest value.
      3. Add a new entry, |newEntry|, to bnodeIdMap using the key from entry and b64urlDigest as the value.
    3. Derive the shuffled mapping from the `bnodeIdMap` as follows:
      1. Set `hmacIds` to be the sorted array of values from the `bnodeIdMap`, and set `bnodeKeys` to be the ordered array of keys from the `bnodeIdMap`.
      2. For each key in `bnodeKeys`, replace the `bnodeIdMap` value for that key with the index position of the value in the `hmacIds` array prefixed by "b", i.e., `bnodeIdMap.set(bkey, 'b' + hmacIds.indexOf(bnodeIdMap.get(bkey)))`.
    4. Return bnodeIdMap.
  2. Return |labelMapFactoryFunction|.

It should be noted that step 1.2 in the above algorithm is identical to step 1.2 in Section 3.3.4 `createHmacIdLabelMapFunction` of [[DI-ECDSA]], so developers might be able to reuse the code or call the function if implementing both.

createHmacIdLabelMapFunction

The following algorithm creates a label map factory function that uses an HMAC to replace canonical blank node identifiers with their encoded HMAC digests. The required input is an HMAC (previously initialized with a secret key), |HMAC|. A function, labelMapFactoryFunction, is produced as output.

  1. Create a function, |labelMapFactoryFunction|, with one required input (a canonical node identifier map, |canonicalIdMap|), that will return a blank node identifier map, bnodeIdMap, as output. Set the function's implementation to:
    1. Generate a new empty bnode identifier map, bnodeIdMap.
    2. For each map entry, entry, in |canonicalIdMap|:
      1. HMAC the canonical identifier from the value in entry to get an HMAC digest, digest.
      2. Generate a new string value, b64urlDigest, and initialize it to "u" followed by appending a base64url-no-pad encoded version of the digest value.
      3. Add a new entry, |newEntry|, to bnodeIdMap using the key from entry and b64urlDigest as the value.
    3. Return bnodeIdMap.
  2. Return |labelMapFactoryFunction|.

A different primitive could be created that returned a label map factory function that would instead sort the resulting HMAC digests and assign labels in the produced label map using a prefix and integers based on their sorted order. This primitive might be useful for selective disclosure schemes, such as BBS, that favor unlinkability over minimizing unrevealed data leakage.

compressLabelMap

The following algorithm compresses a label map. The required inputs are label map (|labelMap|) and the |cryptosuite|. The output is a compressed label map.

  1. Initialize |map| to an empty map.
  2. For each entry (|k|, |v|) in |labelMap|:
    1. if |cryptosuite| is set to `ecdsa-sd-2023` then add an entry to |map| with a key that is a base-10 integer parsed from the characters following the "c14n" prefix in |k| and a value that is a byte array resulting from base64url-no-pad-decoding the characters after the "u" prefix in |v|. Otherwise add an entry to `map`, with a key that is a base-10 integer parsed from the characters following the "c14n" prefix in |k|, and a value that is a base-10 integer parsed from the characters following the "b" prefix in `v`.
  3. Return |map| as compressed label map.

decompressLabelMap

The following algorithm decompresses a label map. The required inputs are a compressed label map (|compressedLabelMap|) and the |cryptosuite|. The output is a decompressed label map.

  1. Initialize |map| to an empty map.
  2. For each entry (|k|, |v|) in |compressedLabelMap|:
    1. If |cryptosuite| is set to `ecdsa-sd-2023` then add an entry to |map| with a key that adds the prefix "c14n" to |k| and a value that adds a prefix of "u" to the base64url-no-pad-encoded value for |v|; Otherwise add an entry to `map`, with a key that adds the prefix "c14n" to `k`, and a value that adds a prefix of "b" to `v`.
  3. Return |map| as decompressed label map.

skolemizeNQuads

The following algorithm replaces all blank node identifiers in an array of N-Quad strings with custom scheme URNs. The required inputs are an array of N-Quad strings (|inputNQuads|) and a URN scheme (|urnScheme|). An array of N-Quad strings, skolemizedNQuads, is produced as output. This operation is intended to be reversible through the use of the algorithm in Section [[[#deskolemizenquads]]].

  1. Create a new array of N-Quad strings, skolemizedNQuads.
  2. For each N-Quad string, s1, in |inputNQuads|:
    1. Create a new string, s2, that is a copy of s1 replacing any occurrence of a blank node identifier with a URN ("urn:"), plus the input custom scheme (|urnScheme|), plus a colon (":"), and the value of the blank node identifier. For example, a regular expression of a similar form to the following would achieve the desired result: s1.replace(/(_:([^\s]+))/g, '<urn:custom-scheme:$2>').
    2. Append s2 to skolemizedNQuads.
  3. Return skolemizedNQuads.

deskolemizeNQuads

The following algorithm replaces all custom scheme URNs in an array of N-Quad statements with a blank node identifier. The required inputs are an array of N-Quad strings (|inputNQuads|) and a URN scheme (|urnScheme|). An array of N-Quad strings, deskolemizedNquads, is produced as output. This operation is intended to reverse use of the algorithm in Section [[[#deskolemizenquads]]].

  1. Create a new array of N-Quad strings, deskolemizedNQuads.
  2. For each N-Quad string, s1, in |inputNQuads|:
    1. Create a new string, s2, that is a copy of s1 replacing any occurrence of a URN ("urn:"), plus the input custom scheme (|urnScheme|), plus a colon (":"), and the value of the blank node identifier with a blank node prefix ("_:"), plus the value of the blank node identifier. For example, a regular expression of a similar form to the following would achieve the desired result: s1.replace(/(<urn:custom-scheme:([^>]+)>)/g, '_:$2')..
    2. Append s2 to deskolemizedNQuads.
  3. Return deskolemizedNQuads.

skolemizeExpandedJsonLd

The following algorithm replaces all blank node identifiers in an expanded JSON-LD document with custom-scheme URNs, including assigning such URNs to blank nodes that are unlabeled. The required inputs are an expanded JSON-LD document (|expanded|), a custom URN scheme (|urnScheme|), a UUID string or other comparably random string (|randomString|), and reference to a shared integer (|count|). Any additional custom options (such as a document loader) can also be passed. It produces the expanded form of the skolemized JSON-LD document (|skolemizedExpandedDocument| as output. The skolemization used in this operation is intended to be reversible through the use of the algorithm in Section [[[#todeskolemizednquads]]].

  1. Initialize |skolemizedExpandedDocument| to an empty array.
  2. For each |element| in |expanded|:
    1. If either |element| is not an object or it contains the key @value, append a copy of |element| to |skolemizedExpandedDocument| and continue to the next |element|.
    2. Otherwise, initialize |skolemizedNode| to an object, and for each property and value in |element|:
      1. If value is an array, set the value of property in |skolemizedNode| to the result of calling this algorithm recursively passing value for |expanded| and keeping the other parameters the same.
      2. Otherwise, set the value of property in |skolemizedNode| to the first element in the array result of calling this algorithm recursively passing an array with value as its only element for |expanded| and keeping the other parameters the same.
    3. If |skolemizedNode| has no @id property, set the value of the @id property in |skolemizedNode| to the concatenation of "urn:", |urnScheme|, "_", |randomString|, "_" and the value of |count|, incrementing the value of |count| afterwards.
    4. Otherwise, if the value of the @id property in |skolemizedNode| starts with "_:", preserve the existing blank node identifier when skolemizing by setting the value of the @id property in |skolemizedNode| to the concatenation of "urn:", |urnScheme|, and the blank node identifier (i.e., the existing value of the @id property minus the "_:" prefix; e.g., if the existing value of the @id property is `_:b0`, the blank node identifier is `b0`).
    5. Append |skolemizedNode| to |skolemizedExpandedDocument|.
  3. Return |skolemizedExpandedDocument|.

skolemizeCompactJsonLd

The following algorithm replaces all blank node identifiers in a compact JSON-LD document with custom-scheme URNs. The required inputs are a compact JSON-LD document (|document|) and a custom URN scheme (|urnScheme|) which defaults to "custom-scheme:". The |document| is assumed to use only one @context property at the top level of the document. Any additional custom options (such as a document loader) can also be passed. It produces both an expanded form of the skolemized JSON-LD document (|skolemizedExpandedDocument| and a compact form of the skolemized JSON-LD document (|skolemizedCompactDocument|) as output. The skolemization used in this operation is intended to be reversible through the use of the algorithm in Section [[[#todeskolemizednquads]]] which uses the same custom URN scheme (|urnScheme|).

  1. Initialize |expanded| to the result of the JSON-LD Expansion Algorithm, passing |document| and any custom options.
  2. Initialize |skolemizedExpandedDocument| to the result of the algorithm in Section [[[#skolemizeexpandedjsonld]]].
  3. Initialize |skolemizedCompactDocument| to the result of the JSON-LD Compaction Algorithm, passing |skolemizedExpandedDocument| and any custom options.
  4. Return an object with both |skolemizedExpandedDocument| and |skolemizedCompactDocument|.

toDeskolemizedNQuads

The following algorithm converts a skolemized JSON-LD document, such as one created using the algorithm in Section [[[#skolemizecompactjsonld]]], to an array of deskolemized N-Quads. The required input is a JSON-LD document, skolemizedDocument. Additional custom options (such as a document loader) can be passed. An array of deskolemized N-Quad strings (|deskolemizedNQuads|) is produced as output.

  1. Initialize |skolemizedDataset| to the result of the Deserialize JSON-LD to RDF algorithm, passing any custom options (such as a document loader), to convert |skolemizedDocument| from JSON-LD to RDF in N-Quads format.
  2. Split |skolemizedDataset| into an array of individual N-Quads, |skolemizedNQuads|.
  3. Set |deskolemizedNQuads| to the result of the algorithm in Section [[[#deskolemizenquads]]] with |skolemizedNQuads| and "custom-scheme:" as parameters. Implementations MAY choose a different urnScheme that is different than "custom-scheme:" so long as the same scheme name was used to generate skolemizedDocument.
  4. Return |deskolemizedNQuads|.

jsonPointerToPaths

The following algorithm converts a JSON Pointer [[RFC6901]] to an array of paths into a JSON tree. The required input is a JSON Pointer string (|pointer|). An array of paths (paths) is produced as output.

  1. Initialize |paths| to an empty array.
  2. Initialize |splitPath| to an array by splitting |pointer| on the "/" character and skipping the first, empty, split element. In Javascript notation, this step is equivalent to the following code: `pointer.split('/').slice(1)`
  3. For each |path| in |splitPath|:
    1. If |path| does not include `~`, then add |path| to |paths|, converting it to an integer if it parses as one, leaving it as a string if it does not.
    2. Otherwise, unescape any JSON pointer escape sequences in |path| and add the result to |paths|.
  4. Return |paths|.

createInitialSelection

The following algorithm creates an initial selection (a fragment of a JSON-LD document) based on a JSON-LD object. This is a helper function used within the algorithm in Section [[[#selectjsonld]]]. The required input is a JSON-LD object (|source|). A JSON-LD document fragment object (|selection|) is produced as output.

  1. Initialize |selection| to an empty object.
  2. If |source| has an `id` that is not a blank node identifier, set |selection|.|id| to its value. Note: All non-blank node identifiers in the path of any JSON Pointer MUST be included in the selection, this includes any root document identifier.
  3. If |source|.|type| is set, set |selection|.|type| to its value. Note: The selection MUST include all `type`s in the path of any JSON Pointer, including any root document `type`.
  4. Return |selection|.

selectPaths

The following algorithm selects a portion of a compact JSON-LD document using paths parsed from a parsed JSON Pointer. This is a helper function used within the algorithm in Section [[[#selectjsonld]]]. The required inputs are an array of paths (|paths|) parsed from a JSON Pointer, a compact JSON-LD document (|document|), a selection document (|selectionDocument|) to be populated, and an array of arrays (|arrays|) for tracking selected arrays. This algorithm produces no output; instead it populates the given |selectionDocument| with any values selected via |paths|.

  1. Initialize |parentValue| to |document|.
  2. Initialize |value| to |parentValue|.
  3. Initialize |selectedParent| to |selectionDocument|.
  4. Initialize |selectedValue| to |selectedParent|.
  5. For each |path| in |paths|:
    1. Set |selectedParent| to |selectedValue|.
    2. Set |parentValue| to |value|.
    3. Set |value| to |parentValue|.|path|. If |value| is now undefined, an error MUST be raised and SHOULD convey an error type of PROOF_GENERATION_ERROR, indicating that the JSON pointer does not match the given |document|.
    4. Set |selectedValue| to |selectedParent|.|path|.
    5. If |selectedValue| is now undefined:
      1. If |value| is an array, set |selectedValue| to an empty array and append |selectedValue| to |arrays|.
      2. Otherwise, set |selectedValue| to an initial selection passing |value| as |source| to the algorithm in Section [[[#createinitialselection]]].
      3. Set |selectedParent|.|path| to |selectedValue|.
  6. Note: With path traversal complete at the target value, the selected value will now be computed.
  7. If |value| is a literal, set |selectedValue| to |value|.
  8. If |value| is an array, Set |selectedValue| to a copy of |value|.
  9. In all other cases, set |selectedValue| to an object that merges a shallow copy of |selectedValue| with a deep copy of |value|, e.g., `{...selectedValue, …deepCopy(value)}`.
  10. Get the last |path|, |lastPath|, from |paths|.
  11. Set |selectedParent|.|lastPath| to |selectedValue|.

selectJsonLd

The following algorithm selects a portion of a compact JSON-LD document using an array of JSON Pointers. The required inputs are an array of JSON Pointers (|pointers|) and a compact JSON-LD document (|document|). The |document| is assumed to use a JSON-LD context that aliases `@id` and `@type` to `id` and `type`, respectively, and to use only one `@context` property at the top level of the document. A new JSON-LD document that represents a selection (selectionDocument) of the original JSON-LD document is produced as output.

  1. If |pointers| is empty, return `null`. This indicates nothing has been selected from the original document.
  2. Initialize |arrays| to an empty array. This variable will be used to track selected sparse arrays to make them dense after all |pointers| have been processed.
  3. Initialize |selectionDocument| to an initial selection passing |document| as |source| to the algorithm in Section [[[#createinitialselection]]].
  4. Set the value of the `@context` property in |selectionDocument| to a copy of the value of the `@context` property in |document|.
  5. For each |pointer| in |pointers|, walk the document from root to the pointer target value, building the |selectionDocument|:
    1. Parse the |pointer| into an array of paths, |paths|, using the algorithm in Section [[[#jsonpointertopaths]]].
    2. Use the algorithm in Section [[[#selectpaths]]], passing |document|, |paths|, |selectionDocument|, and |arrays|.
  6. For each |array| in |arrays|:
    1. Make |array| dense by removing any undefined elements between elements that are defined.
  7. Return |selectionDocument|.

relabelBlankNodes

The following algorithm relabels the blank node identifiers in an array of N-Quad strings using a blank node label map. The required inputs are an array of N-Quad strings (|nquads|) and a blank node label map (|labelMap|). An array of N-Quad strings with relabeled blank node identifiers (|relabeledNQuads|) is produced as output.

  1. Create a new array of N-Quad strings, relabeledNQuads.
  2. For each N-Quad string, s1, in |nquads|:
    1. Create a new string, s2, such it that is a copy of s1 except each blank node identifier therein has been replaced with the value associated with it as a key in |labelMap|.
    2. Append s2 to relabeledNQuads.
  3. Return relabeledNQuads.

selectCanonicalNQuads

The following algorithm selects a portion of a skolemized compact JSON-LD document using an array of JSON Pointers, and outputs the resulting canonical N-Quads with any blank node labels replaced using the given label map. The required inputs are an array of JSON Pointers (|pointers|), a skolemized compact JSON-LD document (|skolemizedCompactDocument|), and a blank node label map (|labelMap|). Additional custom options (such as a document loader) can be passed. The |document| is assumed to use a JSON-LD context that aliases `@id` and `@type` to `id` and `type`, respectively, and to use only one `@context` property at the top level of the document. An object containing the new JSON-LD document that represents a selection of the original JSON-LD document (|selectionDocument|), an array of deskolemized N-Quad strings (|deskolemizedNQuads|), and an array of canonical N-Quads with replacement blank node labels (|nquads|) is produced as output.

  1. Initialize |selectionDocument| to the result of the algorithm in Section [[[#selectjsonld]]], passing |pointers|, and |skolemizedCompactDocument| as document.
  2. Initialize |deskolemizedNQuads| to the result of the algorithm in Section [[[#todeskolemizednquads]]], passing |selectionDocument| as |skolemizedCompactDocument|, and any custom options.
  3. Initialize |nquads| to the result of the algorithm in Section [[[#relabelblanknodes]]], passing |labelMap|, and |deskolemizedNQuads| as |nquads|.
  4. Return an object containing |selectionDocument|, |deskolemizedNQuads|, and |nquads|.

canonicalizeAndGroup

The following algorithm is used to output canonical N-Quad strings that match custom selections of a compact JSON-LD document. It does this by canonicalizing a compact JSON-LD document (replacing any blank node identifiers using a label map) and grouping the resulting canonical N-Quad strings according to the selection associated with each group. Each group will be defined using an assigned name and array of JSON pointers. The JSON pointers will be used to select portions of the skolemized document, such that the output can be converted to canonical N-Quads to perform group matching.

The required inputs are a compact JSON-LD document (|document|), a label map factory function (|labelMapFactoryFunction|), and a map of named group definitions (|groupDefinitions|). Additional custom options (such as a document loader) can be passed. The |document| is assumed to use a JSON-LD context that aliases `@id` and `@type` to `id` and `type`, respectively, and to use only one `@context` property at the top level of the document. An object containing the created groups (|groups|), the skolemized compact JSON-LD document (|skolemizedCompactDocument|), the skolemized expanded JSON-LD document (|skolemizedExpandedDocument|), the deskolemized N-Quad strings (|deskolemizedNQuads|), the blank node label map (|labelMap|), and the canonical N-Quad strings |nquads|, is produced as output.

  1. Initialize |skolemizedExpandedDocument| and |skolemizedCompactDocument| to their associated values in the result of the algorithm in Section [[[#skolemizecompactjsonld]]], passing |document| and any custom options.
  2. Initialize |deskolemizedNQuads| to the result of the algorithm in Section [[[#todeskolemizednquads]]], passing |skolemizedExpandedDocument| and any custom options.
  3. Initialize |nquads| and |labelMap| to their associated values in the result of the algorithm in Section [[[#labelreplacementcanonicalizenquads]]], passing |labelMapFactoryFunction|, |deskolemizedNQuads| as |nquads|, and any custom options.
  4. Initialize |selections| to a new map.
  5. For each key (|name|) and value (|pointers|) entry in |groupDefinitions|:
    1. Add an entry with a key of |name| and a value that is the result of the algorithm in Section [[[#selectcanonicalnquads]]], passing |pointers|, |labelMap|, |skolemizedCompactDocument| as |document|, and any custom options.
  6. Initialize |groups| to an empty object.
  7. For each key (|name|) and value (|selectionResult|) entry in |selections|:
    1. Initialize |matching| to an empty map.
    2. Initialize |nonMatching| to an empty map.
    3. Initialize |selectedNQuads| to nquads from |selectionResult|.
    4. Initialize |selectedDeskolemizedNQuads| from deskolemizedNQuads from |selectionResult|.
    5. For each element (|nq|) and index (|index|) in |nquads|:
      1. Create a map entry, |entry|, with a key of |index| and a value of |nq|.
      2. If |selectedNQuads| includes |nq| then add |entry| to |matching|; otherwise, add |entry| to |nonMatching|.
    6. Set |name| in |groups| to an object containing |matching|, |nonMatching|, and |selectedDeskolemizedNQuads| as |deskolemizedNQuads|.
  8. Return an object containing |groups|, |skolemizedExpandedDocument|, |skolemizedCompactDocument|, |deskolemizedNQuads|, |labelMap|, and |nquads|.

hashMandatoryNQuads

The following algorithm cryptographically hashes an array of mandatory to disclose N-Quads using a provided hashing API. The required input is an array of mandatory to disclose N-Quads (|mandatory|) and a hashing function (|hasher|). A cryptographic hash (mandatoryHash) is produced as output.

  1. Initialize `bytes` to the UTF-8 representation of the joined `mandatory` N-Quads.
  2. Initialize `mandatoryHash` to the result of using `hasher` to hash `bytes`.
  3. Return `mandatoryHash`.

Security Considerations

The following section describes security considerations that developers implementing this specification should be aware of in order to create secure software.

Versioning Cryptography Suites

Cryptography secures information through the use of secrets. Knowledge of the necessary secret makes it computationally easy to access certain information. The same information can be accessed if a computationally-difficult, brute-force effort successfully guesses the secret. All modern cryptography requires the computationally difficult approach to remain difficult throughout time, which does not always hold due to breakthroughs in science and mathematics. That is to say that Cryptography has a shelf life.

This specification plans for the obsolescence of all cryptographic approaches by asserting that whatever cryptography is in use today is highly likely to be broken over time. Software systems have to be able to change the cryptography in use over time in order to continue to secure information. Such changes might involve increasing required secret sizes or modifications to the cryptographic primitives used. However, some combinations of cryptographic parameters might actually reduce security. Given these assumptions, systems need to be able to distinguish different combinations of safe cryptographic parameters, also known as cryptographic suites, from one another. When identifying or versioning cryptographic suites, there are several approaches that can be taken which include: parameters, numbers, and dates.

Parametric versioning specifies the particular cryptographic parameters that are employed in a cryptographic suite. For example, one could use an identifier such as `RSASSA-PKCS1-v1_5-SHA1`. The benefit to this scheme is that a well-trained cryptographer will be able to determine all of the parameters in play by the identifier. The drawback to this scheme is that most of the population that uses these sorts of identifiers are not well trained and thus will not understand that the previously mentioned identifier is a cryptographic suite that is no longer safe to use. Additionally, this lack of knowledge might lead software developers to generalize the parsing of cryptographic suite identifiers such that any combination of cryptographic primitives becomes acceptable, resulting in reduced security. Ideally, cryptographic suites are implemented in software as specific, acceptable profiles of cryptographic parameters instead.

Numbered versioning might specify a major and minor version number such as `1.0` or `2.1`. Numbered versioning conveys a specific order and suggests that higher version numbers are more capable than lower version numbers. The benefit of this approach is that it removes complex parameters that less expert developers might not understand with a simpler model that conveys that an upgrade might be appropriate. The drawback of this approach is that its not clear if an upgrade is necessary, as software version number increases often don't require an upgrade for the software to continue functioning. This can lead to developers thinking their usage of a particular version is safe, when it is not. Ideally, additional signals would be given to developers that use cryptographic suites in their software that periodic reviews of those suites for continued security are required.

Date-based versioning specifies a particular release date for a specific cryptographic suite. The benefit of a date, such as a year, is that it is immediately clear to a developer if the date is relatively old or new. Seeing an old date might prompt the developer to go searching for a newer cryptographic suite, where as a parametric or number-based versioning scheme might not. The downside of a date-based version is that some cryptographic suites might not expire for 5-10 years, prompting the developer to go searching for a newer cryptographic suite only to not find one that is newer. While this might be an inconvenience, it is one that results in safer ecosystem behavior.

Protecting Application Developers

Modern cryptographic algorithms provide a number of tunable parameters and options to ensure that the algorithms can meet the varied requirements of different use cases. For example, embedded systems have limited processing and memory environments and might not have the resources to generate the strongest digital signatures for a given algorithm. Other environments, like financial trading systems, might only need to protect data for a day while the trade is occurring, while other environments might need to protect data for multiple decades. To meet these needs, cryptographic algorithm designers often provide multiple ways to configure a cryptographic algorithm.

Cryptographic library implementers often take the specifications created by cryptographic algorithm designers and specification authors and implement them such that all options are available to the application developers that use their libraries. This can be due to not knowing which combination of features a particular application developer might need for a given cryptographic deployment. All options are often exposed to application developers.

Application developers that use cryptographic libraries often do not have the requisite cryptographic expertise and knowledge necessary to appropriately select cryptographic parameters and options for a given application. This lack of expertise can lead to an inappropriate selection of cryptographic parameters and options for a particular application.

This specification sets the priority of constituencies to protect application developers over cryptographic library implementers over cryptographic specification authors over cryptographic algorithm designers. Given these priorities, the following recommendations are made:

The guidance above is meant to ensure that useful cryptographic options and parameters are provided at the lower layers of the architecture while not exposing those options and parameters to application developers who may not fully understand the balancing benefits and drawbacks of each option.

Conventions for Naming Cryptography Suites

Section [[[#versioning-cryptography-suites]]] emphasized the importance of providing relatively easy to understand information concerning the timeliness of particular cryptographic suite, while section [[[#protecting-application-developers]]] further emphasized minimizing the number of options to be specified. Indeed, section [[[#cryptographic-suites]]] lists requirements for cryptographic suites which include detailed specification of algorithm, transformation, hashing, and serialization. Hence, the name of the cryptographic suite does not need to include all this detail, which implies the parametric versioning mentioned in section [[[#versioning-cryptography-suites]]] is neither necessary nor desirable.

The recommended naming convention for cryptographic suites is a string composed of a signature algorithm identifier, separated by a hyphen from an option identifier (if the cryptosuite supports incompatible implementation options), followed by a hyphen and designation of the approximate year that the suite was proposed.

For example, the [[?DI-EDDSA]] is based on EdDSA digital signatures, supports two incompatible options based on canonicalization approaches, and was proposed in roughly the year 2022, so it would have two different cryptosuite names: eddsa-rdfc-2022 and eddsa-jcs-2022.

Although the [[?DI-ECDSA]] is based on ECDSA digital signatures, supports the same two incompatible canonicalization approaches as [[?DI-EDDSA]], and supports two different levels of security (128 bit and 192 bit) via two alternative sets of elliptic curves and hashes, it has only two cryptosuite names: ecdsa-rdfc-2019 and ecdsa-jcs-2019. The security level and corresponding curves and hashes are determined from the multi-key format of the public key used in validation.

Agility and Layering

Cryptographic agility is a practice by which one designs frequently connected information security systems to support switching between multiple cryptographic primitives and/or algorithms. The primary goal of cryptographic agility is to enable systems to rapidly adapt to new cryptographic primitives and algorithms without making disruptive changes to the systems' infrastructure. Thus, when a particular cryptographic primitive, such as the SHA-1 algorithm, is determined to be no longer safe to use, systems can be reconfigured to use a newer primitive via a simple configuration file change.

Cryptographic agility is most effective when the client and the server in the information security system are in regular contact. However, when the messages protected by a particular cryptographic algorithm are long-lived, as with [=verifiable credentials=], and/or when the client (holder) might not be able to easily recontact the server (issuer), then cryptographic agility does not provide the desired protections.

Cryptographic layering is a practice where one designs rarely connected information security systems to employ multiple primitives and/or algorithms at the same time. The primary goal of cryptographic layering is to enable systems to survive the failure or one or more cryptographic algorithms or primitives without losing cryptographic protection on the payload. For example, digitally signing a single piece of information using RSA, ECDSA, and Falcon algorithms in parallel would provide a mechanism that could survive the failure of two of these three digital signature algorithms. When a particular cryptographic protection is compromised, such as an RSA digital signature using 768-bit keys, systems can still utilize the non-compromised cryptographic protections to continue to protect the information. Developers are urged to take advantage of this feature for all signed content that might need to be protected for a year or longer.

This specification provides for both forms of agility. It provides for cryptographic agility, which allows one to easily switch from one algorithm to another. It also provides for cryptographic layering, which allows one to simultaneously use multiple cryptographic algorithms, typically in parallel, such that any of those used to protect information can be used without reliance on or requirement of the others, while still keeping the digital proof format easy to use for developers.

Safer Abstractions

A [=proof=] contains a `proofValue`, in which a number of parameters related to a cryptographic proof can be embedded. For example, the selective disclosure algorithms in the [[[?VC-DI-ECDSA]]] specification include multiple cryptographic signatures in the `proofValue`, one for every item that is selectively disclosable. This has been done to make the technology easier and safer to use by application developers.

This specification urges specification authors to use a single value to abstract information that is rarely needed by the application layer. Much like a `data:` URL expressing an image encapsulates many of the image rendering parameters into a single value, the `proofValue` property (and other similar properties) abstracts information that is not useful to an application developer in order to simplify identification of fields that are important to the application. Abstracting information in this way leads to a data structure that is easier for developers to work with while being less susceptible to, for instance, accidental corruption due to a programming error that adds or removes a critical property, negatively impacting the cryptographic layer.

The design of data integrity in this specification abstracts information that is generally only useful to the cryptographic layer into a single property to ease the burden on application developers and to enhance the security of the system.

Transformations

At times, it is beneficial to transform the data being protected during the cryptographic protection process. Such "in-line" transformation can enable a particular type of cryptographic protection to be agnostic to the data format it is carried in. For example, some Data Integrity cryptographic suites utilize RDF Dataset Canonicalization [[?RDF-CANON]] which transforms the initial representation into a canonical form [[?N-QUADS]] that is then serialized, hashed, and digitally signed. As long as any syntax expressing the protected data can be transformed into this canonical form, the digital signature can be verified. This enables the same digital signature over the information to be expressed in JSON, CBOR, YAML, and other compatible syntaxes without having to create a cryptographic proof for every syntax.

Being able to express the same digital signature across a variety of syntaxes is beneficial because systems often have native data formats with which they operate. For example, some systems are written against JSON data, while others are written against CBOR data. Without transformation, systems that process their data internally as CBOR are required to store the digitally signed data structures as JSON (or vice-versa). This leads to double-storing data and can lead to increased security attack surface if the unsigned representation stored in databases accidentally deviates from the signed representation. By using transformations, the digital proof can live in the native data format to help prevent otherwise undetectable database drift over time.

This specification is designed to avoid requiring the duplication of signed information by utilizing "in-line" data transformations. Application developers are urged to work with cryptographically protected data in the native data format for their application and not separate storage of cryptographic proofs from the data being protected. Developers are also urged to regularly confirm that the cryptographically protected data has not been tampered with as it is written to and read from application storage.

Some transformations, such as RDF Dataset Canonicalization [[?RDF-CANON]], have mitigations for input data sets that can be used by attackers to consume excessive processing cycles. This class of attack is called dataset poisoning, and all modern RDF Dataset canonicalizers are required to detect these sorts of bad inputs and halt processing. The test suites for RDF Dataset Canonicalization includes such poisoned datasets to ensure that such mitigations exist in all conforming implementations. Generally speaking, cryptographic suite specifications that use transformations are required to mitigate these sorts of attacks, and implementers are urged to ensure that the software libraries that they use enforce these mitigations. These attacks are in the same general category as any resource starvation attack, such as HTTP clients that deliberately slow connections, thus starving connections on the server. Implementers are advised to consider these sorts of attacks when implementing defensive security strategies.

Protected Information

The data that is protected by any [=data integrity proof=] is the [=transformation|transformed data=]. [=transformation|Transformed data=] is generated by a [=transformation algorithm=] that is specified by a particular [=cryptosuite=]. This protection mechanism differs from some more traditional digital signature mechanisms that do not perform any sort of [=transformation=] on the input data. The benefits of [=transformation=] are detailed in Section [[[#transformations]]].

For example, [=cryptosuites=] such as ecdsa-jcs-2019 and eddsa-jcs-2022 use the [[[?RFC8785]]] to [=transformation|transform=] the data to canonicalized JSON, which is then cryptographically hashed and digitally signed. One benefit of this approach is that adding or removing formatting characters that do not impact the meaning of the information being signed, such as spaces, tabs, and newlines, does not invalidate the digital signature. More traditional digital signature mechanisms do not have this capability.

Other [=cryptosuites=] such as ecdsa-rdfc-2019 and eddsa-rdfc-2022 use [[[?RDF-CANON]]] to [=transformation|transform=] the data to canonicalized N-Quads [[?N-QUADS]], which is then cryptographically hashed and digitally signed. One benefit of this approach is that the cryptographic signature is portable to a variety of different syntaxes, such as JSON, YAML, and CBOR, without invalidating the signature. More traditional cryptographic signature mechanisms do not have this capability.

Implementers and developers are urged to not trust information that contains a [=data integrity proof=] unless the proof has been [=verified=] and the verified data is provided in a return value from a software library that has confirmed that all data returned has been successfully protected.

Data Opacity

The inspectability of application data has effects on system efficiency and developer productivity. When cryptographically protected application data, such as base-encoded binary data, is not easily processed by application subsystems, such as databases, it increases the effort of working with the cryptographically protected information. For example, a cryptographically protected payload that can be natively stored and indexed by a database will result in a simpler system that:

Similarly, a cryptographically protected payload that can be processed by multiple upstream networked systems increases the ability to properly layer security architectures. For example, if upstream systems do not have to repeatedly decode the incoming payload, it increases the ability for a system to distribute processing load by specializing upstream subsystems to actively combat attacks. While a digital signature needs to always be checked before taking substantive action, other upstream checks can be performed on transparent payloads — such as identifier-based rate limiting, signature expiration checking, or nonce/challenge checking — to reject obviously bad requests.

Additionally, if a developer is not able to easily view data in a system, the ability to easily audit or debug system correctness is hampered. For example, requiring application developers to cut-and-paste base-encoded application data makes development more challenging and increases the chances that obvious bugs will be missed because every message needs to go through a manually operated base-decoding tool.

There are times, however, where the correct design decision is to make data opaque. Data that does not need to be processed by other application subsystems, as well as data that does not need to be modified or accessed by an application developer, can be serialized into opaque formats. Examples include digital signature values, cryptographic key parameters, and other data fields that only need to be accessed by a cryptographic library and need not be modified by the application developer. There are also examples where data opacity is appropriate when the underlying subsystem does not expose the application developer to the underlying complexity of the opaque data, such as databases that perform encryption at rest. In these cases, the application developer continues to develop against transparent application data formats while the database manages the complexity of encrypting and decrypting the application data to and from long-term storage.

This specification strives to provide an architecture where application data remains in its native format and is not made opaque, while other cryptographic data, such as digital signatures, are kept in their opaque binary encoded form. Cryptographic suite implementers are urged to consider appropriate use of data opacity when designing their suites, and to weigh the design trade-offs when making application data opaque versus providing access to cryptographic data at the application layer.

Verification Method Binding

Implementers ensure that a [=verification method=] is bound to a particular controller by going from the definition of the [=verification method=] to the [=controlled identifier document=], and then ensuring that the [=controlled identifier document=] also contains a reference to the [=verification method=]. This process is described in the algorithm for retrieving a verification method.

Verification Relationship Validation

When an implementation is verifying a proof, it is imperative that it verify not only that the [=verification method=] used to generate the proof is listed in the [=controlled identifier document=], but also that it was intended to be used to generate the proof that is being verified. This process is known as "verification relationship validation".

The process of validating a verification relationship is outlined in Section 3.3 Retrieve Verification Method of the [[[CID]]] specification.

This process is used to ensure that cryptographic material, such as a private cryptographic key, is not misused by application to an unintended purpose. An example of cryptographic material misuse would be if a private cryptographic key meant to be used to issue a [=verifiable credential=] was instead used to log into a website (that is, for authentication). Not checking a verification relationship is dangerous because the restriction and protection profile for some cryptographic material could be determined by its intended use. For example, some applications could be trusted to use cryptographic material for only one purpose, or some cryptographic material could be more protected, such as through storage in a hardware security module in a data center versus as an unencrypted file on a laptop.

Proof Purpose Validation

When an implementation is verifying a proof, it is imperative that it verify that the [=proof purpose=] match the intended use.

This process is used to ensure that proofs are not misused by an application for an unintended purpose, as this is dangerous for the proof creator. An example of misuse would be if a proof that stated its purpose was for securing assertions in [=verifiable credentials=] was instead used for [=authentication=] to log into a website. In this case, the proof creator attached proofs to any number of [=verifiable credentials=] that they expected to be distributed to an unbounded number of other parties. Any one of these parties could log into a website as the proof creator if the website erroneously accepted such a proof as [=authentication=] instead of its intended purpose.

Canonicalization Method Security

The way in which a transformation, such as canonicalization, is performed can affect the security characteristics of a system. Selecting the best canonicalization mechanisms depends on the use case. Often, the simplest mechanism that satisfies the desired security requirements is the best choice. This section attempts to provide simple guidance to help implementers choose between the two main canonicalization mechanisms referred to in this specification, namely JSON Canonicalization Scheme [[RFC8785]] and RDF Dataset Canonicalization [[RDF-CANON]].

If an application only uses JSON and does not depend on any form of RDF semantics, then using a cryptography suite that uses JSON Canonicalization Scheme [[RFC8785]] is an attractive approach.

If an application uses JSON-LD and needs to secure the semantics of the document, then using a cryptography suite that uses RDF Dataset Canonicalization [[RDF-CANON]] is an attractive approach.

Implementers are also advised that other mechanisms that perform no transformations are available, that secure the data by wrapping it in a cryptographic envelope instead of embedding the proof in the data, such as JWTs [[?RFC7519]] and CWTs [[?RFC8392]]. These approaches have simplicity advantages in some use cases, at the expense of some of the benefits provided by the approach detailed in this specification.

Canonicalization Method Correctness

One of the algorithmic processes used by this specification is canonicalization, which is a type of [=transformation=]. Canonicalization is the process of taking information that might be expressed in a variety of semantically equivalent ways as input, and expressing all output in a single way, called a "canonical form".

The security of a resulting [=data integrity proof=] that utilizes canonicalization is highly dependent on the correctness of the algorithm. For example, if a canonicalization algorithm converts two inputs that have different meanings into the same output, then the author's intentions can be misrepresented to a [=verifier=]. This can be used as an attack vector by adversaries.

Additionally, if semantically relevant information in an input is not present in the output, then an attacker could insert such information into a message without causing proof verification to fail. This is similar to another transformation that is commonly used when cryptographically signing messages: cryptographic hashing. If an attacker is able to produce the same cryptographic hash from a different input, then the cryptographic hash algorithm is not considered secure.

Implementers are strongly urged to ensure proper vetting of any canonicalization algorithms to be used for [=transformation=] of input to a [=hashing=] process. Proper vetting includes, at a minimum, association with a peer reviewed mathematical proof of algorithm correctness; multiple implementations and vetting by experts in a standards setting organization is preferred. Implementers are strongly urged not to invent or use new mechanisms unless they have formal training in information canonicalization and/or access to experts in the field who are capable of producing a peer reviewed mathematical proof of algorithm correctness.

Network Requests

This specification is designed in such a way that no network requests are required when verifying a proof on a [=conforming secured document=]. Readers might note, however, that JSON-LD contexts and [=verification methods=] can contain URLs that might be retrieved over a network connection. This concern exists for any URL that might be loaded from the network during or after verification.

To the extent possible, implementers are urged to permanently or aggressively cache such information to reduce the attack surface on an implementation that might need to fetch such URLs over the network. For example, caching techniques for JSON-LD contexts are described in Section [[[#contexts-and-vocabularies]]], and some [=verification methods=], such as `did:key` [[?DID-KEY]], do not need to be fetched from the network at all.

When it is not possible to use cached information, such as when a specific HTTP URL-based instance of a [=verification method=] is encountered for the first time, implementers are cautioned to use defensive measures to mitigate denial-of-service attacks during any process that might fetch a resource from the network.

Other Security Considerations

Since the technology to secure documents described by this specification is generalized in nature, the security implications of its use might not be immediately apparent to readers. To understand the sort of security concerns one might need to consider in a complete software system, implementers are urged to read about how this technology is used in the [=verifiable credentials=] ecosystem [[?VC-DATA-MODEL-2.0]]; see the section on Verifiable Credential Security Considerations for more information.

Privacy Considerations

The following section describes privacy considerations that developers implementing this specification should be aware of in order to create privacy enhancing software.

Unlinkability

When a digitally-signed payload contains data that is seen by multiple verifiers, it becomes a point of correlation. An example of such data is a shopping loyalty card number. Correlatable data can be used for tracking purposes by verifiers, which can sometimes violate privacy expectations. The fact that some data can be used for tracking might not be immediately apparent. Examples of such correlatable data include, but are not limited to, a static digital signature or a cryptographic hash of an image.

It is possible to create a digitally-signed payload that does not have any correlatable tracking data while also providing some level of assurance that the payload is trustworthy for a given interaction. This characteristic is called unlinkability which ensures that no correlatable data are used in a digitally-signed payload while still providing some level of trust, the sufficiency of which has to be determined by each verifier.

It is important to understand that not all use cases require or even permit unlinkability. There are use cases where linkability and correlation are required due to regulatory or safety reasons, such as correlating organizations and individuals that are shipping and storing hazardous materials. Unlinkability is useful when there is an expectation of privacy for a particular interaction.

There are at least two mechanisms that can provide some level of unlinkability. The first method is to ensure that no data value used in the message is ever repeated in a future message. The second is to ensure that any repeated data value provides adequate herd privacy such that it becomes practically impossible to correlate the entity that expects some level of privacy in the interaction.

A variety of methods can be used to achieve unlinkability. These methods include ensuring that a message is a single use bearer token with no information that can be used for the purposes of correlation, using attributes that ensure an adequate level of herd privacy, and the use of cryptosuites that enable the entity presenting a message to regenerate new signatures while not compromising the trust in the message being presented.

Selective Disclosure

Selective disclosure is a technique that enables the recipient of a previously-signed message (that is, a message signed by its creator) to reveal only parts of the message without disturbing the verifiability of those parts. For example, one might selectively disclose a digital driver's license for the purpose of renting a car. This could involve revealing only the issuing authority, license number, birthday, and authorized motor vehicle class from the license. Note that in this case, the license number is correlatable information, but some amount of privacy is preserved because the driver's full name and address are not shared.

Not all software or cryptosuites are capable of providing selective disclosure. If the author of a message wishes it to be selectively disclosable by its recipient, then they need to enable selective disclosure on the specific message, and both need to use a capable cryptosuite. The author might also make it mandatory to disclose certain parts of the message. A recipient that wants to selectively disclose partial content of the message needs to utilize software that is able to perform the technique. An example of a cryptosuite that supports selective disclosure is `bbs-2023`.

It is possible to selectively disclose information in a way that does not preserve unlinkability. For example, one might want to disclose the inspection results related to a shipment, which include the shipment identifier or lot number, which might have to be correlatable due to regulatory requirements. However, disclosure of the entire inspection result might not be required as selectively disclosing just the pass/fail status could be deemed adequate. For more information on disclosing information while preserving privacy, see Section [[[#unlinkability]]].

Previous Proofs

When using the `previousProof` feature defined in [[[#proof-chains]]], implementations are required to digitally sign over one or more previous proofs, so as to include them in the secured payload. This inevitably exposes information related to each entity that added a previous proof.

At minimum, the [=verification method=] for the previous proof, such as a public key, is seen by the creator of the next proof in a proof chain. This can be a privacy concern if the creator of the previous proof did not intend to be included in a proof chain, but is an inevitable outcome when adding a non-repudiable digital signature to a document of any kind.

It is possible to use more advanced cryptographic mechanisms, such as a group signature, to hide the identity of the signer of a message, and it is also possible for a Data Integrity cryptographic suite to mitigate this privacy concern.

Fingerprinting Network Requests

Fingerprinting concerns exist for any URL that might be loaded from the network during or after proof verification. This specification is designed in such a way that no network requests are necessary when verifying a proof on a [=conforming secured document=]. Readers might note, however, that JSON-LD contexts and [=verification methods=] can contain resource URLs that might be retrieved over a network connection leading to fingerprinting concerns.

For example, creators of [=conforming secured documents=] might craft unique per-document URLs for JSON-LD contexts and [=verification methods=]. When verifying such a document, a verifier fetching that information from the network would reveal their interest in the [=conforming secured document=] to the creator of the document, which might lead to a mismatch in privacy expectations for any entity that is not the creator of the document.

Implementers are urged to follow the guidance in Section [[[#network-requests]]] on URL caching and implementing defensively when fetching URLs from the network. Usage of techniques such as Oblivious HTTP to retrieve resources from the network, without revealing the client that is making the request, are encouraged. Additionally, heuristics might be used to determine whether creators of [=conforming secured documents=] are using fingerprinting URLs in a way that might violate privacy expectations. These heuristics could be used to display warnings to entities that might process documents containing suspected fingerprinting URLs.

Canonicalization Method Privacy

The way in which a transformation, namely canonicalization, is performed can affect the privacy characteristics of a system. Selecting the best canonicalization mechanism depends on the use case. This section attempts to provide simple guidance to help implementers pick between the two main canonicalization mechanisms referred to in this specification, namely JSON Canonicalization Scheme [[RFC8785]] and RDF Dataset Canonicalization [[RDF-CANON]], from a privacy perspective.

If an application does not require performing a selective disclosure of information in a secured document, nor does it utilize JSON-LD, then JSON Canonicalization Scheme [[RFC8785]] is an attractive approach.

If an application uses JSON-LD and might require selective disclosure of information in a secured document, then using a cryptography suite that uses RDF Dataset Canonicalization [[RDF-CANON]] is an attractive approach.

Implementers are also advised that other selective disclosure mechanisms that perform no transformations are available, that secure the data by wrapping it in a cryptographic envelope instead of embedding the proof in the data, such as SD-JWTs [[?SD-JWT]]. This approach has simplicity advantages in some use cases, at the expense of some of the benefits provided by the approach detailed in this specification.

Other Privacy Considerations

Since the technology to secure documents described by this specification is generalized in nature, the privacy implications of its use might not be immediately apparent to readers. To understand the sort of privacy concerns one might need to consider in a complete software system, implementers are urged to read about how this technology is used in the [=verifiable credentials=] ecosystem [[?VC-DATA-MODEL-2.0]]; see the section on Verifiable Credential Privacy Considerations for more information.

Accessibility Considerations

The following section describes accessibility considerations that developers implementing this specification are urged to consider in order to ensure that their software is usable by people with different cognitive, motor, and visual needs. As a general rule, this specification is used by system software and does not directly expose individuals to information subject to accessibility considerations. However, there are instances where individuals might be indirectly exposed to information expressed by this specification and thus the guidance below is provided for those situations.

Presenting Time Values

This specification enables the expression of dates and times related to the validity period of cryptographic proofs. This information might be indirectly exposed to an individual if a proof is processed and is detected to be outside an allowable time range. When exposing these dates and times to an individual, implementers are urged to take into account cultural normas and locales when representing dates and times in display software. In addition to these considerations, presenting time values in a way that eases the cognitive burden on the individual receiving the information is a suggested best practice.

For example, when conveying the expiration date for a particular set of digitally signed information, implementers are urged to present the time of expiration using language that is easier to understand rather than language that optimizes for accuracy. Presenting the expiration time as "This ticket expired three days ago." is preferred over a phrase such as "This ticket expired on July 25th 2023 at 3:43 PM." The former provides a relative time that is easier to comprehend than the latter time, which requires the individual to do the calculation in their head and presumes that they are capable of doing such a calculation.

Understanding Proof Sets and Proof Chains

Sections [[[#proof-sets]]] and [[[#proof-chains]]] describe how multiple proofs can be expressed in a [=secured data document=]; that is, instead of a single [=proof=] included in the [=secured data document=], one can express multiple proofs in an [=list=] as shown in [[[#example-a-proof-set-in-a-data-document]]] and [[[#example-a-proof-chain-in-a-data-document]]]. The elements of this [=list=] are members of a [=proof set=] and, optionally, a [=proof chain=]. The purpose of this section is to explain the intended use of each of these features and, in particular, their differing security properties. These differing security properties lead to differences in the processing in section [[[#add-proof-set-chain]]].

This section represents [=secured data documents=], including their proofs, in an abbreviated manner so that the important security properties can be observed.

Consider a scenario with three signatories: a CEO, a CFO, and a VP of Engineering. Each will need to have a public key and secret key pair for signing a document. We denote the secret/public keys of each of these signatories by secretCEO/publicCEO, secretCFO/publicCFO, and secretVPE/publicVPE, respectively.

When constructing a [=proof set=] where each of the signatories signs an |inputDocument| without concern, we construct a proof symbolically as:

{
  "type": "DataIntegrityProof",
  "cryptosuite": "eddsa-jcs-2022",
  "created": "2023-03-05T19:23:24Z",
  "proofPurpose": "assertionMethod",
  "verificationMethod": publicCEO,
  "proofValue": signature(secretCEO, inputDocument)
}
      

Where publicCEO is used as a placeholder for a reference that resolves to the CEO's public key and signature(`secretKey`, `inputDocument`) denotes the computation of a digital signature by a particular data integrity cryptosuite using a particular secret key over a particular document. The `type`, `cryptosuite`, `created`, and `proofPurpose` attributes do not factor into our discussion so we will omit them. In particular, below we show all the proofs in a [=proof set=] on a document that has been signed by the VP of Engineering, the CFO, and the CEO:

{
  // Remainder of secured data document not shown (above)
  "proof": [{
    "verificationMethod": publicVPE,
    "proofValue": signature(secretVPE, inputDocument)
  }, {
    "verificationMethod": publicCFO,
    "proofValue": signature(secretCFO, inputDocument)
  }, {
    "verificationMethod": publicCEO,
    "proofValue": signature(secretCEO, inputDocument)
  }]
}
      

A [=holder=] or any other intermediary receiving a [=secured data document=] containing a [=proof set=] is able to remove any of the `proof` values within the set prior to passing it on to another entity and the [=secured data document=] will still verify. This might or might not have been the intent. For the signatories sending a birthday card to a valued employee, using a [=proof set=] is probably fine. If we are trying to model a business process where approvals ascend the company hierarchy, this would not be ideal, since any intermediary could remove signatures from the [=proof set=] and still have it verify; for instance, in the example below, it looks like the CFO and CEO approved something without the VP of Engineering's concurrence.

{
  // Remainder of secured data document not shown (above)
  "proof": [{
    "verificationMethod": publicCFO,
    "proofValue": signature(secretCFO, inputDocument)
  }, {
    "verificationMethod": publicCEO,
    "proofValue": signature(secretCEO, inputDocument)
  }]
}
      

It is possible to introduce a dependency between [=proofs=] in a [=proof set=] by setting the `id` property of each proof such that another proof can reference it. In other words, a dependent proof will be referenced by other relying proofs by using the `previousProof` property. Such dependency chains can have arbitrary depth. The intent of such a [=proof chain=] is to model an approval chain in a business process or a notary witnessing analog signatures.

The examples below demonstrate how a [=proof chain=] can be constructed when the VP of Engineering signs off on the document first; based on the VP of Engineering's signature and a review, the CFO then signs off on the document; and finally, based on both prior signatures and a review, the CEO signs off on the document. Since others will be referring to the VP of Engineering's signature, we need to add an `id` to the proof. First the VP of Engineering signs the [=input document=]:

{
  // Remainder of secured data document not shown (above)
  "proof": {
    "id": "urn:proof-1",
    "verificationMethod": publicVPE,
    "proofValue": signature(secretVPE, inputDocument)
  }
}
      

Next, the CFO receives the document, verifies that the VP of Engineering signed it, and signs it based on a review and on the signature of the VP of Engineering. For this, we need to set up the [=proof chain=] by indicating a dependency on the proof in the document just received. We do this by setting the `previousProof` property of the second proof to the value `urn:proof-1`, which "binds" the second proof to the first proof, which is then signed. The following example shows how the dependency on the first proof is created:

{
  // Remainder of secured data document not shown (above)
  "proof": [{
    "id": "urn:proof-1",
    "verificationMethod": publicVPE,
    "proofValue": signature(secretVPE, inputDocument)
  }, {
    "id": "urn:proof-2",
    "verificationMethod": publicCFO,
    "previousProof": "urn:proof-1",
    "proofValue": signature(secretCFO, inputDocumentWithProof1)
  }]
}
      

Now, when the CEO verifies the received [=secured data document=] with the above [=proof chain=], they will check that the CFO signed based on the signature of the VP of Engineering. First, they will check the proof with an `id` property whose value is `urn:proof-1` against the public key of the VP of Engineering. Note that this proof is over the original document.

Next, the CEO will check the proof with an `id` property whose value is `urn:proof-2` against the public key of the CFO. However, to make sure that the CFO signed the document with proof that the VP of Engineering had already signed, we verify this proof over the combination of the document and `urn:proof-1`. If verification is successful, the CEO signs, producing a proof over the document which includes `urn:proof-1` and `urn:proof-2`. The final [=proof chain=] looks like this:

{
  // Remainder of secured data document not shown (above)
  "proof": [{
    "id": "urn:proof-1",
    "verificationMethod": publicVPE,
    "proofValue": signature(secretVPE, inputDocument)
  }, {
    "id": "urn:proof-2",
    "verificationMethod": publicCFO,
    "previousProof": "urn:proof-1",
    "proofValue": signature(secretCFO, inputDocumentWithProof1)
  }, {
    "id": "urn:proof-3",
    "verificationMethod": publicCEO,
    "previousProof": "urn:proof-2",
    "proofValue": signature(secretCEO, inputDocumentWithProof2)
  }]
}
      

The recipient of this [=secured data document=] then validates it in a similar way, checking each proof in the chain.

Revision History

This section contains the substantive changes that have been made to this specification over time.

Changes since the Second Candidate Recommendation:

Changes since the First Candidate Recommendation:

Changes since the First Public Working Draft:

Acknowledgements

Work on this specification has been supported by the Rebooting the Web of Trust community facilitated by Christopher Allen, Shannon Appelcline, Kiara Robles, Brian Weller, Betty Dhamers, Kaliya Young, Manu Sporny, Drummond Reed, Joe Andrieu, Heather Vescent, Kim Hamilton Duffy, Samantha Chase, Andrew Hughes, Will Abramson, Erica Connell and Eric Schuh. The participants in the Internet Identity Workshop, facilitated by Phil Windley, Kaliya Young, Doc Searls, and Heidi Nobantu Saul, also supported the refinement of this work through numerous working sessions designed to educate about, debate on, and improve this specification.

The Working Group also thanks our Chair, Brent Zundel, our ex-Chair, Kristina Yasuda, as well as our W3C Staff Contact, Ivan Herman, for their expert management and steady guidance of the group through the W3C standardization process.

Portions of the work on this specification have been funded by the United States Department of Homeland Security's Science and Technology Directorate under contracts 70RSAT20T00000029, 70RSAT21T00000016, 70RSAT23T00000005, 70RSAT20T00000010/P00001, 70RSAT20T00000029, 70RSAT21T00000016/P00001, 70RSAT23T00000005, 70RSAT23C00000030, 70RSAT23R00000006, 70RSAT24T00000011, and the National Science Foundation through NSF 22-572. The content of this specification does not necessarily reflect the position or the policy of the U.S. Government and no official endorsement should be inferred.

The Working Group would like to thank the following individuals for reviewing and providing feedback on the specification (in alphabetical order by last name or their GitHub handle if a name was not provided):

Will Abramson, Mahmoud Alkhraishi, Christopher Allen, Joe Andrieu, Bohdan Andriyiv, George Aristy, Anthony, Greg Bernstein, Bob420, Sarven Capadisli, Melvin Carvalho, David Chadwick, Gabe Cohen, Matt Collier, Sebastian Crane, Kim Hamilton Duffy, Snorre Lothar von Gohren Edwin, Veikko Eeva, Eric Elliott, Raphael Flechtner, Julien Fraichot, Benjamin Goering, Kyle Den Hartog, Joseph Heenan, Helge Krueger, Ivan Herman, Michael Herman, Alen Horvat, Anil John, Andrew Jones, Michael B. Jones, Rieks Joosten, Gregory K., Gregg Kellogg, Filip Kolarik, David I. Lehn, Charles E. Lehner, Christine Lemmer-Webber, Eric Lim, Dave Longley, Tobias Looker, Jer Miller, nightpool, Bert Van Nuffelen, Luis Osta, Nate Otto, George J. Padayatti, Addison Phillips, Mike Prorock, Brian Richter, Anders Rundgren, Eugeniu Rusu, Markus Sabadello, silverpill, Wesley Smith, Manu Sporny, Orie Steele, Patrick St-Louis, Henry Story, Oliver Terbu, Ted Thibodeau Jr., John Toohey, Mike Varley, Jeffrey Yasskin, Kristina Yasuda, Benjamin Young, Dmitri Zagidulin, and Brent Zundel.