Code Monkey home page Code Monkey logo

ssi's Introduction

SSI

The SSI library provides a simple and modular API to sign and verify claims exchanged between applications using Decentralized Identifiers (DIDs). SSI is embedded in the cross-platform didkit library as a core dependency.

This library supports the two main families of verifiable claims:

Basic Usage

SSI provides various functions to parse, verify, create and sign various kind of claims. This section shows how to use these functions in combination with JSON Web Signatures (or Tokens) and Verifiable Credentials.

Verification

The simplest type of claim to load and verify is probably JSON Web Signatures (JWSs), often use to encode JSON Web Tokens (JWTs). To represent such claims SSI provides the CompactJWSString type representing a JWS in compact textual form. One can load a JWS using from_string and verify it using verify.

use ssi::prelude::*;

// Load a JWT from the file system.
let jwt = CompactJWSString::from_string(
  std::fs::read_to_string("examples/files/claims.jwt")
  .expect("unable to load JWT")
).expect("invalid JWS");

// Setup a verification method resolver, in charge of retrieving the
// public key used to sign the JWT.
// Here we use the example `ExampleDIDResolver` resolver, enabled with the
// `example` feature.
let vm_resolver = ExampleDIDResolver::default().into_vm_resolver::<AnyJwkMethod>();

// Verify the JWT.
assert!(jwt.verify(&vm_resolver).await.expect("verification failed").is_ok())

Verifiable Credentials

Verifiable Credential are much more complex as they require interpreting the input claims and proofs, such as Data-Integrity proofs as Linked-Data using JSON-LD. This operation is highly configurable. SSI provides functions exposing various levels of implementation details that you can tweak as needed. The simplest of them is any_credential_from_json_str that will simply load a VC from a string, assuming it is signed using any Data-Integrity proof supported by SSI.

use ssi::prelude::*;

let vc = ssi::claims::vc::v1::data_integrity::any_credential_from_json_str(
  &std::fs::read_to_string("examples/files/vc.jsonld")
  .expect("unable to load VC")
).await.expect("invalid VC");

// Setup a verification method resolver, in charge of retrieving the
// public key used to sign the JWT.
let vm_resolver = ExampleDIDResolver::default().into_vm_resolver();

assert!(vc.verify(&vm_resolver).await.expect("verification failed").is_ok());

Signature & Custom Claims

In the previous section we have seen how to load and verify arbitrary claims. This section shows how to create and sign custom claims. With SSI, any Rust type can serve as claims as long as it complies to certain conditions such as implementing serialization/deserialization functions using serde. Don't forget to enable the derive feature for serde.

In the following example, we create a custom type MyClaims and sign it as a JWT.

use serde::{Serialize, Deserialize};
use ssi::prelude::*;

// Defines the shape of our custom claims.
#[derive(Serialize, Deserialize)]
pub struct MyClaims {
  name: String,
  email: String
}

// Create JWT claims from our custom ("private") claims.
let claims = JWTClaims::from_private_claims(MyClaims {
  name: "John Smith".to_owned(),
  email: "[email protected]".to_owned()
});

// Create a random signing key, and turn its public part into a DID URL.
let mut key = JWK::generate_p256(); // requires the `p256` feature.
let did = DIDJWK::generate_url(&key.to_public());
key.key_id = Some(did.into());

// Sign the claims.
let jwt = claims.sign(&key).await.expect("signature failed");

// Create a verification method resolver, which will be in charge of
// decoding the DID back into a public key.
let vm_resolver = DIDJWK.into_vm_resolver::<AnyJwkMethod>();

// Verify the JWT.
assert!(jwt.verify(&vm_resolver).await.expect("verification failed").is_ok());

// Print the JWT.
println!("{jwt}")

Verifiable Credential

We can use a similar technique to sign a VC with custom claims. The SpecializedJsonCredential type provides a customizable implementation of the VC data-model where you can set the credential type yourself.

use static_iref::uri;
use serde::{Serialize, Deserialize};
use ssi::prelude::*;

// Defines the shape of our custom claims.
#[derive(Serialize, Deserialize)]
pub struct MyCredentialSubject {
  #[serde(rename = "https://example.org/#name")]
  name: String,

  #[serde(rename = "https://example.org/#email")]
  email: String
}

let credential = ssi::claims::vc::v1::JsonCredential::<MyCredentialSubject>::new(
  Some(uri!("https://example.org/#CredentialId").to_owned()), // id
  uri!("https://example.org/#Issuer").to_owned().into(), // issuer
  DateTime::now(), // issuance date
  vec![MyCredentialSubject {
    name: "John Smith".to_owned(),
    email: "[email protected]".to_owned()
  }]
);

// Create a random signing key, and turn its public part into a DID URL.
let key = JWK::generate_p256(); // requires the `p256` feature.
let did = DIDJWK::generate_url(&key.to_public());

// Create a verification method resolver, which will be in charge of
// decoding the DID back into a public key.
let vm_resolver = DIDJWK.into_vm_resolver();

// Create a signer from the secret key.
// Here we use the simple `SingleSecretSigner` signer type which always uses
// the same provided secret key to sign messages.
let signer = SingleSecretSigner::new(key.clone());

// Turn the DID URL into a verification method reference.
let verification_method = did.into_iri().into();

// Automatically pick a suitable Data-Integrity signature suite for our key.
let cryptosuite = AnySuite::pick(&key, Some(&verification_method))
  .expect("could not find appropriate cryptosuite");

let vc = cryptosuite.sign(
  credential,
  AnyInputContext::default(),
  &vm_resolver,
  &signer,
  ProofConfiguration::from_method(verification_method)
).await.expect("signature failed");

It is critical that custom claims can be interpreted as Linked-Data. In the above example this is done by specifying a serialization URL for each field of MyCredentialSubject. This can also be done by creating a custom JSON-LD context and embed it to credential using either SpecializedJsonCredential's context field or leveraging its context type parameter.

Features

Security Audits

ssi has undergone the following security reviews:

Testing

Testing SSI requires the RDF canonicalization test suite, which is embedded as a git submodule.

$ git submodule update --init
$ cargo test --workspace

Contribution

We are setting up a process to accept contributions. Please feel free to open issues or PRs in the interim, but we cannot merge external changes until this process is in place.

ssi's People

Contributors

bumblefudge avatar charlesshuller avatar chunningham avatar clehner avatar cobward avatar fairingrey avatar ivan770 avatar kevinz917 avatar krhoda avatar mikelodder7 avatar mrappard avatar obstropolos avatar rschulman avatar sbihel avatar shoito avatar taylorbeeston avatar theosirian avatar timothee-haudebourg avatar tristanmiller-spruceid avatar vdods avatar voronar avatar w4ll3 avatar wyc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ssi's Issues

Asynchronous API for WASM

#47 introduces a dependency on async_std, as json-ld's JSON-LD expansion implementation is asynchronous. We keep the existing synchronous API by using block_on internally. But for WASM, block_on does not a return a value, and requires the future to be static, since it uses wasm_bindgen_futures::spawn_local. This is a problem for using #47 with WASM (#52) while keeping ssi's blocking API.

I see the following options to proceed:

  • Make block_on/spawn_local work somehow, maybe using global static storage and/or message passing.
  • Make json-ld support synchronous expansion.
  • Implement synchronous expansion in some limited way instead of using json-ld.
  • Require JSON-LD documents to be already expanded for generating/verifying ld-proofs. Add an asynchronous function to perform the expansion.
  • Make generate and verify functions asynchronous.

The last option might be the most sensible. I think we want to support asynchronous usage anyway, e.g. to be able to make HTTP requests during expansion and verification.

wasm-bindgen-futures can convert a Rust Future to a JS Promise. So if ssi uses async functions, DIDKit's WASM package should be able to wrap those with Promises.

Non-WASM use of ssi could continue to operate synchronously by using block_on in the application code. I think that would work for the DIDKit CLI and FFIs.
When we start to do actual I/O then we can update the FFIs to integrate with native event loops.

1 test pending for vc-test-suite?

@clehner, phenomenal work on the test suite. I was running the test suite and found that only a single test was pending under the optional section. Is this intentional or is there a different mode I should run the test suite in?

Screen Shot 2020-09-06 at 5 32 00 PM

Implement `did-web`

We believe that many users would like to use this to test out the library. Fortunately, it looks like we have much of the implementation in did-resolver as per the HTTP resolver!

Implement X.509-based DID method

We are encountering users who want to utilize traditional CA infrastructure in conjunction with DIDs/VCs. A DID method may be an appropriate way to ensure this interoperability.

I think an ultimate demo of this would be X.509-based DIDs talking to did:onion-based DIDs over TorGap as per spruceid/didkit#68

Possible examples of DIDs based on X.509:

did:x509:canonical:ccadb:example.com
did:x509:canonical:fcpca:website.gov
did:x509:md5:444bcb3a3fcf8389296c49467f27e1d6:server.corpinternal.com
did:x509:sha1:99b4251e2eee05d8292e8397a90165293d116028:server.corpinternal.com 
did:x509:sha2:2689367b205c16ce32ed4200942b8b8b1e262dfc70d9bc9fbc77c49699a4f1df:server.corpinternal.com

The finger/thumbprints (md5/sha1/sha2) can be defined as per:

Uncurated and undirected dump of prior and related work:
https://github.com/WebOfTrustInfo/rwot9-prague/blob/master/topics-and-advance-readings/X.509-DID-Method.md
https://www.researchgate.net/publication/342027346_Distributed-Ledger-based_Authentication_with_Decentralized_Identifiers_and_Verifiable_Credentials
https://hyperledger-fabric.readthedocs.io/en/release-2.2/identity/identity.html
https://arxiv.org/pdf/2003.05106.pdf
https://www.ndss-symposium.org/wp-content/uploads/diss2019_05_Lagutin_paper.pdf
https://github.com/WebOfTrustInfo/rwot1-sf/blob/master/draft-documents/Decentralized-Public-Key-Infrastructure-CURRENT.md
https://arxiv.org/pdf/2004.07063.pdf

This would be a good candidate specification for a CCG work item.

Check for changes in DID draft

The required context in did-core is now https://www.w3.org/ns/did/v1. Check if there are other changes in the data model that src/did.rs should be updated to reflect.

Support OS-provided crypto libraries/APIs

Re: spruceid/didkit#16, spruceid/didkit#16 (comment)

Performance: an OS may provide access to cryptographic hardware that is faster than using our own crypto dependencies on the CPU.

Security and interop: an OS may provide access to manage and use keys where the private key material is not exposed to the application (including ssi). This could be considered a special case of #53.

Using OS-provided crypto libraries could be done with feature flags as in #52, and/or or by detecting the library at runtime (dynamic linking), with fallback to our compiled code.

Compile-time JSON-LD context expansion

If we were to build a JSON-LD library, I wonder how tenable would be the idea of resolving and expanding contexts into code via metaprogramming prior to compilation. We could assume the JSON files are in hand--I wonder if there would be any benefits from a safety and performance perspective, and how much binary bloat such a flag might add. Just a thought--this is likely one of those "someday maybe" features that we revisit when the need actually arises.

cc @clehner

Support JSON Schema for VCs

A credential's credentialSchema may specify a URI for a JSON Schema file for validating the credential. (https://w3c.github.io/vc-data-model/#proofs-signatures)

A Rust library for JSON Schema is jsonschema.

jsonschema uses HTTP client library reqwest which pulls in a lot of dependencies (#18). Also, the requests are blocking and responses are not cached (Stranger6667/jsonschema-rs#75). But it looks like the resolver is not used internally to validation but only if the caller calls it. So this might not be a problem us, especially if reqwest is made optional (Stranger6667/jsonschema-rs#137), as we can do our own HTTP resolution (edit: maybe just using reqwest as well), e.g. alongside resolution for JSON-LD Context documents.

Another library is schemafy (mentioned in #21 (comment)) which generates types from schemas at compile time. But it doesn't have full support for the latest JSON Schema drafts. Its use may be limited here since I think we want to be able to validate credentials against arbitrary schemas at runtime.

Replacing git dependencies

Currently the ssi package depends on these:

Cargo book says crates.io does not allow packages to be published with git dependencies. So for publishing to crates.io, I think we need to either:

Edit: #139 means json no longer needs to be vendored. Vendoring json-ld is implemented in #138.

URI -> IRI for JSON-LD Processing

@clehner the JSON-LD spec works in IRIs instead of URIs, should we update our implementation and variable naming to accommodate for this? I realize that a lot of our data structures are not JSON-LD specific, but perhaps it would be cleaner to adopt IRI wholesale. I believe this would affect only variable naming in most instances, as Rust strings support UTF-8.

Do you have thoughts on this?

References:

Build Fails on MacOS Catalina 10.15.7

Issue description

The http-jni-docs branch fails to build on MacOS Catalina with the following rustc version:

% rustc --version
rustc 1.50.0-nightly (da3846948 2020-11-21) 

Steps to reproduce the issue

  1. Run MacOS
  2. git clone [email protected]:spruceid/ssi.git
  3. git checkout http-jni-docs
  4. cargo build

What's the expected result?

  • Build success.

What's the actual result?

  • Build failure.

Build logs

didkit % cargo build
warning: unused import: `Statement`
 --> /Users/wayne/work/ssi/src/jsonld.rs:6:5
  |
6 |     Statement,
  |     ^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: unused variable: `identifier`
  --> /Users/wayne/work/ssi/src/jsonld.rs:69:9
   |
69 |     let identifier = match identifier {
   |         ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_identifier`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: unused variable: `node_map`
   --> /Users/wayne/work/ssi/src/jsonld.rs:329:5
    |
329 |     node_map: &NodeMap,
    |     ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_node_map`

warning: unused variable: `dataset`
   --> /Users/wayne/work/ssi/src/jsonld.rs:330:5
    |
330 |     dataset: &mut DataSet,
    |     ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_dataset`

warning: unused variable: `options`
   --> /Users/wayne/work/ssi/src/jsonld.rs:331:5
    |
331 |     options: Option<&JsonLdOptions>,
    |     ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_options`

warning: field is never read: `produce_generalized_rdf`
  --> /Users/wayne/work/ssi/src/jsonld.rs:23:5
   |
23 |     produce_generalized_rdf: Option<bool>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

warning: field is never read: `rdf_direction`
  --> /Users/wayne/work/ssi/src/jsonld.rs:24:5
   |
24 |     rdf_direction: Option<RdfDirection>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: 7 warnings emitted

   Compiling didkit_cbindings v0.1.0 (/Users/wayne/work/didkit/lib/cbindings)
   Compiling didkit-http v0.0.1 (/Users/wayne/work/didkit/http)
error: future cannot be sent between threads safely
   --> http/src/lib.rs:164:9
    |
164 | /         Box::pin(async move {
165 | |             let body_reader = hyper::body::aggregate(req).await?.reader();
166 | |             let issue_req: IssueCredentialRequest = match serde_json::from_reader(body_reader) {
167 | |                 Ok(reader) => reader,
...   |
188 | |                 .map_err(|err| err.into())
189 | |         })
    | |__________^ future created by async block is not `Send`
    |
    = help: the trait `Send` is not implemented for `(dyn StdError + 'static)`
note: future is not `Send` as this value is used across an await
   --> http/src/lib.rs:178:28
    |
177 |                   Err(err) => {
    |                       --- has type `didkit::Error` which is not `Send`
178 |                       return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |  ____________________________^
179 | |                         .await;
    | |______________________________^ await occurs here, with `err` maybe used later
180 |                   }
    |                   - `err` is later dropped here
    = note: required for the cast to the object type `dyn Future<Output = std::result::Result<Response<Body>, error::Error>> + Send`

error: future cannot be sent between threads safely
   --> http/src/lib.rs:164:9
    |
164 | /         Box::pin(async move {
165 | |             let body_reader = hyper::body::aggregate(req).await?.reader();
166 | |             let issue_req: IssueCredentialRequest = match serde_json::from_reader(body_reader) {
167 | |                 Ok(reader) => reader,
...   |
188 | |                 .map_err(|err| err.into())
189 | |         })
    | |__________^ future created by async block is not `Send`
    |
    = help: the trait `Sync` is not implemented for `(dyn StdError + 'static)`
note: future is not `Send` as this value is used across an await
   --> http/src/lib.rs:178:28
    |
178 |                       return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |  ____________________________^
179 | |                         .await;
    | |______________________________^ first, await occurs here, with `err` maybe used later...
note: `err` is later dropped here
   --> http/src/lib.rs:179:31
    |
178 |                     return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |                                                                              --- has type `&didkit::Error` which is not `Send`
179 |                         .await;
    |                               ^
help: consider moving this into a `let` binding to create a shorter lived borrow
   --> http/src/lib.rs:178:78
    |
178 |                     return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |                                                                              ^^^^^^^^^^^^^^^
    = note: required for the cast to the object type `dyn Future<Output = std::result::Result<Response<Body>, error::Error>> + Send`

error: future cannot be sent between threads safely
   --> http/src/lib.rs:246:9
    |
246 | /         Box::pin(async move {
247 | |             let body_reader = hyper::body::aggregate(req).await?.reader();
248 | |             let issue_req: ProvePresentationRequest = match serde_json::from_reader(body_reader) {
249 | |                 Ok(reader) => reader,
...   |
270 | |                 .map_err(|err| err.into())
271 | |         })
    | |__________^ future created by async block is not `Send`
    |
    = help: the trait `Send` is not implemented for `(dyn StdError + 'static)`
note: future is not `Send` as this value is used across an await
   --> http/src/lib.rs:260:28
    |
259 |                   Err(err) => {
    |                       --- has type `didkit::Error` which is not `Send`
260 |                       return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |  ____________________________^
261 | |                         .await;
    | |______________________________^ await occurs here, with `err` maybe used later
262 |                   }
    |                   - `err` is later dropped here
    = note: required for the cast to the object type `dyn Future<Output = std::result::Result<Response<Body>, error::Error>> + Send`

error: future cannot be sent between threads safely
   --> http/src/lib.rs:246:9
    |
246 | /         Box::pin(async move {
247 | |             let body_reader = hyper::body::aggregate(req).await?.reader();
248 | |             let issue_req: ProvePresentationRequest = match serde_json::from_reader(body_reader) {
249 | |                 Ok(reader) => reader,
...   |
270 | |                 .map_err(|err| err.into())
271 | |         })
    | |__________^ future created by async block is not `Send`
    |
    = help: the trait `Sync` is not implemented for `(dyn StdError + 'static)`
note: future is not `Send` as this value is used across an await
   --> http/src/lib.rs:260:28
    |
260 |                       return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |  ____________________________^
261 | |                         .await;
    | |______________________________^ first, await occurs here, with `err` maybe used later...
note: `err` is later dropped here
   --> http/src/lib.rs:261:31
    |
260 |                     return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |                                                                              --- has type `&didkit::Error` which is not `Send`
261 |                         .await;
    |                               ^
help: consider moving this into a `let` binding to create a shorter lived borrow
   --> http/src/lib.rs:260:78
    |
260 |                     return Self::response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
    |                                                                              ^^^^^^^^^^^^^^^
    = note: required for the cast to the object type `dyn Future<Output = std::result::Result<Response<Body>, error::Error>> + Send`

error: aborting due to 4 previous errors

error: could not compile `didkit-http`

To learn more, run the command again with --verbose.
didkit % rustc --version
rustc 1.50.0-nightly (da3846948 2020-11-21)

Create dependency security/reliability audit strategy

We should minimize external dependencies where possible. Towards this end, we should do a cost-benefit analysis of our dependencies and their dependencies (and so on) to see if they warrant being in the branch. Things we want to avoid:

  • Unmaintained dependencies that are not responsive to bug fixes
  • Dependencies of poor quality that already have bugs or security vulnerabilities
  • Dependencies that pull in binaries or non-rust artifacts during compile or runtime
    etc.....

Todo:

  • Find resources online that can guide us in Rust dependency review
  • Come up with a general review strategy ourselves
  • Consult an expert on how to do this and receive feedback on our approach

Setup CI/CD

  • Compilation checks
  • Unit testing
  • Integration testing

Linked data proof / verification method types needing specification

Proof type Verification method type Used with
Ed25519BLAKE2BDigestSize20Base58CheckEncodedSignature2021 Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 did:tz:tz1
P256BLAKE2BDigestSize20Base58CheckEncodedSignature2021 P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021 did:tz:tz3
Eip712Signature2021 Eip712Method2021 did:ethr (experimental)
SolanaSignature2021 SolanaMethod2021 did:sol (experimental) or did:pkh:sol (did:pkh:solana)
AleoSignature2021 AleoMethod2021 or BlockchainVerificationMethod2021 did:pkh:aleo
TezosJcsSignature2021 TezosMethod2021 did:tz or did:pkh:tz
TezosSignature2021 TezosMethod2021 did:tz or did:pkh:tz
EthereumPersonalSignature2021 EcdsaSecp256k1VerificationKey2019 or EcdsaSecp256k1RecoveryMethod2020

Consider moving DID methods out of this repo

By putting DID methods into their own repos but implementing the DID traits, we can ensure maximum modularity and extensibility. We should consider how this affects SSI and DIDKit, and if it would be worthwhile. What are the considerations and major changes?

Implement std::error

To release ssi publicly, we should use thiserror to easily derive std::error::Error and make the API even more easy to use.

The main benefit of std::error::Error is the chain of errors, makes debugging easier.

Consider ID2020 Certification Requirements

ID2020 is an organization committed to solving important human rights and privacy issues globally, especially in the global south. They have technical and other requirements to certify software to their standards. We should use these to inform our roadmap, implementation, and products, then attaining certification if appropriate.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.