Skip to content

Commit

Permalink
Merge branch 'correcting-cryptosuites'
Browse files Browse the repository at this point in the history
  • Loading branch information
iherman committed Jun 7, 2024
2 parents 20ccccd + 4120b7e commit e84abce
Show file tree
Hide file tree
Showing 16 changed files with 747 additions and 182 deletions.
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
# Data Integrity algorithms for RDF Datasets — Proof of concepts implementation

This is a proof-of-concept implementation (in Typescript) of the [Verifiable Credentials Data Integrity (DI)](https://www.w3.org/TR/vc-data-integrity/) specification of the W3C. The DI specification is primarily aimed at [Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/) (i.e., JSON-LD based data structures to express credentials) but the approach is such that it can be used for any kind of RDF Datasets. This implementation does that.
This is a proof-of-concept implementation (in Typescript) of the [Verifiable Credentials Data Integrity (DI)](https://www.w3.org/TR/vc-data-integrity/) specification of the W3C.
The DI specification is primarily aimed at [Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/) (i.e., JSON-LD based RDF Datasets to express credentials), but the approach is general enough for any kind of RDF Datasets.
This implementation implements that.

It is proof-of-concepts, because, primarily at validation time it doesn't do all the checks that the DI specification describes, and have not (yet) been cross-checked with other DI implementations. What it proves, however, is that the DI specification may indeed be used to provide a proof for an RDF Dataset in the form of a separate "Proof Graph", i.e., an RDF Graph containing a signature that can be separated by a verifier.
It is proof-of-concepts, meaning that it is not production ready, and there are also minor discrepancies with the official specification. These are:

- Primarily at validation time, it doesn't do all the checks that the DI specification describes.
- In contrast with the DI specification, the Verification Method (ie, the public key) is expected to be be present in the input. In other words, the package does not retrieve the keys through a URL, it looks for the respective quads in the input dataset.
- Although it implements the the [EdDSA](https://www.w3.org/TR/vc-di-eddsa/) and [ECDSA](https://www.w3.org/TR/vc-di-ecdsa/), the Multikey encoding of the latter is not (yet?) conform to the specification.
The difference is that the Multikey encoding is used for the uncompressed crypto key as opposed to the compressed one (I have not yet found a reliable package to uncompress a compressed key).
- It has not (yet) been cross-checked with other DI implementations and, in general, should be much more thoroughly tested.

There is also a missing feature in the DI specification. In the setting of a Verifiable Credential there is a natural "anchor" Resource that is used to connect the input dataset with the proof.
This is generally not true (see, e.g. [separate discussion](https://github.com/w3c/vc-data-model/issues/1248)) and, in this implementation, it must be provided explicitly.

What the implementation proves, however, is that the _DI specification may indeed be used, with minor adjustment on the "anchor", to provide a proof for an RDF Dataset in the form of a separate "Proof Graph"_, i.e., an RDF Graph containing a signature that can be separated by a verifier.

## Some details

The steps for signature follow the "usual" approach for signing data, namely:

1. The input RDF Dataset is canonicalized, using the [RDF Dataset Canonicalization](https://www.w3.org/TR/rdf-canon/), as defined by the W3C.
2. The resulting canonical N-Quads are sorted, and hashed to yield a canonical hash of the Dataset (the W3C specification relies on SHA-256 for hashing by default, which is used here).
3. The hash is signed using a secret key. The signature value is stored as a base64url value following the [Multibase](https://datatracker.ietf.org/doc/draft-multiformats-multibase) format.
4. A separate "proof graph" is generated, that includes the signature value, some basic metadata, and the public key of for the signature, stored in [JWK format](https://www.rfc-editor.org/rfc/rfc7517).
3. A "proof option graph" is created, which includes crypto keys description and some metadata. The key is stored in [JWK format](https://www.rfc-editor.org/rfc/rfc7517) or in Multikey: the former is used for RSA keys (for which no Multikey encoding has been specified) and the latter is used for ECDSA and EdDSA, as required by the respective cryptosuite specifications. That separate graph is also canonicalized, sorted, and hashed.
4. The the two hash values are concatenated (in the original dataset and proof option graph order), and signed using a secret key. The signature value is stored as a base64url value following the [Multibase](https://datatracker.ietf.org/doc/draft-multiformats-multibase) format, and its value is added to the proof option graph (turning it into a proof graph).

The package has separate API entries to generate, and validate, such proof graphs. It is also possible, following the DI spec, to provide "embedded" proofs, i.e., a new dataset, containing the original data, as well as the proof graph(s), each as a separate graph within the dataset. If a separate "anchor" resource is provided, then this new dataset will also contain additional RDF triples connecting the anchor to the proof graphs.
The package has separate API entries to generate, and validate, such proof graphs. It is also possible, following the DI spec, to provide "embedded" proofs, i.e., a new dataset, containing the original data, as well as the proof graph(s), each as a separate graph within an RDF dataset. If a separate "anchor" resource is provided, then this new dataset will also contain additional RDF triples connecting the anchor to the proof graphs.

The crypto layer for the package relies on the Web Crypto API specification, and its implementation in `node.js` or `deno`. Accordingly, the following crypto algorithms are available for this implementation

- [ECDSA](https://w3c.github.io/webcrypto/#ecdsa)
- [RSA-PSS](https://w3c.github.io/webcrypto/#rsa-pss)
- [RSASSA-PKCS1-v1_5](https://w3c.github.io/webcrypto/#rsassa-pkcs1)
- EDDSA, a.k.a. Ed25519, not official in the WebCrypto specification, but implemented both in `node.js` and `deno`. See also the [EdDSA cryptosuite](https://www.w3.org/TR/vc-di-eddsa/) specification.
- [ECDSA](https://w3c.github.io/webcrypto/#ecdsa). See also the [ECDSA cryptosuite](https://www.w3.org/TR/vc-di-ecdsa/) specification.
- [RSA-PSS](https://w3c.github.io/webcrypto/#rsa-pss). No DI cryptosuite specification exists.
- [RSASSA-PKCS1-v1_5](https://w3c.github.io/webcrypto/#rsassa-pkcs1). No DI cryptosuite specification exists.

Although not strictly necessary for this package, a separate method is available as part of the API to generate cryptography keys for one of these three algorithms. Note that only ECDSA is part of the [VC Working Groups' specification](https://www.w3.org/TR/vc-di-ecdsa/), identified by the cryptosuite name `ecdsa-2022`; the other two are non-standard, and are identified with the temporary cryptosuite name of `rdfjs-di-rsa-pss` and `rdfjs-di-rsa-ssa`, respectively.
Although not strictly necessary for this package, a separate method is available as part of the API to generate cryptography keys for one of these four algorithms.
The first two algorithms are specified by cryptosuites, identified as `eddsa-rdfc-2022` and `ecdsa-rdfc-2019`, respectively.
The other two are non-standard, and are identified with the temporary cryptosuite name of `rsa-pss-rdfc-ih` and `rsa-ssa-rdfc-ih`, respectively. Note that there is no Multikey encoding for RSA keys, so the keys are stored in JWK format as a literal with an `rdf:JSON` datatype.

For more details, see:

Expand Down
66 changes: 35 additions & 31 deletions examples/small_with_proofs.ttl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## The original dataset starts here
@prefix sec: <https://w3id.org/security#>.
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
Expand All @@ -10,6 +11,7 @@
<file:///small.ttl> foaf:primaryTopic <https://iherman.github.io/rdfjs-c14n/>;
dc:issued "2024-02-14T13:21:08.700Z"^^xsd:dateTime;
foaf:maker <https://www.ivan-herman.net/foaf#me>;
# These statements have been added by the program to "anchor" the resource to its various proofs
sec:proof _:b0, _:b1, _:b2.

<https://iherman.github.io/rdfjs-c14n/> foaf:maker <https://www.ivan-herman.net/foaf#me>;
Expand Down Expand Up @@ -45,44 +47,46 @@ _:n3-0 a doap:Version;
_:n3-1 a doap:GitRepository;
doap:location <https://github.com/iherman/rdfjs-c14n>.

# The original dataset ends here, the rest are the various proofs
# Signature based on ECDSA
_:b0 {
<urn:uuid:70b27fe7-89fc-4438-b271-4620b99c61a2> a sec:DataIntegrityProof;
sec:cryptosuite "rdfjs-di-rsa-pss";
sec:verificationMethod <urn:uuid:1724d7fb-52fd-4426-b2f5-8ca935014460>;
sec:proofValue "unTRxbY69Jk5C0G3QO93sAISre0pW77Ws_vTUbXbDzDRuptW_rN4Ps5lNlsrxPRBPtJZIG05LKMCeXYJMEcMP30Lje0M2bIiRSEQcr1ucgY-PEmPYXMlOQhuuXUisUzOgaUdD8OPdajpz2JIhj_lFFXgNCiySEsFkxXzT4nnQg0Vi9b-KS8Raqp097glcT-BjGdx4DBbizVocp6XjAvYoBn3tewpLsCeuqr1Nook2bs6Uff-1veDjZgZHtwB0ImRvvYGI499lO9i7j39WxGMqgS7O1VNsAJMAozSAz2Z_Sk3uOqezJwZbDOygxE2RuZ9gMPGLS4M9W1CrkM9heYUD-w";
sec:created "2024-03-07T11:52:00.821Z"^^xsd:dateTime;
sec:proofPurpose sec:authenticationMethod, sec:assertionMethod.
<urn:uuid:9e8dfbea-3af8-4b91-a7a4-0586338ed8e0> a sec:DataIntegrityProof;
sec:verificationMethod <urn:uuid:20fe3a93-f5dd-417a-83b8-d5f518d8cbdd>;
sec:created "2024-06-07T12:46:16.794Z"^^xsd:dateTime;
sec:proofPurpose sec:authenticationMethod, sec:assertionMethod;
sec:cryptosuite "ecdsa-rdfc-2019";
sec:proofValue "u49pAWDiPa6WEcEpipXYwQ9LBGvVV6QBLR6XyPSQOGycAYQzk6IzGaKZ2tmYgo8LFectiL-WTH3ryogVIGr_HWQ".

<urn:uuid:1724d7fb-52fd-4426-b2f5-8ca935014460> a sec:JsonWebKey;
sec:publicKeyJwk "{\"key_ops\":[\"verify\"],\"ext\":true,\"kty\":\"RSA\",\"n\":\"zX1p6Rl0kTtbFAiISqXQbT9U6kqqIFRfualifsLA5ZNFQMDvuw3cqYUqzIyHAVYV0Bps-mXmwFVjsetQIzqhE0X2VhJ8wBS-bGxm2_E0rQ9y35mN_dWfnVhJYtONrLjkduAk04Xouws60X2ye0QHhG63j0CLNj6bqJQ_fOE_ankHxjGnZ7H7tPIeJfj9Md2GMx98BEr_8iwFXPJ5zT3_ET1zdPUGJV3r6pTOTu5H6sH5457TxCeMIIZfQ3hn3f5lz_5JL5ahtIqZ1BgaAYt5bgg63oqmn67V1fmJ08tx1LT6BbGkt1WQBv8aNgm3Z5ztLWC8MJiqgeFpyQFUOIkVCQ\",\"e\":\"AQAB\",\"alg\":\"PS256\"}"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON>;
<urn:uuid:20fe3a93-f5dd-417a-83b8-d5f518d8cbdd> a sec:Multikey;
sec:controller <https://www.ivan-herman.net#me>;
sec:expires "2050-02-24T00:00:00Z"^^xsd:dateTime
sec:expires "2055-02-24T00:00:00Z"^^xsd:dateTime;
sec:publicKeyMultibase "z4oJ8afhsNVicQJMz5J46thJ84ZBYor4bFL6RBq6F9PdGaV7nUykNfGAQL5PkdqS5THaTVhQQ3oyAtgQsFkY4xNETTxMC"
}

# Signature based on EdDSA, a.k.a. Ed25519
_:b1 {
<urn:uuid:d86aa537-c465-4882-8c6a-9f81533cce69> a sec:DataIntegrityProof;
sec:cryptosuite "ecdsa-2022";
sec:verificationMethod <urn:uuid:af11ce93-eb64-4e90-aedc-d8a1ab4cc1d2>;
sec:proofValue "uUu0Ht5CMJpKN9ChSlKSAcccKA0Ym14v3jZGXSRBSpMZ82U36AuGaWo3V31M2magZG9kgKtSLhNZRoKg-b8paXg";
sec:created "2024-03-07T11:52:00.820Z"^^xsd:dateTime;
<urn:uuid:b598fad9-83bf-4690-bf20-951e229d7f74> a sec:DataIntegrityProof;
sec:verificationMethod <urn:uuid:0e977e73-183c-4fa2-ace1-eadbe6e3fcb5>;
sec:created "2024-06-07T12:46:16.795Z"^^xsd:dateTime;
sec:proofPurpose sec:authenticationMethod, sec:assertionMethod;
sec:previousProof <urn:uuid:70b27fe7-89fc-4438-b271-4620b99c61a2>.

<urn:uuid:af11ce93-eb64-4e90-aedc-d8a1ab4cc1d2> a sec:JsonWebKey;
sec:publicKeyJwk "{\"key_ops\":[\"verify\"],\"ext\":true,\"kty\":\"EC\",\"x\":\"LHGayjy__zWhz14u7vWyGbPWkXNdJN1AnhKiQTv3uK8\",\"y\":\"9T4ThlFhLi84d3LOaOkvrzLrr_EEczB0sIv3S3vzdd8\",\"crv\":\"P-256\"}"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON>;
sec:controller <https://www.ivan-herman.net#me>;
sec:expires "2055-02-24T00:00:00Z"^^xsd:dateTime
sec:cryptosuite "eddsa-rdfc-2022";
sec:proofValue "uncVUipaxBRCseZQ5WYoP_6yrCzQ-Uktv926QMoVNir5_bcfewCYys7CIOet3XIxoL9zE-M8Hu61mfuziLYpyCA".
<urn:uuid:0e977e73-183c-4fa2-ace1-eadbe6e3fcb5> a sec:Multikey;
sec:controller <https://example.org/key/#ivan_eddsa>;
sec:expires "2055-02-24T00:00:00Z"^^xsd:dateTime;
sec:publicKeyMultibase "z6MkqPUPcdvvixcfgGqqEZJ4WZTiDwaCsqF8jHqR5UZA2iae"
}

# Signature based on RSA
_:b2 {
<urn:uuid:00bc3d69-7d7f-4b92-88fe-37c4a3da7349> a sec:DataIntegrityProof;
sec:cryptosuite "ecdsa-2022";
sec:verificationMethod <urn:uuid:382c8f68-3578-4314-a7dc-12bdc3767dd3>;
sec:proofValue "ui7tvde12p6kFPzzKQHV4ARYsxY4xXSpHrEC6YSecboeLtUkRKGUh0qgYWy3whj3Lfl0Wj906sLt80hy5dx7nkw";
sec:created "2024-03-07T11:52:00.821Z"^^xsd:dateTime;
<urn:uuid:aebb1f96-0292-4d9a-b82c-9719e11b75fd> a sec:DataIntegrityProof;
sec:verificationMethod <urn:uuid:290fd98d-1df8-482a-a51c-2310b9facb50>;
sec:created "2024-06-07T12:46:16.796Z"^^xsd:dateTime;
sec:proofPurpose sec:authenticationMethod, sec:assertionMethod;
sec:previousProof <urn:uuid:d86aa537-c465-4882-8c6a-9f81533cce69>.

<urn:uuid:382c8f68-3578-4314-a7dc-12bdc3767dd3> a sec:JsonWebKey;
sec:publicKeyJwk "{\"key_ops\":[\"verify\"],\"ext\":true,\"kty\":\"EC\",\"x\":\"qtQA8VF1KFsvSfq1BkZP0rODQcQF8x-uL1BXgNcKWIY\",\"y\":\"smHmMXIf9FlkY0pbfrZfuCg0BMviSj8IjMhzTPJehLI\",\"crv\":\"P-256\"}"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON>;
sec:controller <https://example.org/key/#alan>
sec:cryptosuite "rsa-pss-rdfc-ih";
sec:proofValue "uo5HPJflrCyL_QujeHTJa_v1hsZ8HtoPRLjS5-ZJTZlOKIZ-ealplEYRNWasYlvU9ip30nO7aE6VrQ2oixLmEfcgK6wUMr7HKxgynowQRSSU39TEbvMNf-lcU-GzW6QBwxV5lteJDcEkNgC4C6iyEFQfAd0J45m4_y8vQzXOqCYB-his99704AcolDbqtNJ_P8y0dxpciTwdVmrR0NPDfpq4kQYHdiCUWPby4zVRePep2Lo5SzA2jZE060UFuOO1ROguqhtAo-98cYTmI5oj66KDfoC0nOnHdP4lPm97Pxgy0XRcbTIIPBD_51JRYJ1W4zkjQkBO-4eAoxbkOMVbPMg".
<urn:uuid:290fd98d-1df8-482a-a51c-2310b9facb50> a sec:JsonWebKey;
sec:controller <https://www.ivan-herman.net#me>;
sec:expires "2050-02-24T00:00:00Z"^^xsd:dateTime;
sec:publicKeyJwk "{\"key_ops\":[\"verify\"],\"ext\":true,\"kty\":\"RSA\",\"n\":\"zX1p6Rl0kTtbFAiISqXQbT9U6kqqIFRfualifsLA5ZNFQMDvuw3cqYUqzIyHAVYV0Bps-mXmwFVjsetQIzqhE0X2VhJ8wBS-bGxm2_E0rQ9y35mN_dWfnVhJYtONrLjkduAk04Xouws60X2ye0QHhG63j0CLNj6bqJQ_fOE_ankHxjGnZ7H7tPIeJfj9Md2GMx98BEr_8iwFXPJ5zT3_ET1zdPUGJV3r6pTOTu5H6sH5457TxCeMIIZfQ3hn3f5lz_5JL5ahtIqZ1BgaAYt5bgg63oqmn67V1fmJ08tx1LT6BbGkt1WQBv8aNgm3Z5ztLWC8MJiqgeFpyQFUOIkVCQ\",\"e\":\"AQAB\",\"alg\":\"PS256\"}"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON>
}

155 changes: 155 additions & 0 deletions lib/base58/baseN.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Base-N/Base-X encoding/decoding functions.
*
* Original implementation from base-x:
* https://github.com/cryptocoinjs/base-x
*
* Which is MIT licensed:
*
* The MIT License (MIT)
*
* Copyright base-x contributors (c) 2016
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
// baseN alphabet indexes
const _reverseAlphabets = {};

/**
* BaseN-encodes a Uint8Array using the given alphabet.
*
* @param {Uint8Array} input - The bytes to encode in a Uint8Array.
* @param {string} alphabet - The alphabet to use for encoding.
* @param {number} maxline - The maximum number of encoded characters per line
* to use, defaults to none.
*
* @returns {string} The baseN-encoded output string.
*/
export function encode(input, alphabet, maxline) {
if(!(input instanceof Uint8Array)) {
throw new TypeError('"input" must be a Uint8Array.');
}
if(typeof alphabet !== 'string') {
throw new TypeError('"alphabet" must be a string.');
}
if(maxline !== undefined && typeof maxline !== 'number') {
throw new TypeError('"maxline" must be a number.');
}
if(input.length === 0) {
return '';
}

let output = '';

let i = 0;
const base = alphabet.length;
const first = alphabet.charAt(0);
const digits = [0];
for(i = 0; i < input.length; ++i) {
let carry = input[i];
for(let j = 0; j < digits.length; ++j) {
carry += digits[j] << 8;
digits[j] = carry % base;
carry = (carry / base) | 0;
}

while(carry > 0) {
digits.push(carry % base);
carry = (carry / base) | 0;
}
}

// deal with leading zeros
for(i = 0; input[i] === 0 && i < input.length - 1; ++i) {
output += first;
}
// convert digits to a string
for(i = digits.length - 1; i >= 0; --i) {
output += alphabet[digits[i]];
}

if(maxline) {
const regex = new RegExp('.{1,' + maxline + '}', 'g');
output = output.match(regex).join('\r\n');
}

return output;
}

/**
* Decodes a baseN-encoded (using the given alphabet) string to a
* Uint8Array.
*
* @param {string} input - The baseN-encoded input string.
* @param {string} alphabet - The alphabet to use for decoding.
*
* @returns {Uint8Array} The decoded bytes in a Uint8Array.
*/
export function decode(input, alphabet) {
if(typeof input !== 'string') {
throw new TypeError('"input" must be a string.');
}
if(typeof alphabet !== 'string') {
throw new TypeError('"alphabet" must be a string.');
}
if(input.length === 0) {
return new Uint8Array();
}

let table = _reverseAlphabets[alphabet];
if(!table) {
// compute reverse alphabet
table = _reverseAlphabets[alphabet] = [];
for(let i = 0; i < alphabet.length; ++i) {
table[alphabet.charCodeAt(i)] = i;
}
}

// remove whitespace characters
input = input.replace(/\s/g, '');

const base = alphabet.length;
const first = alphabet.charAt(0);
const bytes = [0];
for(let i = 0; i < input.length; i++) {
const value = table[input.charCodeAt(i)];
if(value === undefined) {
return;
}

let carry = value;
for(let j = 0; j < bytes.length; ++j) {
carry += bytes[j] * base;
bytes[j] = carry & 0xff;
carry >>= 8;
}

while(carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}

// deal with leading zeros
for(let k = 0; input[k] === first && k < input.length - 1; ++k) {
bytes.push(0);
}

return new Uint8Array(bytes.reverse());
}
4 changes: 4 additions & 0 deletions lib/base58/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// <reference types="node" />

export function encode(input: Uint8Array, maxline?: number): string;
export function decode(input: string): Uint8Array;
18 changes: 18 additions & 0 deletions lib/base58/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*!
* Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
*/
import {
encode as _encode,
decode as _decode
} from './baseN.js';

// base58 characters (Bitcoin alphabet)
const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

export function encode(input, maxline) {
return _encode(input, alphabet, maxline);
}

export function decode(input) {
return _decode(input, alphabet);
}
Loading

0 comments on commit e84abce

Please sign in to comment.