TBD54566975 / web5-go

Apache License 2.0
10 stars 6 forks source link

Foundations #2

Closed mistermoe closed 7 months ago

mistermoe commented 7 months ago

This PR is Chonky McChonkerson, which i'm trying to accommodate for by writing up this long description / internal monologue.

[!WARNING] I would not look at this PR 1 commit at a time or file by file. Probably easiest to read the write-up below and look at whatever chunks are of interest or that i've flagged

Features

Crypto

Usage

dsa

Key Generation

the dsa package provides algorithm IDs that can be passed to the GenerateKey function e.g.

package main

import (
    "fmt"

        "github.com/tbd54566975/web5-go/crypto/dsa"
)

func main() {
    privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmID.SECP256K1)
    if err != nil {
        fmt.Printf("Failed to generate private key: %v\n", err)
        return
    }
}

Signing

Signing takes a private key and a payload to sign. e.g.

package main

import (
    "fmt"

        "github.com/tbd54566975/web5-go/crypto/dsa"
)

func main() {
    // Generate private key
    privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmID.SECP256K1)
    if err != nil {
        fmt.Printf("Failed to generate private key: %v\n", err)
        return
    }

    // Payload to be signed
    payload := []byte("hello world")

    // Signing the payload
    signature, err := dsa.Sign(payload, privateJwk)
    if err != nil {
        fmt.Printf("Failed to sign: %v\n", err)
        return
    }
}

Verifying

Verifying takes a public key, the payload that was signed, and the signature. e.g.

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/crypto/dsa"
)

func main() {
    // Generate ED25519 private key
    privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmID.ED25519)
    if err != nil {
        fmt.Printf("Failed to generate private key: %v\n", err)
        return
    }

    // Payload to be signed
    payload := []byte("hello world")

    // Sign the payload
    signature, err := dsa.Sign(payload, privateJwk)
    if err != nil {
        fmt.Printf("Failed to sign: %v\n", err)
        return
    }

    // Get the public key from the private key
    publicJwk := dsa.GetPublicKey(privateJwk)

    // Verify the signature
    legit, err := dsa.Verify(payload, signature, publicJwk)
    if err != nil {
        fmt.Printf("Failed to verify: %v\n", err)
        return
    }

    if !legit {
        fmt.Println("Failed to verify signature")
    } else {
        fmt.Println("Signature verified successfully")
    }
}

[!NOTE] ecdsa and eddsa provide the same high level api as dsa, but specifically for algorithms within those respective families. this makes it so that if you add an additional algorithm, it automatically gets picked up by dsa as well.

Directory Structure

crypto
├── README.md
├── doc.go
├── dsa
│   ├── README.md
│   ├── dsa.go
│   ├── dsa_test.go
│   ├── ecdsa
│   │   ├── ecdsa.go
│   │   ├── secp256k1.go
│   │   └── secp256k1_test.go
│   └── eddsa
│       ├── ed25519.go
│       └── eddsa.go
├── keymanager.go
└── keymanager_test.go

Rationale

Why compartmentalize dsa?

to make room for non signature related crypto in the future if need be


why compartmentalize ecdsa and eddsa ?

DIDs

[!NOTE] wtf is a Bearer DID? BearerDID is a term i came up with in a state of delirium in order to distinguish between a DID (aka did:ex:moegrammer aka a string) and a DID + a key manager containing private keys associated to the DID. Bearer because..

The term "bearer" in the context of identity and access management originates from the concept of "bearer instruments" in financial services. In finance, a bearer instrument is a document that entitles the holder or "bearer" to the rights or assets it represents. The key characteristic of a bearer instrument is that it grants ownership or rights to whoever physically holds it, without necessarily identifying that person.

Applying this concept to the digital realm, particularly in security and authentication, a bearer token functions similarly.

In summary, the term "bearer" in identity and access management is borrowed from the financial concept of bearer instruments, emphasizing the importance of possession in determining access rights or ownership. This parallel underscores the need for careful security measures in the management of bearer tokens in digital systems.

Usage

DID Creation

did:jwk

Creating a did:jwk

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
)

func main() {
    // Create a new DID
    did, err := dids.NewDIDJWK()
    if err != nil {
        fmt.Printf("Failed to create new DID: %v\n", err)
        return
    }

    // Output the created DID
    fmt.Printf("New DID created: %s\n", did.URI)
}

[!NOTE] if no arguments are provided, uses LocalKeyManager by default and uses Ed25519 to generate key

Providing a custom key manager can be done like so:

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
)

func main() {
    km, err := AWSKeyManager()
    if err != nil {
        fmt.Errorf("failed to initialize AWS Key Manager. %w", err)
    }

    did, err := dids.NewDIDJWK(KeyManager(km))
    if err != nil {
        fmt.Printf("Failed to create new DID: %v\n", err)
        return
    }

    fmt.Printf("New DID created: %s\n", did.URI)
}

[!WARNING] AWSKeyManager doesn't exist yet in web5-go but will soon

Overriding the default Alogithm ID can be done like so:

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
)

func main() {   
    did, err := dids.NewDIDJWK(AlgorithmID(dsa.AlgorithmID.ED25519))
    if err != nil {
        fmt.Printf("Failed to create new DID: %v\n", err)
        return
    }

    fmt.Printf("New DID created: %s\n", did.URI)
}

[!IMPORTANT] Options can be passed in any order and are not mutually exclusive. so you can provide a custom key manager and override the algorithm

Importing/Exporting a DID

In scenarios where a Secrets Manager is being used instead of a HSM based KMS, you'll want to create your DID once, export the key material, save it in a Secrets Manager, and import it whenever the process that wants to use it to sign things etc.

Exporting can be done like so:

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
)

func main() {   
    did, err := dids.NewDIDJWK()
    if err != nil {
        fmt.Printf("Failed to create new DID: %v\n", err)
        return
    }

    portableDID, _ := did.ToKeys()
    bytes, _ := json.Marshal(&portableDID)

    fmt.Println(string(bytes)) // SAVE OUTPUT somewhere safe
}

on the flip side, importing a DID can be done like so:

[!NOTE] Example assumes Key Material is being passed to process via environment variables

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
)

func main() {
    portableDID := os.Getenv("SEC_DID")
    did, err := dids.BearerDIDFromKeys(portableDID)
}

[!NOTE] importing / exporting will work the same exact way regardless of the DID method. we will have did:dht support shortly

[!NOTE] why ToKeys and BearerDIDFromKeys? in order to stay consistent with our other SDKs. export / import are analagous

DID Resolution

Resolving a DID can be done like so:

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
)

func main() {   
    resolutionResult := dids.Resolve("did:jwk:eY...")
    if (resolutionResult.GetError() != "") {
        fmt.Errorf("resolution failed: %s", resolutionResult.GetError())
    }
}

[!IMPORTANT] the default resolver e.g. what Resolve is using works for all DID methods implemented in web5-go. I'm not sure if me providing a "singleton" is a nono in go

[!NOTE] why no err? to remain spec complaint. the spec states that all errors should be surfaced as part of the resolution result

Directory Structure

dids
├── bearerdid.go
├── bearerdid_test.go
├── did.go
├── did_test.go
├── didjwk.go
├── didjwk_test.go
├── document.go
├── document_test.go
├── resolution.go
└── resolver.go

Rationale

Ideally, we would have one package per DID method and all of the common data structures in another package as that's what the tight-jean hipsters would prefer so we could get didjwk.New() over dids.NewDIDJWK().

However, despite my best attempts, i couldn't find a clean way to do so that didn't lead to a weird number of packages in order to prevent a cyclic dependency graph

I've left the directory structure flat for now incase anyone else has better ideas

JWS

Signing:

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
    "github.com/tbd54566975/web5-go/jws"
)

func main() {   
    did, err := dids.NewDIDJWK()
    if err != nil {
        fmt.Errorf("failed to create did: %w", err)
    }

    payload := map[string]interface{}{"hello": "world"}

    compactJWS, err := jws.Sign(payload, did)
    if err != nil {
        fmt.Errorf("failed to sign: %w", err)
    }

    fmt.Printf("compact JWS: %s", compactJWS)
}

returning a JWS with detatched content can be done like so:

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
    "github.com/tbd54566975/web5-go/jws"
)

func main() {   
    did, err := dids.NewDIDJWK()
    if err != nil {
        fmt.Errorf("failed to create did: %w", err)
    }

    payload := map[string]interface{}{"hello": "world"}

    compactJWS, err := jws.Sign(payload, did, Detatched(true))
    if err != nil {
        fmt.Errorf("failed to sign: %w", err)
    }

    fmt.Printf("compact JWS: %s", compactJWS)
}

specifying a specific category of key to use relative to the did provided can be done like so:

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
    "github.com/tbd54566975/web5-go/jws"
)

func main() {   
    did, err := dids.NewDIDJWK()
    if err != nil {
        fmt.Errorf("failed to create did: %w", err)
    }

    payload := map[string]interface{}{"hello": "world"}

    compactJWS, err := jws.Sign(payload, did, Purpose("authentication"))
    if err != nil {
        fmt.Errorf("failed to sign: %w", err)
    }

    fmt.Printf("compact JWS: %s", compactJWS)
}

Verifying

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
    "github.com/tbd54566975/web5-go/jws"
)

func main() {   
    compactJWS := "SOME_JWS"
    ok, err := jws.Verify(compactJWS)
    if (err != nil) {
        fmt.Errorf("failed to verify JWS: %w", err)
    }

    if (!ok) {
        fmt.Errorf("integrity check failed")
    }
}

[!NOTE] an error is returned if something in the process of verification failed whereas !ok means the signature is actually shot

Directory Structure

jws
├── jws.go
└── jws_test.go

Rationale

bc i wanted jws.Sign and jws.Verify hipster vibes

JWT

Sign

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
    "github.com/tbd54566975/web5-go/jwt"
)

func main() {   
    did, err := dids.NewDIDJWK()
    if err != nil {
        t.Fatal(err)
    }

    claims := jwt.Claims{
        Issuer: did.URI,
        Misc:   map[string]interface{}{"c_nonce": "abcd123"},
    }

    jwt, err := jwt.Sign(claims, did)
    if err != nil {
        fmt.Errorf("failed to compute JWT: %w", err)
    }
}

[!IMPORTANT] I had to do some interesting kakamimi to flatten misc (aka unregistered) claims when marshaling / unmarshaling. Reviewers please take a look here and advise if theres a better way!

Verifying

package main

import (
    "fmt"
    "github.com/tbd54566975/web5-go/dids"
    "github.com/tbd54566975/web5-go/jwt"
)

func main() {
    someJWT := "SOME_JWT"
    ok, err := jwt.Verify(signedJWT)
    if err != nil {
        fmt.Errorf("failed to compute JWT: %w", err)
    }

    if (!ok) {
        fmt.Errorf("dookie JWT")
    }
}

specifying a specific category of key to use relative to the did provided can be done in the same way shown with jws.Sign

Directory Structure

jwt
├── jwt.go
└── jwt_test.go

Rationale

same as jws.

Final Thoughts

I started writing READMEs per package but didn't get around to finishing. was hoping to include instructions on how to add new DID methods, additional crypto algorithms etc. Will add that in in subsequent PRs


All of this was written during 30hrs of meetings this week and from 12am-3am so there's a chance it sucks. apologies.

alecthomas commented 7 months ago

🎉🥳