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
secp256k1 keygen, deterministic signing, and verification
ed25519 keygen, signing, and verification
higher-level API for ecdsa (Elliptic Curve Digital Signature Algorithm)
higher-level API for eddsa (Edwards-Curve Digital Signature Algorithm)
higher level API for dsa in general (Digital Signature Algorithm)
KeyManager interface that can leveraged to manage/use keys (create, sign etc) as desired per the given use case. examples of concrete implementations include: AWS KMS, Azure Key Vault, Google Cloud KMS, Hashicorp Vault etc
Concrete implementation of KeyManager that stores keys in memory
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.
to make room for non signature related crypto in the future if need be
why compartmentalize ecdsa and eddsa ?
because it's a family of algorithms have common behavior (e.g. private key -> public key)
to make it easier to add future algorithm support down the line e.g. secp256r1, ed448
DIDs
DID URI parsing
BearerDID concept.
BearerDID import and export
did:jwk creation and resolution
All did core spec data structures
singleton DID resolver
[!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
[!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
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
[!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.
This PR is Chonky McChonkerson, which i'm trying to accommodate for by writing up this long description / internal monologue.
Features
Crypto
ecdsa
(Elliptic Curve Digital Signature Algorithm)eddsa
(Edwards-Curve Digital Signature Algorithm)dsa
in general (Digital Signature Algorithm)KeyManager
interface that can leveraged to manage/use keys (create, sign etc) as desired per the given use case. examples of concrete implementations include: AWS KMS, Azure Key Vault, Google Cloud KMS, Hashicorp Vault etcKeyManager
that stores keys in memoryUsage
dsa
Key Generation
the
dsa
package provides algorithm IDs that can be passed to theGenerateKey
function e.g.Signing
Signing takes a private key and a payload to sign. e.g.
Verifying
Verifying takes a public key, the payload that was signed, and the signature. e.g.
Directory Structure
Rationale
Why compartmentalize
dsa
?to make room for non signature related crypto in the future if need be
why compartmentalize
ecdsa
andeddsa
?secp256r1
,ed448
DIDs
BearerDID
concept.BearerDID
import and exportdid:jwk
creation and resolutionUsage
DID Creation
did:jwk
Creating a
did:jwk
Providing a custom key manager can be done like so:
Overriding the default Alogithm ID can be done like so:
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:
on the flip side, importing a DID can be done like so:
DID Resolution
Resolving a DID can be done like so:
Directory Structure
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()
overdids.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:
returning a JWS with detatched content can be done like so:
specifying a specific category of key to use relative to the did provided can be done like so:
Verifying
Directory Structure
Rationale
bc i wanted
jws.Sign
andjws.Verify
hipster vibesJWT
Sign
Verifying
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
Rationale
same as
jws
.Final Thoughts
I started writing
README
s 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 PRsAll of this was written during 30hrs of meetings this week and from 12am-3am so there's a chance it sucks. apologies.