TBD54566975 / web5-go

Apache License 2.0
10 stars 6 forks source link

`vc` package #60

Closed mistermoe closed 7 months ago

mistermoe commented 7 months ago

Overview

API Surface

[!IMPORTANT] This is the first of at least two iterations for this package. We'll soon be introducing support for vc data model v2 which also includes changes to the security scheme (vc-jwt -> vc-jose-cose) and support for sd-jwt.

this means we'll need to distinguish v1 from v2 either via package name or function/type names. i held off from doing so now because w3cvcv1 really sucks as a package name and am hoping we can come up with something better. maybe w3vc.V1DataModel, w3vc.V2DataModel etc.

Ideally we can also provide a way to prevent consumers from needing knee-length neckbeards and having to know if they should use v1 or v2 by somehow type aliasing vc.* to v2 once it lands. have more thoughts here but will hold them for now

Full lifecycle

Demonstrates how to create, sign, and verify a Verifiable Credential using the vc package.

package main

import (
    "fmt"

    "github.com/tbd54566975/web5-go/dids/didjwk"
    "github.com/tbd54566975/web5-go/vc"
)

func main() {
    // create sample issuer and subject DIDs
    issuer, err := didjwk.Create()
    if err != nil {
        panic(err)
    }

    subject, err := didjwk.Create()
    if err != nil {
        panic(err)
    }

    // creation
    claims := vc.Claims{"id": subject.URI, "name": "Randy McRando"}
    cred := vc.Create(claims)

    // signing
    vcJWT, err := cred.Sign(issuer)
    if err != nil {
        panic(err)
    }

    // verification
    decoded, err := vc.Verify[vc.Claims](vcJWT)
    if err != nil {
        panic(err)
    }

    fmt.Println(decoded.VC.CredentialSubject["name"])
}

[!IMPORTANT] API surface allows for strongly typing credentialSubject. In the example above, you'll notice the use of vc.Claims. This is a convenience type (really just a typealias for map[string]any) provided with the package for scenarios where using a strong type is not possible or undesired

Using strong types

Demonstrates how to use a strongly typed credential subject

package main

import (
    "fmt"

    "github.com/tbd54566975/web5-go/dids/didjwk"
    "github.com/tbd54566975/web5-go/vc"
)

type KnownCustomerClaims struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func (c KnownCustomerClaims) GetID() string {
    return c.ID
}

func (c KnownCustomerClaims) SetID(id string) {
    c.ID = id
}

func main() {
    issuer, err := didjwk.Create()
    if err != nil {
        panic(err)
    }

    subject, err := didjwk.Create()
    if err != nil {
        panic(err)
    }

    claims := KnownCustomerClaims{ID: subject.URI, Name: "Randy McRando"}
    cred := vc.Create(&claims)

    vcJWT, err := cred.Sign(issuer)
    if err != nil {
        panic(err)
    }

    decoded, err := vc.Verify[KnownCustomerClaims](vcJWT)
    if err != nil {
        panic(err)
    }

    fmt.Println(decoded.VC.CredentialSubject.Name)
}

[!IMPORTANT] You'll notice that KnownCustomerClaims has GetID and SetID receiver methods which are part of the CredentialSubject interface included in the vc package. This interface is necessary due to the VC Data Model working group's stance on subject identifiers.

@decentralgabe and I proposed a change to the VC Data Model to avoid this which was declined.

Using both strong types and vc.claims interchangeably

Demonstrates how to use a mix of strongly typed and untyped credential subjects.

There will undoubtedly be scenarios where the structure of credentialSubject is known ahead of time (e.g. when issuing a credential) and also when the structure is both unknown and unnecessary (e.g. presentation exchange evaluation). This example, while not practical, demonstrates the ability to interchange between strong types and the loosely typed vc.Claims

package main

import (
    "fmt"

    "github.com/tbd54566975/web5-go/dids/didjwk"
    "github.com/tbd54566975/web5-go/vc"
)

type KnownCustomerClaims struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func (c KnownCustomerClaims) GetID() string {
    return c.ID
}

func (c KnownCustomerClaims) SetID(id string) {
    c.ID = id
}

func main() {
    issuer, err := didjwk.Create()
    if err != nil {
        panic(err)
    }

    subject, err := didjwk.Create()
    if err != nil {
        panic(err)
    }

    claims := KnownCustomerClaims{ID: subject.URI, Name: "Randy McRando"}
    cred := vc.Create(&claims)

    vcJWT, err := cred.Sign(issuer)
    if err != nil {
        panic(err)
    }

    decoded, err := vc.Verify[vc.Claims](vcJWT)
    if err != nil {
        panic(err)
    }

    fmt.Println(decoded.VC.CredentialSubject["name"])
}

Docs

I've included runnable examples in examples_test.go which also automatically get rendered and are runnable as part of the docs pages:

image
KendallWeihe commented 7 months ago

cc @leordev this PR has doc related matters regarding the examples_test.go file (not sure exactly how it works myself so just intending to notifying you)